diff --git a/api/auth_token.go b/api/auth_token.go index c66fba348a..ed594eee85 100644 --- a/api/auth_token.go +++ b/api/auth_token.go @@ -271,4 +271,5 @@ type TokenCreateRequest struct { DisplayName string `json:"display_name"` NumUses int `json:"num_uses"` Renewable *bool `json:"renewable,omitempty"` + Type string `json:"type"` } diff --git a/api/sys_auth.go b/api/sys_auth.go index 447c5d54b7..e7a9c222d8 100644 --- a/api/sys_auth.go +++ b/api/sys_auth.go @@ -73,46 +73,8 @@ func (c *Sys) DisableAuth(path string) error { return err } -// Structures for the requests/resposne are all down here. They aren't -// individually documented because the map almost directly to the raw HTTP API -// documentation. Please refer to that documentation for more details. - -type EnableAuthOptions struct { - Type string `json:"type"` - Description string `json:"description"` - Config AuthConfigInput `json:"config"` - Local bool `json:"local"` - PluginName string `json:"plugin_name,omitempty"` - SealWrap bool `json:"seal_wrap" mapstructure:"seal_wrap"` - Options map[string]string `json:"options" mapstructure:"options"` -} - -type AuthConfigInput struct { - DefaultLeaseTTL string `json:"default_lease_ttl" mapstructure:"default_lease_ttl"` - MaxLeaseTTL string `json:"max_lease_ttl" mapstructure:"max_lease_ttl"` - PluginName string `json:"plugin_name,omitempty" mapstructure:"plugin_name"` - AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" mapstructure:"audit_non_hmac_request_keys"` - AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" mapstructure:"audit_non_hmac_response_keys"` - ListingVisibility string `json:"listing_visibility,omitempty" mapstructure:"listing_visibility"` - PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" mapstructure:"passthrough_request_headers"` -} - -type AuthMount struct { - Type string `json:"type" mapstructure:"type"` - Description string `json:"description" mapstructure:"description"` - Accessor string `json:"accessor" mapstructure:"accessor"` - Config AuthConfigOutput `json:"config" mapstructure:"config"` - Local bool `json:"local" mapstructure:"local"` - SealWrap bool `json:"seal_wrap" mapstructure:"seal_wrap"` - Options map[string]string `json:"options" mapstructure:"options"` -} - -type AuthConfigOutput struct { - DefaultLeaseTTL int `json:"default_lease_ttl" mapstructure:"default_lease_ttl"` - MaxLeaseTTL int `json:"max_lease_ttl" mapstructure:"max_lease_ttl"` - PluginName string `json:"plugin_name,omitempty" mapstructure:"plugin_name"` - AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" mapstructure:"audit_non_hmac_request_keys"` - AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" mapstructure:"audit_non_hmac_response_keys"` - ListingVisibility string `json:"listing_visibility,omitempty" mapstructure:"listing_visibility"` - PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" mapstructure:"passthrough_request_headers"` -} +// Rather than duplicate, we can use modern Go's type aliasing +type EnableAuthOptions = MountInput +type AuthConfigInput = MountConfigInput +type AuthMount = MountOutput +type AuthConfigOutput = MountConfigOutput diff --git a/api/sys_mounts.go b/api/sys_mounts.go index 8a32b095e6..ec6a09a56c 100644 --- a/api/sys_mounts.go +++ b/api/sys_mounts.go @@ -132,10 +132,10 @@ type MountInput struct { Type string `json:"type"` Description string `json:"description"` Config MountConfigInput `json:"config"` - Options map[string]string `json:"options"` Local bool `json:"local"` PluginName string `json:"plugin_name,omitempty"` SealWrap bool `json:"seal_wrap" mapstructure:"seal_wrap"` + Options map[string]string `json:"options"` } type MountConfigInput struct { @@ -149,6 +149,7 @@ type MountConfigInput struct { AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" mapstructure:"audit_non_hmac_response_keys"` ListingVisibility string `json:"listing_visibility,omitempty" mapstructure:"listing_visibility"` PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" mapstructure:"passthrough_request_headers"` + TokenType string `json:"token_type,omitempty" mapstructure:"token_type"` } type MountOutput struct { @@ -170,4 +171,5 @@ type MountConfigOutput struct { AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" mapstructure:"audit_non_hmac_response_keys"` ListingVisibility string `json:"listing_visibility,omitempty" mapstructure:"listing_visibility"` PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" mapstructure:"passthrough_request_headers"` + TokenType string `json:"token_type,omitempty" mapstructure:"token_type"` } diff --git a/audit/format.go b/audit/format.go index 64b5ea59d6..76f81bf2a4 100644 --- a/audit/format.go +++ b/audit/format.go @@ -134,6 +134,7 @@ func (f *AuditFormatter) FormatRequest(ctx context.Context, w io.Writer, config Metadata: auth.Metadata, EntityID: auth.EntityID, RemainingUses: req.ClientTokenRemainingUses, + TokenType: auth.TokenType.String(), }, Request: AuditRequest{ @@ -304,6 +305,8 @@ func (f *AuditFormatter) FormatResponse(ctx context.Context, w io.Writer, config ExternalNamespacePolicies: resp.Auth.ExternalNamespacePolicies, Metadata: resp.Auth.Metadata, NumUses: resp.Auth.NumUses, + EntityID: resp.Auth.EntityID, + TokenType: resp.Auth.TokenType.String(), } } @@ -334,16 +337,17 @@ func (f *AuditFormatter) FormatResponse(ctx context.Context, w io.Writer, config Type: "response", Error: errString, Auth: AuditAuth{ + ClientToken: auth.ClientToken, + Accessor: auth.Accessor, DisplayName: auth.DisplayName, Policies: auth.Policies, TokenPolicies: auth.TokenPolicies, IdentityPolicies: auth.IdentityPolicies, ExternalNamespacePolicies: auth.ExternalNamespacePolicies, Metadata: auth.Metadata, - ClientToken: auth.ClientToken, - Accessor: auth.Accessor, RemainingUses: req.ClientTokenRemainingUses, EntityID: auth.EntityID, + TokenType: auth.TokenType.String(), }, Request: AuditRequest{ @@ -437,6 +441,7 @@ type AuditAuth struct { NumUses int `json:"num_uses,omitempty"` RemainingUses int `json:"remaining_uses,omitempty"` EntityID string `json:"entity_id"` + TokenType string `json:"token_type"` } type AuditSecret struct { diff --git a/audit/format_json_test.go b/audit/format_json_test.go index f84dcd090a..42f177306d 100644 --- a/audit/format_json_test.go +++ b/audit/format_json_test.go @@ -37,7 +37,13 @@ func TestFormatJSON_formatRequest(t *testing.T) { ExpectedStr string }{ "auth, request": { - &logical.Auth{ClientToken: "foo", Accessor: "bar", DisplayName: "testtoken", Policies: []string{"root"}}, + &logical.Auth{ + ClientToken: "foo", + Accessor: "bar", + DisplayName: "testtoken", + Policies: []string{"root"}, + TokenType: logical.TokenTypeService, + }, &logical.Request{ Operation: logical.UpdateOperation, Path: "/foo", @@ -56,7 +62,13 @@ func TestFormatJSON_formatRequest(t *testing.T) { expectedResultStr, }, "auth, request with prefix": { - &logical.Auth{ClientToken: "foo", Accessor: "bar", DisplayName: "testtoken", Policies: []string{"root"}}, + &logical.Auth{ + ClientToken: "foo", + Accessor: "bar", + DisplayName: "testtoken", + Policies: []string{"root"}, + TokenType: logical.TokenTypeService, + }, &logical.Request{ Operation: logical.UpdateOperation, Path: "/foo", @@ -127,5 +139,5 @@ func TestFormatJSON_formatRequest(t *testing.T) { } } -const testFormatJSONReqBasicStrFmt = `{"time":"2015-08-05T13:45:46Z","type":"request","auth":{"client_token":"%s","accessor":"bar","display_name":"testtoken","policies":["root"],"metadata":null},"request":{"operation":"update","path":"/foo","data":null,"wrap_ttl":60,"remote_address":"127.0.0.1","headers":{"foo":["bar"]}},"error":"this is an error"} +const testFormatJSONReqBasicStrFmt = `{"time":"2015-08-05T13:45:46Z","type":"request","auth":{"client_token":"%s","accessor":"bar","display_name":"testtoken","policies":["root"],"metadata":null,"entity_id":"","token_type":"service"},"request":{"operation":"update","path":"/foo","data":null,"wrap_ttl":60,"remote_address":"127.0.0.1","headers":{"foo":["bar"]}},"error":"this is an error"} ` diff --git a/audit/format_jsonx_test.go b/audit/format_jsonx_test.go index 5cbd5e2fb3..1fc37e904c 100644 --- a/audit/format_jsonx_test.go +++ b/audit/format_jsonx_test.go @@ -36,7 +36,13 @@ func TestFormatJSONx_formatRequest(t *testing.T) { ExpectedStr string }{ "auth, request": { - &logical.Auth{ClientToken: "foo", Accessor: "bar", DisplayName: "testtoken", Policies: []string{"root"}}, + &logical.Auth{ + ClientToken: "foo", + Accessor: "bar", + DisplayName: "testtoken", + Policies: []string{"root"}, + TokenType: logical.TokenTypeService, + }, &logical.Request{ Operation: logical.UpdateOperation, Path: "/foo", @@ -53,11 +59,17 @@ func TestFormatJSONx_formatRequest(t *testing.T) { errors.New("this is an error"), "", "", - fmt.Sprintf(`bar%stesttokenrootthis is an errorbarrootupdate/foofalse127.0.0.160request`, + fmt.Sprintf(`bar%stesttokenrootservicethis is an errorbarrootupdate/foofalse127.0.0.160request`, fooSalted), }, "auth, request with prefix": { - &logical.Auth{ClientToken: "foo", Accessor: "bar", DisplayName: "testtoken", Policies: []string{"root"}}, + &logical.Auth{ + ClientToken: "foo", + Accessor: "bar", + DisplayName: "testtoken", + Policies: []string{"root"}, + TokenType: logical.TokenTypeService, + }, &logical.Request{ Operation: logical.UpdateOperation, Path: "/foo", @@ -74,7 +86,7 @@ func TestFormatJSONx_formatRequest(t *testing.T) { errors.New("this is an error"), "", "@cee: ", - fmt.Sprintf(`bar%stesttokenrootthis is an errorbarrootupdate/foofalse127.0.0.160request`, + fmt.Sprintf(`bar%stesttokenrootservicethis is an errorbarrootupdate/foofalse127.0.0.160request`, fooSalted), }, } diff --git a/builtin/credential/approle/path_login.go b/builtin/credential/approle/path_login.go index 24a2a9fc1c..e8c9ad7e9a 100644 --- a/builtin/credential/approle/path_login.go +++ b/builtin/credential/approle/path_login.go @@ -304,6 +304,15 @@ func (b *backend) pathLoginUpdate(ctx context.Context, req *logical.Request, dat BoundCIDRs: tokenBoundCIDRs, } + switch role.TokenType { + case "default": + auth.TokenType = logical.TokenTypeDefault + case "batch": + auth.TokenType = logical.TokenTypeBatch + case "service": + auth.TokenType = logical.TokenTypeService + } + return &logical.Response{ Auth: auth, }, nil diff --git a/builtin/credential/approle/path_role.go b/builtin/credential/approle/path_role.go index a21261ceb7..d7008cab06 100644 --- a/builtin/credential/approle/path_role.go +++ b/builtin/credential/approle/path_role.go @@ -84,6 +84,9 @@ type roleStorageEntry struct { // SecretIDPrefix is the storage prefix for persisting secret IDs. This // differs based on whether the secret IDs are cluster local or not. SecretIDPrefix string `json:"secret_id_prefix" mapstructure:"secret_id_prefix"` + + // TokenType is the type of token to generate + TokenType string `json:"token_type" mapstructure:"token_type"` } // roleIDStorageEntry represents the reverse mapping from RoleID to Role @@ -196,6 +199,11 @@ TTL will be set to the value of this parameter.`, Description: `If set, the secret IDs generated using this role will be cluster local. This can only be set during role creation and once set, it can't be reset later.`, }, + "token_type": &framework.FieldSchema{ + Type: framework.TypeString, + Default: "default", + Description: `The type of token to generate ("service" or "batch"), or "default" to use the default`, + }, }, ExistenceCheck: b.pathRoleExistenceCheck, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -1007,6 +1015,30 @@ func (b *backend) pathRoleCreateUpdate(ctx context.Context, req *logical.Request role.TokenMaxTTL = time.Second * time.Duration(data.Get("token_max_ttl").(int)) } + tokenType := role.TokenType + if tokenTypeRaw, ok := data.GetOk("token_type"); ok { + tokenType = tokenTypeRaw.(string) + switch tokenType { + case "": + tokenType = "default" + case "service", "batch", "default": + default: + return logical.ErrorResponse(fmt.Sprintf("invalid 'token_type' value %q", tokenType)), nil + } + } else if req.Operation == logical.CreateOperation { + tokenType = data.Get("token_type").(string) + } + role.TokenType = tokenType + + if role.TokenType == "batch" { + if role.Period != 0 { + return logical.ErrorResponse("'token_type' cannot be 'batch' when role is set to generate periodic tokens"), nil + } + if role.TokenNumUses != 0 { + return logical.ErrorResponse("'token_type' cannot be 'batch' when role is set to generate tokens with limited use count"), nil + } + } + // Check that the TokenTTL value provided is less than the TokenMaxTTL. // Sanitizing the TTL and MaxTTL is not required now and can be performed // at credential issue time. @@ -1061,6 +1093,7 @@ func (b *backend) pathRoleRead(ctx context.Context, req *logical.Request, data * "token_num_uses": role.TokenNumUses, "token_ttl": role.TokenTTL / time.Second, "local_secret_ids": false, + "token_type": role.TokenType, } if role.SecretIDPrefix == secretIDLocalPrefix { diff --git a/builtin/credential/approle/path_role_test.go b/builtin/credential/approle/path_role_test.go index 06f5682c82..6ffa055195 100644 --- a/builtin/credential/approle/path_role_test.go +++ b/builtin/credential/approle/path_role_test.go @@ -1159,6 +1159,7 @@ func TestAppRole_RoleCRUD(t *testing.T) { "secret_id_bound_cidrs": []string{"127.0.0.1/32", "127.0.0.1/16"}, "bound_cidr_list": []string{"127.0.0.1/32", "127.0.0.1/16"}, // returned for backwards compatibility "token_bound_cidrs": []string{}, + "token_type": "default", } var expectedStruct roleStorageEntry @@ -1637,6 +1638,7 @@ func TestAppRole_RoleWithTokenBoundCIDRsCRUD(t *testing.T) { "token_bound_cidrs": []string{"127.0.0.1/32", "127.0.0.1/16"}, "secret_id_bound_cidrs": []string{"127.0.0.1/32", "127.0.0.1/16"}, "bound_cidr_list": []string{"127.0.0.1/32", "127.0.0.1/16"}, // provided for backwards compatibility + "token_type": "default", } var expectedStruct roleStorageEntry diff --git a/command/auth_enable.go b/command/auth_enable.go index f871264110..70bb23d646 100644 --- a/command/auth_enable.go +++ b/command/auth_enable.go @@ -30,6 +30,7 @@ type AuthEnableCommand struct { flagOptions map[string]string flagLocal bool flagSealWrap bool + flagTokenType string flagVersion int } @@ -162,6 +163,12 @@ func (c *AuthEnableCommand) Flags() *FlagSets { Usage: "Enable seal wrapping of critical values in the secrets engine.", }) + f.StringVar(&StringVar{ + Name: flagNameTokenType, + Target: &c.flagTokenType, + Usage: "Sets a forced token type for the mount.", + }) + f.IntVar(&IntVar{ Name: "version", Target: &c.flagVersion, @@ -257,6 +264,10 @@ func (c *AuthEnableCommand) Run(args []string) int { if fl.Name == flagNamePassthroughRequestHeaders { authOpts.Config.PassthroughRequestHeaders = c.flagPassthroughRequestHeaders } + + if fl.Name == flagNameTokenType { + authOpts.Config.TokenType = c.flagTokenType + } }) if err := client.Sys().EnableAuthWithOptions(authPath, authOpts); err != nil { diff --git a/command/auth_list.go b/command/auth_list.go index 8a917ef919..61794fb40d 100644 --- a/command/auth_list.go +++ b/command/auth_list.go @@ -143,7 +143,7 @@ func (c *AuthListCommand) detailedMounts(auths map[string]*api.AuthMount) []stri } } - out := []string{"Path | Type | Accessor | Plugin | Default TTL | Max TTL | Replication | Seal Wrap | Options | Description"} + out := []string{"Path | Type | Accessor | Plugin | Default TTL | Max TTL | Token Type | Replication | Seal Wrap | Options | Description"} for _, path := range paths { mount := auths[path] @@ -155,13 +155,14 @@ func (c *AuthListCommand) detailedMounts(auths map[string]*api.AuthMount) []stri replication = "local" } - out = append(out, fmt.Sprintf("%s | %s | %s | %s | %s | %s | %s | %t | %v | %s", + out = append(out, fmt.Sprintf("%s | %s | %s | %s | %s | %s | %s | %s | %t | %v | %s", path, mount.Type, mount.Accessor, mount.Config.PluginName, defaultTTL, maxTTL, + mount.Config.TokenType, replication, mount.SealWrap, mount.Options, diff --git a/command/auth_tune.go b/command/auth_tune.go index 100cf6825b..4a00a422ba 100644 --- a/command/auth_tune.go +++ b/command/auth_tune.go @@ -25,6 +25,7 @@ type AuthTuneCommand struct { flagListingVisibility string flagMaxLeaseTTL time.Duration flagOptions map[string]string + flagTokenType string flagVersion int } @@ -112,6 +113,12 @@ func (c *AuthTuneCommand) Flags() *FlagSets { "This can be specified multiple times.", }) + f.StringVar(&StringVar{ + Name: flagNameTokenType, + Target: &c.flagTokenType, + Usage: "Sets a forced token type for the mount.", + }) + f.IntVar(&IntVar{ Name: "version", Target: &c.flagVersion, @@ -184,6 +191,10 @@ func (c *AuthTuneCommand) Run(args []string) int { if fl.Name == flagNameListingVisibility { mountConfigInput.ListingVisibility = c.flagListingVisibility } + + if fl.Name == flagNameTokenType { + mountConfigInput.TokenType = c.flagTokenType + } }) // Append /auth (since that's where auths live) and a trailing slash to diff --git a/command/commands.go b/command/commands.go index 1efc8f9f12..104ad6040f 100644 --- a/command/commands.go +++ b/command/commands.go @@ -92,6 +92,8 @@ const ( flagNameListingVisibility = "listing-visibility" // flagNamePassthroughRequestHeaders is the flag name used to set passthrough request headers to the backend flagNamePassthroughRequestHeaders = "passthrough-request-headers" + // flagNameTokenType is the flag name used to force a specific token type + flagNameTokenType = "token-type" ) var ( diff --git a/command/token_create.go b/command/token_create.go index fd269d947a..207612a577 100644 --- a/command/token_create.go +++ b/command/token_create.go @@ -26,6 +26,7 @@ type TokenCreateCommand struct { flagNoDefaultPolicy bool flagUseLimit int flagRole string + flagType string flagMetadata map[string]string flagPolicies []string @@ -153,6 +154,13 @@ func (c *TokenCreateCommand) Flags() *FlagSets { "must have permission for \"auth/token/create/\".", }) + f.StringVar(&StringVar{ + Name: "type", + Target: &c.flagType, + Default: "service", + Usage: `The type of token to create. Can be "service" or "batch".`, + }) + f.StringMapVar(&StringMapVar{ Name: "metadata", Target: &c.flagMetadata, @@ -213,6 +221,10 @@ func (c *TokenCreateCommand) Run(args []string) int { } } + if c.flagType == "batch" { + c.flagRenewable = false + } + client, err := c.Client() if err != nil { c.UI.Error(err.Error()) @@ -231,6 +243,7 @@ func (c *TokenCreateCommand) Run(args []string) int { Renewable: &c.flagRenewable, ExplicitMaxTTL: c.flagExplicitMaxTTL.String(), Period: c.flagPeriod.String(), + Type: c.flagType, } var secret *api.Secret diff --git a/helper/namespace/namespace.go b/helper/namespace/namespace.go index 18ccc61c7a..1d21491ac9 100644 --- a/helper/namespace/namespace.go +++ b/helper/namespace/namespace.go @@ -104,7 +104,18 @@ func Canonicalize(nsPath string) string { func SplitIDFromString(input string) (string, string) { prefix := "" slashIdx := strings.LastIndex(input, "/") - if slashIdx > 0 { + + switch { + case strings.HasPrefix(input, "b."): + prefix = "b." + input = input[2:] + + case strings.HasPrefix(input, "s."): + prefix = "s." + input = input[2:] + + case slashIdx > 0: + // Leases will never have a b./s. to start if slashIdx == len(input)-1 { return input, "" } diff --git a/helper/namespace/namespace_test.go b/helper/namespace/namespace_test.go index 022e6d1eb6..7c0499f8ad 100644 --- a/helper/namespace/namespace_test.go +++ b/helper/namespace/namespace_test.go @@ -48,6 +48,21 @@ func TestSplitIDFromString(t *testing.T) { "", "foo.foo/", }, + { + "b.foo", + "", + "b.foo", + }, + { + "s.foo", + "", + "s.foo", + }, + { + "t.foo", + "foo", + "t", + }, } for _, c := range tcases { diff --git a/helper/testhelpers/testhelpers.go b/helper/testhelpers/testhelpers.go index 929970cd41..345f5957e5 100644 --- a/helper/testhelpers/testhelpers.go +++ b/helper/testhelpers/testhelpers.go @@ -5,14 +5,33 @@ import ( "errors" "fmt" "math/rand" + "sync" "time" + log "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/xor" + vaulthttp "github.com/hashicorp/vault/http" + "github.com/hashicorp/vault/physical" + "github.com/hashicorp/vault/physical/inmem" "github.com/hashicorp/vault/vault" - "github.com/mitchellh/go-testing-interface" + testing "github.com/mitchellh/go-testing-interface" ) +type ReplicatedTestClusters struct { + PerfPrimaryCluster *vault.TestCluster + PerfSecondaryCluster *vault.TestCluster + PerfPrimaryDRCluster *vault.TestCluster + PerfSecondaryDRCluster *vault.TestCluster +} + +func (r *ReplicatedTestClusters) Cleanup() { + r.PerfPrimaryCluster.Cleanup() + r.PerfSecondaryCluster.Cleanup() + r.PerfPrimaryDRCluster.Cleanup() + r.PerfSecondaryDRCluster.Cleanup() +} + // Generates a root token on the target cluster. func GenerateRoot(t testing.T, cluster *vault.TestCluster, drToken bool) string { token, err := GenerateRootWithError(t, cluster, drToken) @@ -127,6 +146,65 @@ func WaitForReplicationState(t testing.T, c *vault.Core, state consts.Replicatio } } +func GetClusterAndCore(t testing.T, logger log.Logger) (*vault.TestCluster, *vault.TestClusterCore) { + inm, err := inmem.NewTransactionalInmem(nil, logger) + if err != nil { + t.Fatal(err) + } + inmha, err := inmem.NewInmemHA(nil, logger) + if err != nil { + t.Fatal(err) + } + + coreConfig := &vault.CoreConfig{ + Physical: inm, + HAPhysical: inmha.(physical.HABackend), + } + + cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + Logger: logger, + }) + cluster.Start() + + cores := cluster.Cores + core := cores[0] + + vault.TestWaitActive(t, core.Core) + + return cluster, core +} + +func GetFourReplicatedClusters(t testing.T) *ReplicatedTestClusters { + ret := &ReplicatedTestClusters{} + + logger := log.New(&log.LoggerOptions{ + Mutex: &sync.Mutex{}, + Level: log.Trace, + }) + // Set this lower so that state populates quickly to standby nodes + vault.HeartbeatInterval = 2 * time.Second + + ret.PerfPrimaryCluster, _ = GetClusterAndCore(t, logger.Named("perf-pri")) + + ret.PerfSecondaryCluster, _ = GetClusterAndCore(t, logger.Named("perf-sec")) + + ret.PerfPrimaryDRCluster, _ = GetClusterAndCore(t, logger.Named("perf-pri-dr")) + + ret.PerfSecondaryDRCluster, _ = GetClusterAndCore(t, logger.Named("perf-sec-dr")) + + SetupFourClusterReplication(t, ret.PerfPrimaryCluster, ret.PerfSecondaryCluster, ret.PerfPrimaryDRCluster, ret.PerfSecondaryDRCluster) + + // Wait until poison pills have been read + time.Sleep(45 * time.Second) + EnsureCoresUnsealed(t, ret.PerfPrimaryCluster) + EnsureCoresUnsealed(t, ret.PerfSecondaryCluster) + EnsureCoresUnsealed(t, ret.PerfPrimaryDRCluster) + EnsureCoresUnsealed(t, ret.PerfSecondaryDRCluster) + + return ret +} + func SetupFourClusterReplication(t testing.T, perfPrimary, perfSecondary, perfDRSecondary, perfSecondaryDRSecondary *vault.TestCluster) { // Enable dr primary _, err := perfPrimary.Cores[0].Client.Logical().Write("sys/replication/dr/primary/enable", nil) @@ -137,7 +215,7 @@ func SetupFourClusterReplication(t testing.T, perfPrimary, perfSecondary, perfDR WaitForReplicationState(t, perfPrimary.Cores[0].Core, consts.ReplicationDRPrimary) // Enable performance primary - _, err = perfPrimary.Cores[0].Client.Logical().Write("sys/replication/primary/enable", nil) + _, err = perfPrimary.Cores[0].Client.Logical().Write("sys/replication/performance/primary/enable", nil) if err != nil { t.Fatal(err) } @@ -167,7 +245,7 @@ func SetupFourClusterReplication(t testing.T, perfPrimary, perfSecondary, perfDR EnsureCoresUnsealed(t, perfDRSecondary) // get performance token - secret, err = perfPrimary.Cores[0].Client.Logical().Write("sys/replication/primary/secondary-token", map[string]interface{}{ + secret, err = perfPrimary.Cores[0].Client.Logical().Write("sys/replication/performance/primary/secondary-token", map[string]interface{}{ "id": "1", }) if err != nil { @@ -177,7 +255,7 @@ func SetupFourClusterReplication(t testing.T, perfPrimary, perfSecondary, perfDR token = secret.WrapInfo.Token // enable performace secondary - secret, err = perfSecondary.Cores[0].Client.Logical().Write("sys/replication/secondary/enable", map[string]interface{}{ + secret, err = perfSecondary.Cores[0].Client.Logical().Write("sys/replication/performance/secondary/enable", map[string]interface{}{ "token": token, "ca_file": perfPrimary.CACertPEMFile, }) diff --git a/http/handler.go b/http/handler.go index 6c9303d4ef..afd20f6c56 100644 --- a/http/handler.go +++ b/http/handler.go @@ -421,9 +421,16 @@ func handleRequestForwarding(core *vault.Core, handler http.Handler) http.Handle return } path := ns.TrimmedPath(r.URL.Path[len("/v1/"):]) - if !perfStandbyAlwaysForwardPaths.HasPath(path) { + switch { + case !perfStandbyAlwaysForwardPaths.HasPath(path): handler.ServeHTTP(w, r) return + case strings.HasPrefix(path, "auth/token/create/"): + isBatch, err := core.IsBatchTokenCreationRequest(r.Context(), path) + if err == nil && isBatch { + handler.ServeHTTP(w, r) + return + } } } diff --git a/http/logical_test.go b/http/logical_test.go index 971cec4c7c..54f8089d16 100644 --- a/http/logical_test.go +++ b/http/logical_test.go @@ -13,6 +13,7 @@ import ( "testing" "time" + "github.com/go-test/deep" log "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/helper/consts" @@ -58,8 +59,8 @@ func TestLogical(t *testing.T) { testResponseBody(t, resp, &actual) delete(actual, "lease_id") expected["request_id"] = actual["request_id"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\nactual:\n%#v\nexpected:\n%#v", actual, expected) + if diff := deep.Equal(actual, expected); diff != nil { + t.Fatal(diff) } // DELETE @@ -163,6 +164,7 @@ func TestLogical_StandbyRedirect(t *testing.T) { "explicit_max_ttl": json.Number("0"), "expire_time": nil, "entity_id": "", + "type": "service", }, "warnings": nilWarnings, "wrap_info": nil, @@ -177,8 +179,8 @@ func TestLogical_StandbyRedirect(t *testing.T) { actual["data"] = actualDataMap expected["request_id"] = actual["request_id"] delete(actual, "lease_id") - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: got %#v; expected %#v", actual, expected) + if diff := deep.Equal(actual, expected); diff != nil { + t.Fatal(diff) } //// DELETE to standby @@ -214,6 +216,7 @@ func TestLogical_CreateToken(t *testing.T) { "lease_duration": json.Number("0"), "renewable": false, "entity_id": "", + "token_type": "service", }, "warnings": nilWarnings, } diff --git a/http/sys_auth_test.go b/http/sys_auth_test.go index 01e135b59c..e47073927e 100644 --- a/http/sys_auth_test.go +++ b/http/sys_auth_test.go @@ -5,6 +5,7 @@ import ( "reflect" "testing" + "github.com/go-test/deep" "github.com/hashicorp/vault/vault" ) @@ -32,6 +33,8 @@ func TestSysAuth(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), "plugin_name": "", + "token_type": "default-service", + "force_no_cache": false, }, "local": false, "seal_wrap": false, @@ -45,6 +48,8 @@ func TestSysAuth(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), "plugin_name": "", + "token_type": "default-service", + "force_no_cache": false, }, "local": false, "seal_wrap": false, @@ -98,6 +103,8 @@ func TestSysEnableAuth(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), "plugin_name": "", + "token_type": "default-service", + "force_no_cache": false, }, "local": false, "seal_wrap": false, @@ -110,6 +117,8 @@ func TestSysEnableAuth(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), "plugin_name": "", + "force_no_cache": false, + "token_type": "default-service", }, "local": false, "seal_wrap": false, @@ -123,6 +132,8 @@ func TestSysEnableAuth(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), "plugin_name": "", + "token_type": "default-service", + "force_no_cache": false, }, "local": false, "seal_wrap": false, @@ -135,6 +146,8 @@ func TestSysEnableAuth(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), "plugin_name": "", + "token_type": "default-service", + "force_no_cache": false, }, "local": false, "seal_wrap": false, @@ -153,8 +166,8 @@ func TestSysEnableAuth(t *testing.T) { expected["data"].(map[string]interface{})[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] } - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: expected:%#v\nactual:%#v", expected, actual) + if diff := deep.Equal(actual, expected); diff != nil { + t.Fatal(diff) } } @@ -189,6 +202,8 @@ func TestSysDisableAuth(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), "plugin_name": "", + "token_type": "default-service", + "force_no_cache": false, }, "description": "token based credentials", "type": "token", @@ -202,6 +217,8 @@ func TestSysDisableAuth(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), "plugin_name": "", + "token_type": "default-service", + "force_no_cache": false, }, "description": "token based credentials", "type": "token", @@ -222,8 +239,8 @@ func TestSysDisableAuth(t *testing.T) { expected["data"].(map[string]interface{})[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"] } - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: expected:%#v\nactual:%#v", expected, actual) + if diff := deep.Equal(actual, expected); diff != nil { + t.Fatal(diff) } } @@ -263,12 +280,14 @@ func TestSysTuneAuth_nonHMACKeys(t *testing.T) { "force_no_cache": false, "audit_non_hmac_request_keys": []interface{}{"foo"}, "audit_non_hmac_response_keys": []interface{}{"bar"}, + "token_type": "default-service", }, "default_lease_ttl": json.Number("2764800"), "max_lease_ttl": json.Number("2764800"), "force_no_cache": false, "audit_non_hmac_request_keys": []interface{}{"foo"}, "audit_non_hmac_response_keys": []interface{}{"bar"}, + "token_type": "default-service", } testResponseBody(t, resp, &actual) expected["request_id"] = actual["request_id"] @@ -302,10 +321,12 @@ func TestSysTuneAuth_nonHMACKeys(t *testing.T) { "default_lease_ttl": json.Number("2764800"), "max_lease_ttl": json.Number("2764800"), "force_no_cache": false, + "token_type": "default-service", }, "default_lease_ttl": json.Number("2764800"), "max_lease_ttl": json.Number("2764800"), "force_no_cache": false, + "token_type": "default-service", } testResponseBody(t, resp, &actual) expected["request_id"] = actual["request_id"] @@ -336,10 +357,12 @@ func TestSysTuneAuth_showUIMount(t *testing.T) { "default_lease_ttl": json.Number("2764800"), "max_lease_ttl": json.Number("2764800"), "force_no_cache": false, + "token_type": "default-service", }, "default_lease_ttl": json.Number("2764800"), "max_lease_ttl": json.Number("2764800"), "force_no_cache": false, + "token_type": "default-service", } testResponseBody(t, resp, &actual) expected["request_id"] = actual["request_id"] @@ -370,11 +393,13 @@ func TestSysTuneAuth_showUIMount(t *testing.T) { "max_lease_ttl": json.Number("2764800"), "force_no_cache": false, "listing_visibility": "unauth", + "token_type": "default-service", }, "default_lease_ttl": json.Number("2764800"), "max_lease_ttl": json.Number("2764800"), "force_no_cache": false, "listing_visibility": "unauth", + "token_type": "default-service", } testResponseBody(t, resp, &actual) expected["request_id"] = actual["request_id"] diff --git a/http/sys_generate_root_test.go b/http/sys_generate_root_test.go index 1a7054b1e8..0b36fc64ba 100644 --- a/http/sys_generate_root_test.go +++ b/http/sys_generate_root_test.go @@ -9,6 +9,7 @@ import ( "reflect" "testing" + "github.com/go-test/deep" "github.com/hashicorp/vault/helper/pgpkeys" "github.com/hashicorp/vault/helper/xor" "github.com/hashicorp/vault/vault" @@ -333,6 +334,7 @@ func TestSysGenerateRoot_Update_OTP(t *testing.T) { "explicit_max_ttl": json.Number("0"), "expire_time": nil, "entity_id": "", + "type": "service", } resp = testHttpGet(t, newRootToken, addr+"/v1/auth/token/lookup-self") @@ -431,6 +433,7 @@ func TestSysGenerateRoot_Update_PGP(t *testing.T) { "explicit_max_ttl": json.Number("0"), "expire_time": nil, "entity_id": "", + "type": "service", } resp = testHttpGet(t, newRootToken, addr+"/v1/auth/token/lookup-self") @@ -440,7 +443,7 @@ func TestSysGenerateRoot_Update_PGP(t *testing.T) { expected["creation_time"] = actual["data"].(map[string]interface{})["creation_time"] expected["accessor"] = actual["data"].(map[string]interface{})["accessor"] - if !reflect.DeepEqual(actual["data"], expected) { - t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual["data"]) + if diff := deep.Equal(actual["data"], expected); diff != nil { + t.Fatal(diff) } } diff --git a/logical/auth.go b/logical/auth.go index d15cf1cdb4..7b12e39986 100644 --- a/logical/auth.go +++ b/logical/auth.go @@ -89,6 +89,9 @@ type Auth struct { // change the perceived path of the lease, even though they don't change // the request path itself. CreationPath string `json:"creation_path"` + + // TokenType is the type of token being requested + TokenType TokenType `json:"token_type"` } func (a *Auth) GoString() string { diff --git a/logical/plugin/pb/backend.pb.go b/logical/plugin/pb/backend.pb.go index f92890b94b..911bb4978d 100644 --- a/logical/plugin/pb/backend.pb.go +++ b/logical/plugin/pb/backend.pb.go @@ -527,7 +527,9 @@ type Auth struct { IdentityPolicies []string `sentinel:"" protobuf:"bytes,15,rep,name=identity_policies,json=identityPolicies,proto3" json:"identity_policies,omitempty"` // Explicit maximum lifetime for the token. Unlike normal TTLs, the maximum // TTL is a hard limit and cannot be exceeded, also counts for periodic tokens. - ExplicitMaxTTL int64 `sentinel:"" protobuf:"varint,16,opt,name=explicit_max_ttl,json=explicitMaxTtl,proto3" json:"explicit_max_ttl,omitempty"` + ExplicitMaxTTL int64 `sentinel:"" protobuf:"varint,16,opt,name=explicit_max_ttl,json=explicitMaxTtl,proto3" json:"explicit_max_ttl,omitempty"` + // TokenType is the type of token being requested + TokenType uint32 `sentinel:"" protobuf:"varint,17,opt,name=token_type,json=tokenType,proto3" json:"token_type,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -670,6 +672,13 @@ func (m *Auth) GetExplicitMaxTTL() int64 { return 0 } +func (m *Auth) GetTokenType() uint32 { + if m != nil { + return m.TokenType + } + return 0 +} + type TokenEntry struct { ID string `sentinel:"" protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` Accessor string `sentinel:"" protobuf:"bytes,2,opt,name=accessor,proto3" json:"accessor,omitempty"` @@ -688,6 +697,7 @@ type TokenEntry struct { BoundCIDRs []string `sentinel:"" protobuf:"bytes,15,rep,name=bound_cidrs,json=boundCidrs,proto3" json:"bound_cidrs,omitempty"` NamespaceID string `sentinel:"" protobuf:"bytes,16,opt,name=namespace_id,json=namespaceID,proto3" json:"namespace_id,omitempty"` CubbyholeID string `sentinel:"" protobuf:"bytes,17,opt,name=cubbyhole_id,json=cubbyholeId,proto3" json:"cubbyhole_id,omitempty"` + Type uint32 `sentinel:"" protobuf:"varint,18,opt,name=type,proto3" json:"type,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -837,6 +847,13 @@ func (m *TokenEntry) GetCubbyholeID() string { return "" } +func (m *TokenEntry) GetType() uint32 { + if m != nil { + return m.Type + } + return 0 +} + type LeaseOptions struct { TTL int64 `sentinel:"" protobuf:"varint,1,opt,name=TTL,proto3" json:"TTL,omitempty"` Renewable bool `sentinel:"" protobuf:"varint,2,opt,name=renewable,proto3" json:"renewable,omitempty"` @@ -3614,159 +3631,161 @@ var _SystemView_serviceDesc = grpc.ServiceDesc{ func init() { proto.RegisterFile("logical/plugin/pb/backend.proto", fileDescriptor_25821d34acc7c5ef) } var fileDescriptor_25821d34acc7c5ef = []byte{ - // 2462 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x59, 0x5f, 0x73, 0xdb, 0xc6, - 0x11, 0x1f, 0xfe, 0x27, 0x97, 0xff, 0xa4, 0xb3, 0xa2, 0xc2, 0x8c, 0x53, 0x33, 0x48, 0x6d, 0x2b, - 0xae, 0x4d, 0xd9, 0x4a, 0xd3, 0x38, 0xed, 0x24, 0x1d, 0x45, 0x56, 0x1c, 0x35, 0x52, 0xa2, 0x81, - 0xe8, 0xa6, 0xff, 0x66, 0x90, 0x23, 0x70, 0xa2, 0x30, 0x02, 0x01, 0xf4, 0x70, 0x90, 0xc5, 0xa7, - 0x7e, 0x8b, 0xbe, 0xf4, 0x43, 0xf4, 0xad, 0xd3, 0xb7, 0xbe, 0x75, 0x3a, 0xd3, 0xe7, 0x7e, 0x8d, - 0x7e, 0x86, 0xce, 0xed, 0x1d, 0x40, 0x80, 0xa4, 0x62, 0x67, 0xa6, 0x7d, 0xbb, 0xdb, 0xdd, 0xdb, - 0xbb, 0xdb, 0xfb, 0xed, 0x6f, 0x17, 0x24, 0xdc, 0xf5, 0xc3, 0xa9, 0xe7, 0x50, 0x7f, 0x37, 0xf2, - 0x93, 0xa9, 0x17, 0xec, 0x46, 0x93, 0xdd, 0x09, 0x75, 0x2e, 0x59, 0xe0, 0x8e, 0x22, 0x1e, 0x8a, - 0x90, 0x94, 0xa3, 0xc9, 0xe0, 0xee, 0x34, 0x0c, 0xa7, 0x3e, 0xdb, 0x45, 0xc9, 0x24, 0x39, 0xdf, - 0x15, 0xde, 0x8c, 0xc5, 0x82, 0xce, 0x22, 0x65, 0x34, 0xd8, 0x4e, 0xbd, 0x78, 0x2e, 0x0b, 0x84, - 0x27, 0xe6, 0x5a, 0xbe, 0x55, 0xf4, 0xae, 0xa4, 0x66, 0x03, 0x6a, 0x87, 0xb3, 0x48, 0xcc, 0xcd, - 0x21, 0xd4, 0xbf, 0x60, 0xd4, 0x65, 0x9c, 0x6c, 0x43, 0xfd, 0x02, 0x47, 0x46, 0x69, 0x58, 0xd9, - 0x69, 0x59, 0x7a, 0x66, 0xfe, 0x0e, 0xe0, 0x54, 0xae, 0x39, 0xe4, 0x3c, 0xe4, 0xe4, 0x36, 0x34, - 0x19, 0xe7, 0xb6, 0x98, 0x47, 0xcc, 0x28, 0x0d, 0x4b, 0x3b, 0x5d, 0xab, 0xc1, 0x38, 0x1f, 0xcf, - 0x23, 0x46, 0x7e, 0x00, 0x72, 0x68, 0xcf, 0xe2, 0xa9, 0x51, 0x1e, 0x96, 0xa4, 0x07, 0xc6, 0xf9, - 0x49, 0x3c, 0x4d, 0xd7, 0x38, 0xa1, 0xcb, 0x8c, 0xca, 0xb0, 0xb4, 0x53, 0xc1, 0x35, 0x07, 0xa1, - 0xcb, 0xcc, 0x3f, 0x95, 0xa0, 0x76, 0x4a, 0xc5, 0x45, 0x4c, 0x08, 0x54, 0x79, 0x18, 0x0a, 0xbd, - 0x39, 0x8e, 0xc9, 0x0e, 0xf4, 0x93, 0x80, 0x26, 0xe2, 0x42, 0xde, 0xc8, 0xa1, 0x82, 0xb9, 0x46, - 0x19, 0xd5, 0xcb, 0x62, 0xf2, 0x1e, 0x74, 0xfd, 0xd0, 0xa1, 0xbe, 0x1d, 0x8b, 0x90, 0xd3, 0xa9, - 0xdc, 0x47, 0xda, 0x75, 0x50, 0x78, 0xa6, 0x64, 0xe4, 0x21, 0x6c, 0xc6, 0x8c, 0xfa, 0xf6, 0x2b, - 0x4e, 0xa3, 0xcc, 0xb0, 0xaa, 0x1c, 0x4a, 0xc5, 0x37, 0x9c, 0x46, 0xda, 0xd6, 0xfc, 0x7b, 0x1d, - 0x1a, 0x16, 0xfb, 0x43, 0xc2, 0x62, 0x41, 0x7a, 0x50, 0xf6, 0x5c, 0xbc, 0x6d, 0xcb, 0x2a, 0x7b, - 0x2e, 0x19, 0x01, 0xb1, 0x58, 0xe4, 0xcb, 0xad, 0xbd, 0x30, 0x38, 0xf0, 0x93, 0x58, 0x30, 0xae, - 0xef, 0xbc, 0x46, 0x43, 0xee, 0x40, 0x2b, 0x8c, 0x18, 0x47, 0x19, 0x06, 0xa0, 0x65, 0x2d, 0x04, - 0xf2, 0xe2, 0x11, 0x15, 0x17, 0x46, 0x15, 0x15, 0x38, 0x96, 0x32, 0x97, 0x0a, 0x6a, 0xd4, 0x94, - 0x4c, 0x8e, 0x89, 0x09, 0xf5, 0x98, 0x39, 0x9c, 0x09, 0xa3, 0x3e, 0x2c, 0xed, 0xb4, 0xf7, 0x60, - 0x14, 0x4d, 0x46, 0x67, 0x28, 0xb1, 0xb4, 0x86, 0xdc, 0x81, 0xaa, 0x8c, 0x8b, 0xd1, 0x40, 0x8b, - 0xa6, 0xb4, 0xd8, 0x4f, 0xc4, 0x85, 0x85, 0x52, 0xb2, 0x07, 0x0d, 0xf5, 0xa6, 0xb1, 0xd1, 0x1c, - 0x56, 0x76, 0xda, 0x7b, 0x86, 0x34, 0xd0, 0xb7, 0x1c, 0x29, 0x18, 0xc4, 0x87, 0x81, 0xe0, 0x73, - 0x2b, 0x35, 0x24, 0xef, 0x42, 0xc7, 0xf1, 0x3d, 0x16, 0x08, 0x5b, 0x84, 0x97, 0x2c, 0x30, 0x5a, - 0x78, 0xa2, 0xb6, 0x92, 0x8d, 0xa5, 0x88, 0xec, 0xc1, 0x5b, 0x79, 0x13, 0x9b, 0x3a, 0x0e, 0x8b, - 0xe3, 0x90, 0x1b, 0x80, 0xb6, 0xb7, 0x72, 0xb6, 0xfb, 0x5a, 0x25, 0xdd, 0xba, 0x5e, 0x1c, 0xf9, - 0x74, 0x6e, 0x07, 0x74, 0xc6, 0x8c, 0xb6, 0x72, 0xab, 0x65, 0x5f, 0xd1, 0x19, 0x23, 0x77, 0xa1, - 0x3d, 0x0b, 0x93, 0x40, 0xd8, 0x51, 0xe8, 0x05, 0xc2, 0xe8, 0xa0, 0x05, 0xa0, 0xe8, 0x54, 0x4a, - 0xc8, 0x3b, 0xa0, 0x66, 0x0a, 0x8c, 0x5d, 0x15, 0x57, 0x94, 0x20, 0x1c, 0xef, 0x41, 0x4f, 0xa9, - 0xb3, 0xf3, 0xf4, 0xd0, 0xa4, 0x8b, 0xd2, 0xec, 0x24, 0x4f, 0xa0, 0x85, 0x78, 0xf0, 0x82, 0xf3, - 0xd0, 0xe8, 0x63, 0xdc, 0x6e, 0xe5, 0xc2, 0x22, 0x31, 0x71, 0x14, 0x9c, 0x87, 0x56, 0xf3, 0x95, - 0x1e, 0x91, 0x4f, 0xe0, 0xed, 0xc2, 0x7d, 0x39, 0x9b, 0x51, 0x2f, 0xf0, 0x82, 0xa9, 0x9d, 0xc4, - 0x2c, 0x36, 0x36, 0x10, 0xe1, 0x46, 0xee, 0xd6, 0x56, 0x6a, 0xf0, 0x32, 0x66, 0x31, 0x79, 0x1b, - 0x5a, 0x2a, 0x41, 0x6d, 0xcf, 0x35, 0x36, 0xf1, 0x48, 0x4d, 0x25, 0x38, 0x72, 0xc9, 0x03, 0xe8, - 0x47, 0xa1, 0xef, 0x39, 0x73, 0x3b, 0xbc, 0x62, 0x9c, 0x7b, 0x2e, 0x33, 0xc8, 0xb0, 0xb4, 0xd3, - 0xb4, 0x7a, 0x4a, 0xfc, 0xb5, 0x96, 0xae, 0x4b, 0x8d, 0x5b, 0x68, 0xb8, 0x92, 0x1a, 0x23, 0x00, - 0x27, 0x0c, 0x02, 0xe6, 0x20, 0xfc, 0xb6, 0xf0, 0x86, 0x3d, 0x79, 0xc3, 0x83, 0x4c, 0x6a, 0xe5, - 0x2c, 0x06, 0x9f, 0x43, 0x27, 0x0f, 0x05, 0xb2, 0x01, 0x95, 0x4b, 0x36, 0xd7, 0xf0, 0x97, 0x43, - 0x32, 0x84, 0xda, 0x15, 0xf5, 0x13, 0x86, 0x90, 0xd7, 0x40, 0x54, 0x4b, 0x2c, 0xa5, 0xf8, 0x59, - 0xf9, 0x59, 0xc9, 0xfc, 0x73, 0x0d, 0xaa, 0x12, 0x7c, 0xe4, 0x43, 0xe8, 0xfa, 0x8c, 0xc6, 0xcc, - 0x0e, 0x23, 0xb9, 0x41, 0x8c, 0xae, 0xda, 0x7b, 0x1b, 0x72, 0xd9, 0xb1, 0x54, 0x7c, 0xad, 0xe4, - 0x56, 0xc7, 0xcf, 0xcd, 0x64, 0x4a, 0x7b, 0x81, 0x60, 0x3c, 0xa0, 0xbe, 0x8d, 0xc9, 0xa0, 0x12, - 0xac, 0x93, 0x0a, 0x9f, 0xcb, 0xa4, 0x58, 0xc6, 0x51, 0x65, 0x15, 0x47, 0x03, 0x68, 0x62, 0xec, - 0x3c, 0x16, 0xeb, 0x64, 0xcf, 0xe6, 0x64, 0x0f, 0x9a, 0x33, 0x26, 0xa8, 0xce, 0x35, 0x99, 0x12, - 0xdb, 0x69, 0xce, 0x8c, 0x4e, 0xb4, 0x42, 0x25, 0x44, 0x66, 0xb7, 0x92, 0x11, 0xf5, 0xd5, 0x8c, - 0x18, 0x40, 0x33, 0x03, 0x5d, 0x43, 0xbd, 0x70, 0x3a, 0x97, 0x34, 0x1b, 0x31, 0xee, 0x85, 0xae, - 0xd1, 0x44, 0xa0, 0xe8, 0x99, 0x24, 0xc9, 0x20, 0x99, 0x29, 0x08, 0xb5, 0x14, 0x49, 0x06, 0xc9, - 0x6c, 0x15, 0x31, 0xb0, 0x84, 0x98, 0x1f, 0x41, 0x8d, 0xfa, 0x1e, 0x8d, 0x31, 0x85, 0xe4, 0xcb, - 0x6a, 0xbe, 0x1f, 0xed, 0x4b, 0xa9, 0xa5, 0x94, 0xe4, 0x03, 0xe8, 0x4e, 0x79, 0x98, 0x44, 0x36, - 0x4e, 0x59, 0x6c, 0x74, 0xf0, 0xb6, 0xcb, 0xd6, 0x1d, 0x34, 0xda, 0x57, 0x36, 0x32, 0x03, 0x27, - 0x61, 0x12, 0xb8, 0xb6, 0xe3, 0xb9, 0x3c, 0x36, 0xba, 0x18, 0x3c, 0x40, 0xd1, 0x81, 0x94, 0xc8, - 0x14, 0x53, 0x29, 0x90, 0x05, 0xb8, 0x87, 0x36, 0x5d, 0x94, 0x9e, 0xa6, 0x51, 0xfe, 0x31, 0x6c, - 0xa6, 0x45, 0x69, 0x61, 0xd9, 0x47, 0xcb, 0x8d, 0x54, 0x91, 0x19, 0xef, 0xc0, 0x06, 0xbb, 0x96, - 0x14, 0xea, 0x09, 0x7b, 0x46, 0xaf, 0x6d, 0x21, 0x7c, 0x9d, 0x52, 0xbd, 0x54, 0x7e, 0x42, 0xaf, - 0xc7, 0xc2, 0x1f, 0xfc, 0x1c, 0xba, 0x85, 0x37, 0x5a, 0x83, 0xd4, 0xad, 0x3c, 0x52, 0x5b, 0x79, - 0x74, 0xfe, 0xb5, 0x0a, 0x80, 0x8f, 0xa5, 0x96, 0x2e, 0x53, 0x7c, 0xfe, 0x05, 0xcb, 0x6b, 0x5e, - 0x90, 0x72, 0x16, 0x08, 0x8d, 0x36, 0x3d, 0xfb, 0x4e, 0xa0, 0xa5, 0x24, 0x5f, 0xcb, 0x91, 0xfc, - 0x23, 0xa8, 0x4a, 0x50, 0x19, 0xf5, 0x05, 0x17, 0x2f, 0x4e, 0x84, 0xf0, 0x53, 0xd0, 0x43, 0xab, - 0x15, 0xa4, 0x37, 0x56, 0x91, 0x9e, 0x87, 0x50, 0xb3, 0x08, 0xa1, 0xf7, 0xa0, 0xeb, 0x70, 0x86, - 0x05, 0xc7, 0x96, 0x9d, 0x83, 0x86, 0x58, 0x27, 0x15, 0x8e, 0xbd, 0x19, 0x93, 0xf1, 0x93, 0xd1, - 0x06, 0x54, 0xc9, 0xe1, 0xda, 0xc7, 0x68, 0xaf, 0x7b, 0x0c, 0x55, 0xbe, 0x7d, 0xa6, 0x69, 0x1a, - 0xc7, 0x39, 0xa8, 0x77, 0x0b, 0x50, 0x2f, 0xe0, 0xb9, 0xb7, 0x84, 0xe7, 0x25, 0xd0, 0xf5, 0x57, - 0x40, 0xf7, 0x2e, 0x74, 0x64, 0x00, 0xe2, 0x88, 0x3a, 0x4c, 0x3a, 0xd8, 0x50, 0x81, 0xc8, 0x64, - 0x47, 0x2e, 0xa6, 0x68, 0x32, 0x99, 0xcc, 0x2f, 0x42, 0x9f, 0x2d, 0x58, 0xb6, 0x9d, 0xc9, 0x8e, - 0xdc, 0xc1, 0x47, 0xd0, 0xca, 0x22, 0xfc, 0xbd, 0x80, 0xf3, 0x97, 0x12, 0x74, 0xf2, 0xac, 0x25, - 0x17, 0x8f, 0xc7, 0xc7, 0xb8, 0xb8, 0x62, 0xc9, 0xa1, 0xac, 0xf7, 0x9c, 0x05, 0xec, 0x15, 0x9d, - 0xf8, 0xca, 0x41, 0xd3, 0x5a, 0x08, 0xa4, 0xd6, 0x0b, 0x1c, 0xce, 0x66, 0x29, 0x82, 0x2a, 0xd6, - 0x42, 0x40, 0x3e, 0x06, 0xf0, 0xe2, 0x38, 0x61, 0xea, 0x95, 0xaa, 0x98, 0xd3, 0x83, 0x91, 0x6a, - 0xfe, 0x46, 0x69, 0xf3, 0x37, 0x1a, 0xa7, 0xcd, 0x9f, 0xd5, 0x42, 0x6b, 0x7c, 0xbe, 0x6d, 0xa8, - 0xcb, 0xc7, 0x18, 0x1f, 0x23, 0xca, 0x2a, 0x96, 0x9e, 0x99, 0x7f, 0x84, 0xba, 0x6a, 0x13, 0xfe, - 0xaf, 0x4c, 0x7c, 0x1b, 0x9a, 0xca, 0xb7, 0xe7, 0xea, 0xbc, 0x68, 0xe0, 0xfc, 0xc8, 0x35, 0xff, - 0x55, 0x82, 0xa6, 0xc5, 0xe2, 0x28, 0x0c, 0x62, 0x96, 0x6b, 0x63, 0x4a, 0xaf, 0x6d, 0x63, 0xca, - 0x6b, 0xdb, 0x98, 0xb4, 0x39, 0xaa, 0xe4, 0x9a, 0xa3, 0x01, 0x34, 0x39, 0x73, 0x3d, 0xce, 0x1c, - 0xa1, 0x1b, 0xa9, 0x6c, 0x2e, 0x75, 0xaf, 0x28, 0x97, 0xf5, 0x37, 0x46, 0x92, 0x6f, 0x59, 0xd9, - 0x9c, 0x3c, 0xcd, 0x57, 0x7f, 0xd5, 0x57, 0x6d, 0xa9, 0xea, 0xaf, 0x8e, 0xbb, 0x5a, 0xfe, 0xcd, - 0x7f, 0x96, 0x61, 0x63, 0x59, 0xbd, 0x06, 0x04, 0x5b, 0x50, 0x53, 0xf5, 0x41, 0x23, 0x48, 0xac, - 0x54, 0x86, 0xca, 0x12, 0xaf, 0xfc, 0x62, 0x39, 0x47, 0x5f, 0xff, 0xfa, 0xc5, 0xfc, 0x7d, 0x1f, - 0x36, 0xe4, 0x29, 0x23, 0xe6, 0x2e, 0x7a, 0x1e, 0x45, 0x38, 0x7d, 0x2d, 0xcf, 0xba, 0x9e, 0x87, - 0xb0, 0x99, 0x9a, 0x2e, 0x52, 0xb1, 0x5e, 0xb0, 0x3d, 0x4c, 0x33, 0x72, 0x1b, 0xea, 0xe7, 0x21, - 0x9f, 0x51, 0xa1, 0x39, 0x47, 0xcf, 0x0a, 0x9c, 0x82, 0xe4, 0xd6, 0x54, 0xb0, 0x48, 0x85, 0xb2, - 0xaf, 0x97, 0xb9, 0x9e, 0xf5, 0xdc, 0x48, 0x3a, 0x4d, 0xab, 0x99, 0xf6, 0xda, 0xe6, 0xaf, 0xa1, - 0xbf, 0xd4, 0x66, 0xad, 0x09, 0xe4, 0x62, 0xfb, 0x72, 0x61, 0xfb, 0x82, 0xe7, 0xca, 0x92, 0xe7, - 0xdf, 0xc0, 0xe6, 0x17, 0x34, 0x70, 0x7d, 0xa6, 0xfd, 0xef, 0xf3, 0x69, 0x2c, 0x1b, 0x46, 0xdd, - 0xf5, 0xdb, 0x9a, 0xec, 0xbb, 0x56, 0x4b, 0x4b, 0x8e, 0x5c, 0x72, 0x0f, 0x1a, 0x5c, 0x59, 0x6b, - 0xe0, 0xb5, 0x73, 0x7d, 0xa0, 0x95, 0xea, 0xcc, 0x6f, 0x81, 0x14, 0x5c, 0xcb, 0x86, 0x7f, 0x4e, - 0x76, 0x24, 0x00, 0x15, 0x28, 0x34, 0xb0, 0x3b, 0x79, 0x1c, 0x59, 0x99, 0x96, 0x0c, 0xa1, 0xc2, - 0x38, 0xd7, 0x5b, 0x60, 0x23, 0xb6, 0xf8, 0xbc, 0xb2, 0xa4, 0xca, 0xfc, 0x09, 0x6c, 0x9e, 0x45, - 0xcc, 0xf1, 0xa8, 0x8f, 0x9f, 0x46, 0x6a, 0x83, 0xbb, 0x50, 0x93, 0x41, 0x4e, 0x73, 0xb6, 0x85, - 0x0b, 0x51, 0xad, 0xe4, 0xe6, 0xb7, 0x60, 0xa8, 0x73, 0x1d, 0x5e, 0x7b, 0xb1, 0x60, 0x81, 0xc3, - 0x0e, 0x2e, 0x98, 0x73, 0xf9, 0x3f, 0xbc, 0xf9, 0x15, 0xdc, 0x5e, 0xb7, 0x43, 0x7a, 0xbe, 0xb6, - 0x23, 0x67, 0xf6, 0xb9, 0xa4, 0x6a, 0xdc, 0xa3, 0x69, 0x01, 0x8a, 0x3e, 0x97, 0x12, 0xf9, 0x8e, - 0x4c, 0xae, 0x8b, 0x35, 0x25, 0xea, 0x59, 0x1a, 0x8f, 0xca, 0xcd, 0xf1, 0xf8, 0x5b, 0x09, 0x5a, - 0x67, 0x4c, 0x24, 0x11, 0xde, 0xe5, 0x6d, 0x68, 0x4d, 0x78, 0x78, 0xc9, 0xf8, 0xe2, 0x2a, 0x4d, - 0x25, 0x38, 0x72, 0xc9, 0x53, 0xa8, 0x1f, 0x84, 0xc1, 0xb9, 0x37, 0xc5, 0x0f, 0xc5, 0xf6, 0xde, - 0x6d, 0xc5, 0x2e, 0x7a, 0xed, 0x48, 0xe9, 0x54, 0x59, 0xd5, 0x86, 0x64, 0x08, 0x6d, 0xfd, 0xb9, - 0xfd, 0xf2, 0xe5, 0xd1, 0xf3, 0xb4, 0x83, 0xcc, 0x89, 0x06, 0x1f, 0x43, 0x3b, 0xb7, 0xf0, 0x7b, - 0x55, 0x8b, 0x1f, 0x02, 0xe0, 0xee, 0x2a, 0x46, 0x1b, 0xea, 0xaa, 0x7a, 0xa5, 0xbc, 0xda, 0x5d, - 0x68, 0xc9, 0x8f, 0x15, 0xa5, 0x26, 0x50, 0xcd, 0x7d, 0x57, 0xe3, 0xd8, 0xbc, 0x07, 0x9b, 0x47, - 0xc1, 0x15, 0xf5, 0x3d, 0x97, 0x0a, 0xf6, 0x25, 0x9b, 0x63, 0x08, 0x56, 0x4e, 0x60, 0x9e, 0x41, - 0x47, 0x7f, 0xb9, 0xbe, 0xd1, 0x19, 0x3b, 0xfa, 0x8c, 0xdf, 0x9d, 0x44, 0xef, 0x43, 0x5f, 0x3b, - 0x3d, 0xf6, 0x74, 0x0a, 0xc9, 0x92, 0xce, 0xd9, 0xb9, 0x77, 0xad, 0x5d, 0xeb, 0x99, 0xf9, 0x0c, - 0x36, 0x72, 0xa6, 0xd9, 0x75, 0x2e, 0xd9, 0x3c, 0x4e, 0xbf, 0xe8, 0xe5, 0x38, 0x8d, 0x40, 0x79, - 0x11, 0x01, 0x13, 0x7a, 0x7a, 0xe5, 0x0b, 0x26, 0x6e, 0xb8, 0xdd, 0x97, 0xd9, 0x41, 0x5e, 0x30, - 0xed, 0xfc, 0x3e, 0xd4, 0x98, 0xbc, 0x69, 0xbe, 0x84, 0xe5, 0x23, 0x60, 0x29, 0xf5, 0x9a, 0x0d, - 0x9f, 0x65, 0x1b, 0x9e, 0x26, 0x6a, 0xc3, 0x37, 0xf4, 0x65, 0xbe, 0x97, 0x1d, 0xe3, 0x34, 0x11, - 0x37, 0xbd, 0xe8, 0x3d, 0xd8, 0xd4, 0x46, 0xcf, 0x99, 0xcf, 0x04, 0xbb, 0xe1, 0x4a, 0xf7, 0x81, - 0x14, 0xcc, 0x6e, 0x72, 0x77, 0x07, 0x9a, 0xe3, 0xf1, 0x71, 0xa6, 0x2d, 0x72, 0xa3, 0xf9, 0x09, - 0x6c, 0x9e, 0x25, 0x6e, 0x78, 0xca, 0xbd, 0x2b, 0xcf, 0x67, 0x53, 0xb5, 0x59, 0xda, 0x6b, 0x96, - 0x72, 0xbd, 0xe6, 0xda, 0x6a, 0x64, 0xee, 0x00, 0x29, 0x2c, 0xcf, 0xde, 0x2d, 0x4e, 0xdc, 0x50, - 0xa7, 0x30, 0x8e, 0xcd, 0x1d, 0xe8, 0x8c, 0xa9, 0xac, 0xf7, 0xae, 0xb2, 0x31, 0xa0, 0x21, 0xd4, - 0x5c, 0x9b, 0xa5, 0x53, 0x73, 0x0f, 0xb6, 0x0e, 0xa8, 0x73, 0xe1, 0x05, 0xd3, 0xe7, 0x5e, 0x2c, - 0x1b, 0x1e, 0xbd, 0x62, 0x00, 0x4d, 0x57, 0x0b, 0xf4, 0x92, 0x6c, 0x6e, 0x3e, 0x86, 0xb7, 0x72, - 0x3f, 0x9b, 0x9c, 0x09, 0x9a, 0xc6, 0x63, 0x0b, 0x6a, 0xb1, 0x9c, 0xe1, 0x8a, 0x9a, 0xa5, 0x26, - 0xe6, 0x57, 0xb0, 0x95, 0x2f, 0xc0, 0xb2, 0xfd, 0x48, 0x2f, 0x8e, 0x8d, 0x41, 0x29, 0xd7, 0x18, - 0xe8, 0x98, 0x95, 0x17, 0xf5, 0x64, 0x03, 0x2a, 0xbf, 0xfc, 0x66, 0xac, 0xc1, 0x2e, 0x87, 0xe6, - 0xef, 0xe5, 0xf6, 0x45, 0x7f, 0x6a, 0xfb, 0x42, 0x77, 0x50, 0x7a, 0x93, 0xee, 0x60, 0x0d, 0xde, - 0x1e, 0xc3, 0xe6, 0x89, 0x1f, 0x3a, 0x97, 0x87, 0x41, 0x2e, 0x1a, 0x06, 0x34, 0x58, 0x90, 0x0f, - 0x46, 0x3a, 0x35, 0x1f, 0x40, 0xff, 0x38, 0x74, 0xa8, 0x7f, 0x12, 0x26, 0x81, 0xc8, 0xa2, 0x80, - 0xbf, 0x63, 0x69, 0x53, 0x35, 0x31, 0x1f, 0x43, 0x4f, 0x97, 0xe8, 0xe0, 0x3c, 0x4c, 0x99, 0x71, - 0x51, 0xcc, 0x4b, 0xc5, 0xbe, 0xda, 0x3c, 0x86, 0xfe, 0xc2, 0x5c, 0xf9, 0x7d, 0x00, 0x75, 0xa5, - 0xd6, 0x77, 0xeb, 0x67, 0x5f, 0x83, 0xca, 0xd2, 0xd2, 0xea, 0x35, 0x97, 0x9a, 0x41, 0xef, 0x14, - 0x7f, 0x4f, 0x3c, 0x0c, 0xae, 0x94, 0xb3, 0x23, 0x20, 0xea, 0x17, 0x46, 0x9b, 0x05, 0x57, 0x1e, - 0x0f, 0x03, 0xec, 0x6f, 0x4b, 0xba, 0x85, 0x49, 0x1d, 0x67, 0x8b, 0x52, 0x0b, 0x6b, 0x33, 0x5a, - 0x16, 0xad, 0x8d, 0x21, 0x2c, 0x7e, 0xad, 0x90, 0xa5, 0x86, 0xb3, 0x59, 0x28, 0x98, 0x4d, 0x5d, - 0x37, 0xcd, 0x16, 0x50, 0xa2, 0x7d, 0xd7, 0xe5, 0x7b, 0xff, 0x29, 0x43, 0xe3, 0x33, 0x45, 0xe0, - 0xe4, 0x53, 0xe8, 0x16, 0xca, 0x35, 0x79, 0x0b, 0x7f, 0xae, 0x58, 0x6e, 0x0e, 0x06, 0xdb, 0x2b, - 0x62, 0x75, 0xaf, 0x27, 0xd0, 0xc9, 0x17, 0x63, 0x82, 0x85, 0x17, 0x7f, 0x3b, 0x1d, 0xa0, 0xa7, - 0xd5, 0x4a, 0x7d, 0x06, 0x5b, 0xeb, 0xca, 0x24, 0xb9, 0xb3, 0xd8, 0x61, 0xb5, 0x44, 0x0f, 0xde, - 0xb9, 0x49, 0x9b, 0x96, 0xd7, 0xc6, 0x81, 0xcf, 0x68, 0x90, 0x44, 0xf9, 0x13, 0x2c, 0x86, 0xe4, - 0x29, 0x74, 0x0b, 0x85, 0x42, 0xdd, 0x73, 0xa5, 0x76, 0xe4, 0x97, 0xdc, 0x87, 0x1a, 0x16, 0x27, - 0xd2, 0x2d, 0x54, 0xc9, 0x41, 0x2f, 0x9b, 0xaa, 0xbd, 0x87, 0x50, 0xc5, 0x5f, 0xd4, 0x72, 0x1b, - 0xe3, 0x8a, 0xac, 0x72, 0xed, 0xfd, 0xbb, 0x04, 0x8d, 0xf4, 0x57, 0xd6, 0xa7, 0x50, 0x95, 0x35, - 0x80, 0xdc, 0xca, 0xd1, 0x68, 0x5a, 0x3f, 0x06, 0x5b, 0x4b, 0x42, 0xb5, 0xc1, 0x08, 0x2a, 0x2f, - 0x98, 0x20, 0x24, 0xa7, 0xd4, 0xc5, 0x60, 0x70, 0xab, 0x28, 0xcb, 0xec, 0x4f, 0x93, 0xa2, 0xbd, - 0xe6, 0xf2, 0x82, 0x7d, 0xc6, 0xd2, 0x1f, 0x41, 0x5d, 0xb1, 0xac, 0x0a, 0xca, 0x0a, 0x3f, 0xab, - 0xc7, 0x5f, 0xe5, 0xe3, 0xbd, 0x7f, 0x54, 0x01, 0xce, 0xe6, 0xb1, 0x60, 0xb3, 0x5f, 0x79, 0xec, - 0x15, 0x79, 0x08, 0xfd, 0xe7, 0xec, 0x9c, 0x26, 0xbe, 0xc0, 0xaf, 0x25, 0xc9, 0x26, 0xb9, 0x98, - 0x60, 0xc3, 0x97, 0x91, 0xf5, 0x7d, 0x68, 0x9f, 0xd0, 0xeb, 0xd7, 0xdb, 0x7d, 0x0a, 0xdd, 0x02, - 0x07, 0xeb, 0x23, 0x2e, 0xb3, 0xba, 0x3e, 0xe2, 0x2a, 0x5b, 0xdf, 0x87, 0x86, 0x66, 0xe6, 0xfc, - 0x1e, 0x58, 0xc3, 0x0a, 0x8c, 0xfd, 0x53, 0xe8, 0x2f, 0xf1, 0x72, 0xde, 0x1e, 0x7f, 0x7d, 0x58, - 0xcb, 0xdb, 0xcf, 0xe4, 0xd7, 0x4e, 0x91, 0x9b, 0xf3, 0x0b, 0x6f, 0x2b, 0x3e, 0x5c, 0x47, 0xde, - 0x2f, 0x8a, 0xdf, 0x49, 0xf8, 0x95, 0x68, 0x2c, 0xd3, 0x67, 0x4a, 0xde, 0xa9, 0xa3, 0x75, 0x34, - 0xfc, 0x04, 0x3a, 0x79, 0x06, 0x5d, 0x49, 0xc1, 0x55, 0x7a, 0x7d, 0x04, 0xb0, 0x20, 0xd1, 0xbc, - 0x3d, 0xc2, 0x63, 0x99, 0x5f, 0x3f, 0x04, 0x58, 0x50, 0xa3, 0x42, 0x55, 0x91, 0x59, 0xd5, 0xb2, - 0x65, 0xfa, 0x7c, 0x08, 0xad, 0x8c, 0xce, 0xf2, 0x7b, 0xa0, 0x83, 0x22, 0x3b, 0x7e, 0x36, 0xfa, - 0xed, 0xa3, 0xa9, 0x27, 0x2e, 0x92, 0xc9, 0xc8, 0x09, 0x67, 0xbb, 0x17, 0x34, 0xbe, 0xf0, 0x9c, - 0x90, 0x47, 0xbb, 0x57, 0x12, 0x4c, 0xbb, 0x2b, 0x7f, 0x00, 0x4d, 0xea, 0xf8, 0xb1, 0xf7, 0xc1, - 0x7f, 0x03, 0x00, 0x00, 0xff, 0xff, 0x4e, 0xbe, 0xe0, 0xab, 0x1c, 0x1a, 0x00, 0x00, + // 2483 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x59, 0xcd, 0x72, 0x1b, 0xc7, + 0x11, 0x2e, 0x00, 0xc4, 0x5f, 0xe3, 0x8f, 0x18, 0xd1, 0xcc, 0x0a, 0x96, 0x23, 0x78, 0x1d, 0x49, + 0xb4, 0x22, 0x81, 0x12, 0x1d, 0xc7, 0x72, 0x52, 0x76, 0x8a, 0xa6, 0x68, 0x99, 0x31, 0x69, 0xb3, + 0x96, 0x50, 0x9c, 0xbf, 0x2a, 0x78, 0xb0, 0x3b, 0x04, 0xb7, 0xb8, 0xd8, 0xdd, 0xcc, 0xce, 0x52, + 0xc4, 0x29, 0x6f, 0x91, 0xd7, 0xc8, 0x35, 0x95, 0x4b, 0x6e, 0xa9, 0x54, 0x72, 0xce, 0x6b, 0xe4, + 0x19, 0x52, 0xd3, 0x33, 0xfb, 0x07, 0x80, 0x96, 0x5c, 0x95, 0xdc, 0x66, 0xba, 0x7b, 0x7a, 0x66, + 0x7a, 0xbe, 0xfe, 0xba, 0x17, 0x80, 0xbb, 0x5e, 0x30, 0x73, 0x6d, 0xea, 0xed, 0x86, 0x5e, 0x3c, + 0x73, 0xfd, 0xdd, 0x70, 0xba, 0x3b, 0xa5, 0xf6, 0x25, 0xf3, 0x9d, 0x51, 0xc8, 0x03, 0x11, 0x90, + 0x72, 0x38, 0x1d, 0xdc, 0x9d, 0x05, 0xc1, 0xcc, 0x63, 0xbb, 0x28, 0x99, 0xc6, 0xe7, 0xbb, 0xc2, + 0x9d, 0xb3, 0x48, 0xd0, 0x79, 0xa8, 0x8c, 0x06, 0xdb, 0x89, 0x17, 0xd7, 0x61, 0xbe, 0x70, 0xc5, + 0x42, 0xcb, 0xb7, 0x8a, 0xde, 0x95, 0xd4, 0xac, 0x43, 0xf5, 0x70, 0x1e, 0x8a, 0x85, 0x39, 0x84, + 0xda, 0x17, 0x8c, 0x3a, 0x8c, 0x93, 0x6d, 0xa8, 0x5d, 0xe0, 0xc8, 0x28, 0x0d, 0x2b, 0x3b, 0x4d, + 0x4b, 0xcf, 0xcc, 0xdf, 0x01, 0x9c, 0xca, 0x35, 0x87, 0x9c, 0x07, 0x9c, 0xdc, 0x86, 0x06, 0xe3, + 0x7c, 0x22, 0x16, 0x21, 0x33, 0x4a, 0xc3, 0xd2, 0x4e, 0xc7, 0xaa, 0x33, 0xce, 0xc7, 0x8b, 0x90, + 0x91, 0x1f, 0x80, 0x1c, 0x4e, 0xe6, 0xd1, 0xcc, 0x28, 0x0f, 0x4b, 0xd2, 0x03, 0xe3, 0xfc, 0x24, + 0x9a, 0x25, 0x6b, 0xec, 0xc0, 0x61, 0x46, 0x65, 0x58, 0xda, 0xa9, 0xe0, 0x9a, 0x83, 0xc0, 0x61, + 0xe6, 0x9f, 0x4a, 0x50, 0x3d, 0xa5, 0xe2, 0x22, 0x22, 0x04, 0x36, 0x78, 0x10, 0x08, 0xbd, 0x39, + 0x8e, 0xc9, 0x0e, 0xf4, 0x62, 0x9f, 0xc6, 0xe2, 0x42, 0xde, 0xc8, 0xa6, 0x82, 0x39, 0x46, 0x19, + 0xd5, 0xcb, 0x62, 0xf2, 0x1e, 0x74, 0xbc, 0xc0, 0xa6, 0xde, 0x24, 0x12, 0x01, 0xa7, 0x33, 0xb9, + 0x8f, 0xb4, 0x6b, 0xa3, 0xf0, 0x4c, 0xc9, 0xc8, 0x43, 0xe8, 0x47, 0x8c, 0x7a, 0x93, 0x57, 0x9c, + 0x86, 0xa9, 0xe1, 0x86, 0x72, 0x28, 0x15, 0xdf, 0x70, 0x1a, 0x6a, 0x5b, 0xf3, 0x6f, 0x35, 0xa8, + 0x5b, 0xec, 0x0f, 0x31, 0x8b, 0x04, 0xe9, 0x42, 0xd9, 0x75, 0xf0, 0xb6, 0x4d, 0xab, 0xec, 0x3a, + 0x64, 0x04, 0xc4, 0x62, 0xa1, 0x27, 0xb7, 0x76, 0x03, 0xff, 0xc0, 0x8b, 0x23, 0xc1, 0xb8, 0xbe, + 0xf3, 0x1a, 0x0d, 0xb9, 0x03, 0xcd, 0x20, 0x64, 0x1c, 0x65, 0x18, 0x80, 0xa6, 0x95, 0x09, 0xe4, + 0xc5, 0x43, 0x2a, 0x2e, 0x8c, 0x0d, 0x54, 0xe0, 0x58, 0xca, 0x1c, 0x2a, 0xa8, 0x51, 0x55, 0x32, + 0x39, 0x26, 0x26, 0xd4, 0x22, 0x66, 0x73, 0x26, 0x8c, 0xda, 0xb0, 0xb4, 0xd3, 0xda, 0x83, 0x51, + 0x38, 0x1d, 0x9d, 0xa1, 0xc4, 0xd2, 0x1a, 0x72, 0x07, 0x36, 0x64, 0x5c, 0x8c, 0x3a, 0x5a, 0x34, + 0xa4, 0xc5, 0x7e, 0x2c, 0x2e, 0x2c, 0x94, 0x92, 0x3d, 0xa8, 0xab, 0x37, 0x8d, 0x8c, 0xc6, 0xb0, + 0xb2, 0xd3, 0xda, 0x33, 0xa4, 0x81, 0xbe, 0xe5, 0x48, 0xc1, 0x20, 0x3a, 0xf4, 0x05, 0x5f, 0x58, + 0x89, 0x21, 0x79, 0x17, 0xda, 0xb6, 0xe7, 0x32, 0x5f, 0x4c, 0x44, 0x70, 0xc9, 0x7c, 0xa3, 0x89, + 0x27, 0x6a, 0x29, 0xd9, 0x58, 0x8a, 0xc8, 0x1e, 0xbc, 0x95, 0x37, 0x99, 0x50, 0xdb, 0x66, 0x51, + 0x14, 0x70, 0x03, 0xd0, 0xf6, 0x56, 0xce, 0x76, 0x5f, 0xab, 0xa4, 0x5b, 0xc7, 0x8d, 0x42, 0x8f, + 0x2e, 0x26, 0x3e, 0x9d, 0x33, 0xa3, 0xa5, 0xdc, 0x6a, 0xd9, 0x57, 0x74, 0xce, 0xc8, 0x5d, 0x68, + 0xcd, 0x83, 0xd8, 0x17, 0x93, 0x30, 0x70, 0x7d, 0x61, 0xb4, 0xd1, 0x02, 0x50, 0x74, 0x2a, 0x25, + 0xe4, 0x1d, 0x50, 0x33, 0x05, 0xc6, 0x8e, 0x8a, 0x2b, 0x4a, 0x10, 0x8e, 0xf7, 0xa0, 0xab, 0xd4, + 0xe9, 0x79, 0xba, 0x68, 0xd2, 0x41, 0x69, 0x7a, 0x92, 0x27, 0xd0, 0x44, 0x3c, 0xb8, 0xfe, 0x79, + 0x60, 0xf4, 0x30, 0x6e, 0xb7, 0x72, 0x61, 0x91, 0x98, 0x38, 0xf2, 0xcf, 0x03, 0xab, 0xf1, 0x4a, + 0x8f, 0xc8, 0x27, 0xf0, 0x76, 0xe1, 0xbe, 0x9c, 0xcd, 0xa9, 0xeb, 0xbb, 0xfe, 0x6c, 0x12, 0x47, + 0x2c, 0x32, 0x36, 0x11, 0xe1, 0x46, 0xee, 0xd6, 0x56, 0x62, 0xf0, 0x32, 0x62, 0x11, 0x79, 0x1b, + 0x9a, 0x2a, 0x41, 0x27, 0xae, 0x63, 0xf4, 0xf1, 0x48, 0x0d, 0x25, 0x38, 0x72, 0xc8, 0x03, 0xe8, + 0x85, 0x81, 0xe7, 0xda, 0x8b, 0x49, 0x70, 0xc5, 0x38, 0x77, 0x1d, 0x66, 0x90, 0x61, 0x69, 0xa7, + 0x61, 0x75, 0x95, 0xf8, 0x6b, 0x2d, 0x5d, 0x97, 0x1a, 0xb7, 0xd0, 0x70, 0x25, 0x35, 0x46, 0x00, + 0x76, 0xe0, 0xfb, 0xcc, 0x46, 0xf8, 0x6d, 0xe1, 0x0d, 0xbb, 0xf2, 0x86, 0x07, 0xa9, 0xd4, 0xca, + 0x59, 0x0c, 0x3e, 0x87, 0x76, 0x1e, 0x0a, 0x64, 0x13, 0x2a, 0x97, 0x6c, 0xa1, 0xe1, 0x2f, 0x87, + 0x64, 0x08, 0xd5, 0x2b, 0xea, 0xc5, 0x0c, 0x21, 0xaf, 0x81, 0xa8, 0x96, 0x58, 0x4a, 0xf1, 0xb3, + 0xf2, 0xb3, 0x92, 0xf9, 0xd7, 0x2a, 0x6c, 0x48, 0xf0, 0x91, 0x0f, 0xa1, 0xe3, 0x31, 0x1a, 0xb1, + 0x49, 0x10, 0xca, 0x0d, 0x22, 0x74, 0xd5, 0xda, 0xdb, 0x94, 0xcb, 0x8e, 0xa5, 0xe2, 0x6b, 0x25, + 0xb7, 0xda, 0x5e, 0x6e, 0x26, 0x53, 0xda, 0xf5, 0x05, 0xe3, 0x3e, 0xf5, 0x26, 0x98, 0x0c, 0x2a, + 0xc1, 0xda, 0x89, 0xf0, 0xb9, 0x4c, 0x8a, 0x65, 0x1c, 0x55, 0x56, 0x71, 0x34, 0x80, 0x06, 0xc6, + 0xce, 0x65, 0x91, 0x4e, 0xf6, 0x74, 0x4e, 0xf6, 0xa0, 0x31, 0x67, 0x82, 0xea, 0x5c, 0x93, 0x29, + 0xb1, 0x9d, 0xe4, 0xcc, 0xe8, 0x44, 0x2b, 0x54, 0x42, 0xa4, 0x76, 0x2b, 0x19, 0x51, 0x5b, 0xcd, + 0x88, 0x01, 0x34, 0x52, 0xd0, 0xd5, 0xd5, 0x0b, 0x27, 0x73, 0x49, 0xb3, 0x21, 0xe3, 0x6e, 0xe0, + 0x18, 0x0d, 0x04, 0x8a, 0x9e, 0x49, 0x92, 0xf4, 0xe3, 0xb9, 0x82, 0x50, 0x53, 0x91, 0xa4, 0x1f, + 0xcf, 0x57, 0x11, 0x03, 0x4b, 0x88, 0xf9, 0x11, 0x54, 0xa9, 0xe7, 0xd2, 0x08, 0x53, 0x48, 0xbe, + 0xac, 0xe6, 0xfb, 0xd1, 0xbe, 0x94, 0x5a, 0x4a, 0x49, 0x3e, 0x80, 0xce, 0x8c, 0x07, 0x71, 0x38, + 0xc1, 0x29, 0x8b, 0x8c, 0x36, 0xde, 0x76, 0xd9, 0xba, 0x8d, 0x46, 0xfb, 0xca, 0x46, 0x66, 0xe0, + 0x34, 0x88, 0x7d, 0x67, 0x62, 0xbb, 0x0e, 0x8f, 0x8c, 0x0e, 0x06, 0x0f, 0x50, 0x74, 0x20, 0x25, + 0x32, 0xc5, 0x54, 0x0a, 0xa4, 0x01, 0xee, 0xa2, 0x4d, 0x07, 0xa5, 0xa7, 0x49, 0x94, 0x7f, 0x0c, + 0xfd, 0xa4, 0x28, 0x65, 0x96, 0x3d, 0xb4, 0xdc, 0x4c, 0x14, 0xa9, 0xf1, 0x0e, 0x6c, 0xb2, 0x6b, + 0x49, 0xa1, 0xae, 0x98, 0xcc, 0xe9, 0xf5, 0x44, 0x08, 0x4f, 0xa7, 0x54, 0x37, 0x91, 0x9f, 0xd0, + 0xeb, 0xb1, 0xf0, 0x64, 0xfe, 0xab, 0xdd, 0x31, 0xff, 0xfb, 0x58, 0x8c, 0x9a, 0x28, 0x91, 0xf9, + 0x3f, 0xf8, 0x39, 0x74, 0x0a, 0x4f, 0xb8, 0x06, 0xc8, 0x5b, 0x79, 0x20, 0x37, 0xf3, 0xe0, 0xfd, + 0xe7, 0x06, 0x00, 0xbe, 0xa5, 0x5a, 0xba, 0x5c, 0x01, 0xf2, 0x0f, 0x5c, 0x5e, 0xf3, 0xc0, 0x94, + 0x33, 0x5f, 0x68, 0x30, 0xea, 0xd9, 0x77, 0xe2, 0x30, 0xa9, 0x01, 0xd5, 0x5c, 0x0d, 0x78, 0x04, + 0x1b, 0x12, 0x73, 0x46, 0x2d, 0xa3, 0xea, 0xec, 0x44, 0x88, 0x4e, 0x85, 0x4c, 0xb4, 0x5a, 0x49, + 0x84, 0xfa, 0x6a, 0x22, 0xe4, 0x11, 0xd6, 0x28, 0x22, 0xec, 0x3d, 0xe8, 0xd8, 0x9c, 0x61, 0x3d, + 0x9a, 0xc8, 0xc6, 0x42, 0x23, 0xb0, 0x9d, 0x08, 0xc7, 0xee, 0x9c, 0xc9, 0xf8, 0xc9, 0xc7, 0x00, + 0x54, 0xc9, 0xe1, 0xda, 0xb7, 0x6a, 0xad, 0x7d, 0x2b, 0xac, 0xee, 0x1e, 0xd3, 0x2c, 0x8e, 0xe3, + 0x5c, 0x26, 0x74, 0x0a, 0x99, 0x50, 0x80, 0x7b, 0x77, 0x09, 0xee, 0x4b, 0x98, 0xec, 0xad, 0x60, + 0xf2, 0x5d, 0x68, 0xcb, 0x00, 0x44, 0x21, 0xb5, 0x99, 0x74, 0xb0, 0xa9, 0x02, 0x91, 0xca, 0x8e, + 0x1c, 0xcc, 0xe0, 0x78, 0x3a, 0x5d, 0x5c, 0x04, 0x1e, 0xcb, 0x48, 0xb8, 0x95, 0xca, 0x8e, 0x1c, + 0x79, 0x5e, 0x44, 0x15, 0x41, 0x54, 0xe1, 0x78, 0xf0, 0x11, 0x34, 0xd3, 0xa8, 0x7f, 0x2f, 0x30, + 0xfd, 0xb9, 0x04, 0xed, 0x3c, 0xd1, 0xc9, 0xc5, 0xe3, 0xf1, 0x31, 0x2e, 0xae, 0x58, 0x72, 0x28, + 0x5b, 0x04, 0xce, 0x7c, 0xf6, 0x8a, 0x4e, 0x3d, 0xe5, 0xa0, 0x61, 0x65, 0x02, 0xa9, 0x75, 0x7d, + 0x9b, 0xb3, 0x79, 0x82, 0xaa, 0x8a, 0x95, 0x09, 0xc8, 0xc7, 0x00, 0x6e, 0x14, 0xc5, 0x4c, 0xbd, + 0xdc, 0x06, 0xd2, 0xc0, 0x60, 0xa4, 0xfa, 0xc5, 0x51, 0xd2, 0x2f, 0x8e, 0xc6, 0x49, 0xbf, 0x68, + 0x35, 0xd1, 0x1a, 0x9f, 0x74, 0x1b, 0x6a, 0xf2, 0x81, 0xc6, 0xc7, 0x88, 0xbc, 0x8a, 0xa5, 0x67, + 0xe6, 0x1f, 0xa1, 0xa6, 0x3a, 0x8b, 0xff, 0x2b, 0x79, 0xdf, 0x86, 0x86, 0xf2, 0xed, 0x3a, 0x3a, + 0x57, 0xea, 0x38, 0x3f, 0x72, 0xcc, 0x7f, 0x95, 0xa0, 0x61, 0xb1, 0x28, 0x0c, 0xfc, 0x88, 0xe5, + 0x3a, 0x9f, 0xd2, 0x6b, 0x3b, 0x9f, 0xf2, 0xda, 0xce, 0x27, 0xe9, 0xa7, 0x2a, 0xb9, 0x7e, 0x6a, + 0x00, 0x0d, 0xce, 0x1c, 0x97, 0x33, 0x5b, 0xe8, 0xde, 0x2b, 0x9d, 0x4b, 0xdd, 0x2b, 0xca, 0x65, + 0xc9, 0x8e, 0xb0, 0x2e, 0x34, 0xad, 0x74, 0x4e, 0x9e, 0xe6, 0x1b, 0x06, 0xd5, 0x8a, 0x6d, 0xa9, + 0x86, 0x41, 0x1d, 0x77, 0xb5, 0x63, 0x30, 0xff, 0x51, 0x86, 0xcd, 0x65, 0xf5, 0x1a, 0x10, 0x6c, + 0x41, 0x55, 0x95, 0x14, 0x8d, 0x20, 0xb1, 0x52, 0x4c, 0x2a, 0x4b, 0x5c, 0xf3, 0x8b, 0xe5, 0xbc, + 0x7d, 0xfd, 0xeb, 0x17, 0x73, 0xfa, 0x7d, 0xd8, 0x94, 0xa7, 0x0c, 0x99, 0x93, 0xb5, 0x49, 0x8a, + 0x84, 0x7a, 0x5a, 0x9e, 0x36, 0x4a, 0x0f, 0xa1, 0x9f, 0x98, 0x66, 0xe9, 0x59, 0x2b, 0xd8, 0x1e, + 0x26, 0x59, 0xba, 0x0d, 0xb5, 0xf3, 0x80, 0xcf, 0xa9, 0xd0, 0x3c, 0xa4, 0x67, 0x05, 0x9e, 0x41, + 0xc2, 0x6b, 0x28, 0x58, 0x24, 0x42, 0xf9, 0x29, 0x20, 0xf3, 0x3f, 0x6d, 0xd3, 0x91, 0x88, 0x1a, + 0x56, 0x23, 0x69, 0xcf, 0xcd, 0x5f, 0x43, 0x6f, 0xa9, 0x33, 0x5b, 0x13, 0xc8, 0x6c, 0xfb, 0x72, + 0x61, 0xfb, 0x82, 0xe7, 0xca, 0x92, 0xe7, 0xdf, 0x40, 0xff, 0x0b, 0xea, 0x3b, 0x1e, 0xd3, 0xfe, + 0xf7, 0xf9, 0x2c, 0x92, 0x35, 0x46, 0x7f, 0x28, 0x4c, 0x74, 0x01, 0xe8, 0x58, 0x4d, 0x2d, 0x39, + 0x72, 0xc8, 0x3d, 0xa8, 0x73, 0x65, 0xad, 0x81, 0xd7, 0xca, 0xb5, 0x8e, 0x56, 0xa2, 0x33, 0xbf, + 0x05, 0x52, 0x70, 0x2d, 0xbf, 0x11, 0x16, 0x64, 0x47, 0x02, 0x50, 0x81, 0x42, 0x03, 0xbb, 0x9d, + 0xc7, 0x91, 0x95, 0x6a, 0xc9, 0x10, 0x2a, 0x8c, 0x73, 0xbd, 0x05, 0xf6, 0x6e, 0xd9, 0x17, 0x99, + 0x25, 0x55, 0xe6, 0x4f, 0xa0, 0x7f, 0x16, 0x32, 0xdb, 0xa5, 0x1e, 0x7e, 0x4d, 0xa9, 0x0d, 0xee, + 0x42, 0x55, 0x06, 0x39, 0xc9, 0xd9, 0x26, 0x2e, 0x44, 0xb5, 0x92, 0x9b, 0xdf, 0x82, 0xa1, 0xce, + 0x75, 0x78, 0xed, 0x46, 0x82, 0xf9, 0x36, 0x3b, 0xb8, 0x60, 0xf6, 0xe5, 0xff, 0xf0, 0xe6, 0x57, + 0x70, 0x7b, 0xdd, 0x0e, 0xc9, 0xf9, 0x5a, 0xb6, 0x9c, 0x4d, 0xce, 0x25, 0x7d, 0xe3, 0x1e, 0x0d, + 0x0b, 0x50, 0xf4, 0xb9, 0x94, 0xc8, 0x77, 0x64, 0x72, 0x5d, 0xa4, 0x29, 0x51, 0xcf, 0x92, 0x78, + 0x54, 0x6e, 0x8e, 0xc7, 0x5f, 0x4a, 0xd0, 0x3c, 0x63, 0x22, 0x0e, 0xf1, 0x2e, 0x6f, 0x43, 0x73, + 0xca, 0x83, 0x4b, 0xc6, 0xb3, 0xab, 0x34, 0x94, 0xe0, 0xc8, 0x21, 0x4f, 0xa1, 0x76, 0x10, 0xf8, + 0xe7, 0xee, 0x0c, 0xbf, 0x2d, 0x5b, 0x7b, 0xb7, 0x15, 0xbb, 0xe8, 0xb5, 0x23, 0xa5, 0x53, 0xa5, + 0x56, 0x1b, 0x92, 0x21, 0xb4, 0xf4, 0x17, 0xfa, 0xcb, 0x97, 0x47, 0xcf, 0x93, 0xa6, 0x33, 0x27, + 0x1a, 0x7c, 0x0c, 0xad, 0xdc, 0xc2, 0xef, 0x55, 0x2d, 0x7e, 0x08, 0x80, 0xbb, 0xab, 0x18, 0x6d, + 0xaa, 0xab, 0xea, 0x95, 0xf2, 0x6a, 0x77, 0xa1, 0x29, 0xfb, 0x1b, 0xa5, 0x4e, 0xea, 0x54, 0x29, + 0xab, 0x53, 0xe6, 0x3d, 0xe8, 0x1f, 0xf9, 0x57, 0xd4, 0x73, 0x1d, 0x2a, 0xd8, 0x97, 0x6c, 0x81, + 0x21, 0x58, 0x39, 0x81, 0x79, 0x06, 0x6d, 0xfd, 0xb1, 0xfb, 0x46, 0x67, 0x6c, 0xeb, 0x33, 0x7e, + 0x77, 0x12, 0xbd, 0x0f, 0x3d, 0xed, 0xf4, 0xd8, 0xd5, 0x29, 0x24, 0xcb, 0x3c, 0x67, 0xe7, 0xee, + 0xb5, 0x76, 0xad, 0x67, 0xe6, 0x33, 0xd8, 0xcc, 0x99, 0xa6, 0xd7, 0xb9, 0x64, 0x8b, 0x28, 0xf9, + 0x11, 0x40, 0x8e, 0x93, 0x08, 0x94, 0xb3, 0x08, 0x98, 0xd0, 0xd5, 0x2b, 0x5f, 0x30, 0x71, 0xc3, + 0xed, 0xbe, 0x4c, 0x0f, 0xf2, 0x82, 0x69, 0xe7, 0xf7, 0xa1, 0xca, 0xe4, 0x4d, 0xf3, 0x25, 0x2c, + 0x1f, 0x01, 0x4b, 0xa9, 0xd7, 0x6c, 0xf8, 0x2c, 0xdd, 0xf0, 0x34, 0x56, 0x1b, 0xbe, 0xa1, 0x2f, + 0xf3, 0xbd, 0xf4, 0x18, 0xa7, 0xb1, 0xb8, 0xe9, 0x45, 0xef, 0x41, 0x5f, 0x1b, 0x3d, 0x67, 0x1e, + 0x13, 0xec, 0x86, 0x2b, 0xdd, 0x07, 0x52, 0x30, 0xbb, 0xc9, 0xdd, 0x1d, 0x68, 0x8c, 0xc7, 0xc7, + 0xa9, 0xb6, 0xc8, 0x8d, 0xe6, 0x27, 0xd0, 0x3f, 0x8b, 0x9d, 0xe0, 0x94, 0xbb, 0x57, 0xae, 0xc7, + 0x66, 0x6a, 0xb3, 0xa4, 0xff, 0x2c, 0xe5, 0xfa, 0xcf, 0xb5, 0xd5, 0xc8, 0xdc, 0x01, 0x52, 0x58, + 0x9e, 0xbe, 0x5b, 0x14, 0x3b, 0x81, 0x4e, 0x61, 0x1c, 0x9b, 0x3b, 0xd0, 0x1e, 0x53, 0x59, 0xef, + 0x1d, 0x65, 0x63, 0x40, 0x5d, 0xa8, 0xb9, 0x36, 0x4b, 0xa6, 0xe6, 0x1e, 0x6c, 0x1d, 0x50, 0xfb, + 0xc2, 0xf5, 0x67, 0xcf, 0xdd, 0x48, 0x36, 0x3c, 0x7a, 0xc5, 0x00, 0x1a, 0x8e, 0x16, 0xe8, 0x25, + 0xe9, 0xdc, 0x7c, 0x0c, 0x6f, 0xe5, 0x7e, 0x69, 0x39, 0x13, 0x34, 0x89, 0xc7, 0x16, 0x54, 0x23, + 0x39, 0xc3, 0x15, 0x55, 0x4b, 0x4d, 0xcc, 0xaf, 0x60, 0x2b, 0x5f, 0x80, 0x65, 0xfb, 0x91, 0x5c, + 0x1c, 0x1b, 0x83, 0x52, 0xae, 0x31, 0xd0, 0x31, 0x2b, 0x67, 0xf5, 0x64, 0x13, 0x2a, 0xbf, 0xfc, + 0x66, 0xac, 0xc1, 0x2e, 0x87, 0xe6, 0xef, 0xe5, 0xf6, 0x45, 0x7f, 0x6a, 0xfb, 0x42, 0x77, 0x50, + 0x7a, 0x93, 0xee, 0x60, 0x0d, 0xde, 0x1e, 0x43, 0xff, 0xc4, 0x0b, 0xec, 0xcb, 0x43, 0x3f, 0x17, + 0x0d, 0x03, 0xea, 0xcc, 0xcf, 0x07, 0x23, 0x99, 0x9a, 0x0f, 0xa0, 0x77, 0x1c, 0xd8, 0xd4, 0x3b, + 0x09, 0x62, 0x5f, 0xa4, 0x51, 0xc0, 0x9f, 0xbe, 0xb4, 0xa9, 0x9a, 0x98, 0x8f, 0xa1, 0xab, 0x4b, + 0xb4, 0x7f, 0x1e, 0x24, 0xcc, 0x98, 0x15, 0xf3, 0x52, 0xb1, 0xd7, 0x36, 0x8f, 0xa1, 0x97, 0x99, + 0x2b, 0xbf, 0x0f, 0xa0, 0xa6, 0xd4, 0xfa, 0x6e, 0xbd, 0xf4, 0x03, 0x52, 0x59, 0x5a, 0x5a, 0xbd, + 0xe6, 0x52, 0x73, 0xe8, 0x9e, 0xe2, 0x4f, 0x90, 0x87, 0xfe, 0x95, 0x72, 0x76, 0x04, 0x44, 0xfd, + 0x28, 0x39, 0x61, 0xfe, 0x95, 0xcb, 0x03, 0x1f, 0xfb, 0xdb, 0x92, 0x6e, 0x61, 0x12, 0xc7, 0xe9, + 0xa2, 0xc4, 0xc2, 0xea, 0x87, 0xcb, 0xa2, 0xb5, 0x31, 0x84, 0xec, 0x07, 0x0e, 0x59, 0x6a, 0x38, + 0x9b, 0x07, 0x82, 0x4d, 0xa8, 0xe3, 0x24, 0xd9, 0x02, 0x4a, 0xb4, 0xef, 0x38, 0x7c, 0xef, 0x3f, + 0x65, 0xa8, 0x7f, 0xa6, 0x08, 0x9c, 0x7c, 0x0a, 0x9d, 0x42, 0xb9, 0x26, 0x6f, 0xe1, 0x2f, 0x1c, + 0xcb, 0xcd, 0xc1, 0x60, 0x7b, 0x45, 0xac, 0xee, 0xf5, 0x04, 0xda, 0xf9, 0x62, 0x4c, 0xb0, 0xf0, + 0xe2, 0xcf, 0xad, 0x03, 0xf4, 0xb4, 0x5a, 0xa9, 0xcf, 0x60, 0x6b, 0x5d, 0x99, 0x24, 0x77, 0xb2, + 0x1d, 0x56, 0x4b, 0xf4, 0xe0, 0x9d, 0x9b, 0xb4, 0x49, 0x79, 0xad, 0x1f, 0x78, 0x8c, 0xfa, 0x71, + 0x98, 0x3f, 0x41, 0x36, 0x24, 0x4f, 0xa1, 0x53, 0x28, 0x14, 0xea, 0x9e, 0x2b, 0xb5, 0x23, 0xbf, + 0xe4, 0x3e, 0x54, 0xb1, 0x38, 0x91, 0x4e, 0xa1, 0x4a, 0x0e, 0xba, 0xe9, 0x54, 0xed, 0x3d, 0x84, + 0x0d, 0xfc, 0x11, 0x2e, 0xb7, 0x31, 0xae, 0x48, 0x2b, 0xd7, 0xde, 0xbf, 0x4b, 0x50, 0x4f, 0x7e, + 0x98, 0x7d, 0x0a, 0x1b, 0xb2, 0x06, 0x90, 0x5b, 0x39, 0x1a, 0x4d, 0xea, 0xc7, 0x60, 0x6b, 0x49, + 0xa8, 0x36, 0x18, 0x41, 0xe5, 0x05, 0x13, 0x84, 0xe4, 0x94, 0xba, 0x18, 0x0c, 0x6e, 0x15, 0x65, + 0xa9, 0xfd, 0x69, 0x5c, 0xb4, 0xd7, 0x5c, 0x5e, 0xb0, 0x4f, 0x59, 0xfa, 0x23, 0xa8, 0x29, 0x96, + 0x55, 0x41, 0x59, 0xe1, 0x67, 0xf5, 0xf8, 0xab, 0x7c, 0xbc, 0xf7, 0xf7, 0x0d, 0x80, 0xb3, 0x45, + 0x24, 0xd8, 0xfc, 0x57, 0x2e, 0x7b, 0x45, 0x1e, 0x42, 0xef, 0x39, 0x3b, 0xa7, 0xb1, 0x27, 0xf0, + 0x6b, 0x49, 0xb2, 0x49, 0x2e, 0x26, 0xd8, 0xf0, 0xa5, 0x64, 0x7d, 0x1f, 0x5a, 0x27, 0xf4, 0xfa, + 0xf5, 0x76, 0x9f, 0x42, 0xa7, 0xc0, 0xc1, 0xfa, 0x88, 0xcb, 0xac, 0xae, 0x8f, 0xb8, 0xca, 0xd6, + 0xf7, 0xa1, 0xae, 0x99, 0x39, 0xbf, 0x07, 0xd6, 0xb0, 0x02, 0x63, 0xff, 0x14, 0x7a, 0x4b, 0xbc, + 0x9c, 0xb7, 0xc7, 0x5f, 0x24, 0xd6, 0xf2, 0xf6, 0x33, 0xf9, 0xb5, 0x53, 0xe4, 0xe6, 0xfc, 0xc2, + 0xdb, 0x8a, 0x0f, 0xd7, 0x91, 0xf7, 0x8b, 0xe2, 0x77, 0x12, 0x7e, 0x25, 0x1a, 0xcb, 0xf4, 0x99, + 0x90, 0x77, 0xe2, 0x68, 0x1d, 0x0d, 0x3f, 0x81, 0x76, 0x9e, 0x41, 0x57, 0x52, 0x70, 0x95, 0x5e, + 0x1f, 0x01, 0x64, 0x24, 0x9a, 0xb7, 0x47, 0x78, 0x2c, 0xf3, 0xeb, 0x87, 0x00, 0x19, 0x35, 0x2a, + 0x54, 0x15, 0x99, 0x55, 0x2d, 0x5b, 0xa6, 0xcf, 0x87, 0xd0, 0x4c, 0xe9, 0x2c, 0xbf, 0x07, 0x3a, + 0x28, 0xb2, 0xe3, 0x67, 0xa3, 0xdf, 0x3e, 0x9a, 0xb9, 0xe2, 0x22, 0x9e, 0x8e, 0xec, 0x60, 0xbe, + 0x7b, 0x41, 0xa3, 0x0b, 0xd7, 0x0e, 0x78, 0xb8, 0x7b, 0x25, 0xc1, 0xb4, 0xbb, 0xf2, 0x9f, 0xd1, + 0xb4, 0x86, 0x1f, 0x7b, 0x1f, 0xfc, 0x37, 0x00, 0x00, 0xff, 0xff, 0x93, 0x15, 0xb9, 0x42, 0x4f, + 0x1a, 0x00, 0x00, } diff --git a/logical/plugin/pb/backend.proto b/logical/plugin/pb/backend.proto index d134179b28..e02cc1f9af 100644 --- a/logical/plugin/pb/backend.proto +++ b/logical/plugin/pb/backend.proto @@ -31,7 +31,7 @@ message ProtoError { } // Paths is the structure of special paths that is used for SpecialPaths. -message Paths { +message Paths { // Root are the paths that require a root token to access repeated string root = 1; @@ -199,7 +199,7 @@ message Auth { // the range of the specified CIDR(s). repeated string bound_cidrs = 13; - // TokenPolicies and IdentityPolicies break down the list in Policies to + // TokenPolicies and IdentityPolicies break down the list in Policies to // help determine where a policy was sourced repeated string token_policies = 14; repeated string identity_policies = 15; @@ -207,6 +207,9 @@ message Auth { // Explicit maximum lifetime for the token. Unlike normal TTLs, the maximum // TTL is a hard limit and cannot be exceeded, also counts for periodic tokens. int64 explicit_max_ttl = 16; + + // TokenType is the type of token being requested + uint32 token_type = 17; } message TokenEntry { @@ -227,6 +230,7 @@ message TokenEntry { repeated string bound_cidrs = 15; string namespace_id = 16; string cubbyhole_id = 17; + uint32 type = 18; } message LeaseOptions { diff --git a/logical/plugin/pb/translation.go b/logical/plugin/pb/translation.go index 58532dec0e..c777cae505 100644 --- a/logical/plugin/pb/translation.go +++ b/logical/plugin/pb/translation.go @@ -486,6 +486,7 @@ func LogicalAuthToProtoAuth(a *logical.Auth) (*Auth, error) { return &Auth{ LeaseOptions: lo, + TokenType: uint32(a.TokenType), InternalData: string(buf[:]), DisplayName: a.DisplayName, Policies: a.Policies, @@ -532,6 +533,7 @@ func ProtoAuthToLogicalAuth(a *Auth) (*logical.Auth, error) { return &logical.Auth{ LeaseOptions: lo, + TokenType: logical.TokenType(a.TokenType), InternalData: data, DisplayName: a.DisplayName, Policies: a.Policies, @@ -578,6 +580,7 @@ func LogicalTokenEntryToProtoTokenEntry(t *logical.TokenEntry) *TokenEntry { BoundCIDRs: boundCIDRs, NamespaceID: t.NamespaceID, CubbyholeID: t.CubbyholeID, + Type: uint32(t.Type), } } @@ -614,5 +617,6 @@ func ProtoTokenEntryToLogicalTokenEntry(t *TokenEntry) (*logical.TokenEntry, err BoundCIDRs: boundCIDRs, NamespaceID: t.NamespaceID, CubbyholeID: t.CubbyholeID, + Type: logical.TokenType(t.Type), }, nil } diff --git a/logical/testing/testing.go b/logical/testing/testing.go index 865a4767c6..1b65d0baec 100644 --- a/logical/testing/testing.go +++ b/logical/testing/testing.go @@ -213,6 +213,20 @@ func Test(tt TestT, c TestCase) { return } + tokenInfo, err := client.Auth().Token().LookupSelf() + if err != nil { + tt.Fatal("error looking up token: ", err) + return + } + var tokenPolicies []string + if tokenPoliciesRaw, ok := tokenInfo.Data["policies"]; ok { + if tokenPoliciesSliceRaw, ok := tokenPoliciesRaw.([]interface{}); ok { + for _, p := range tokenPoliciesSliceRaw { + tokenPolicies = append(tokenPolicies, p.(string)) + } + } + } + // Make requests var revoke []*logical.Request for i, s := range c.Steps { @@ -228,6 +242,12 @@ func Test(tt TestT, c TestCase) { } if !s.Unauthenticated { req.ClientToken = client.Token() + req.SetTokenEntry(&logical.TokenEntry{ + ID: req.ClientToken, + NamespaceID: namespace.RootNamespaceID, + Policies: tokenPolicies, + DisplayName: tokenInfo.Data["display_name"].(string), + }) } if s.RemoteAddr != "" { req.Connection = &logical.Connection{RemoteAddr: s.RemoteAddr} diff --git a/logical/token.go b/logical/token.go index a991441fe2..27b81d1a38 100644 --- a/logical/token.go +++ b/logical/token.go @@ -6,8 +6,49 @@ import ( sockaddr "github.com/hashicorp/go-sockaddr" ) +type TokenType uint8 + +const ( + // TokenTypeDefault means "use the default, if any, that is currently set + // on the mount". If not set, results in a Service token. + TokenTypeDefault TokenType = iota + + // TokenTypeService is a "normal" Vault token for long-lived services + TokenTypeService + + // TokenTypeBatch is a batch token + TokenTypeBatch + + // TokenTypeDefaultService, configured on a mount, means that if + // TokenTypeDefault is sent back by the mount, create Service tokens + TokenTypeDefaultService + + // TokenTypeDefaultBatch, configured on a mount, means that if + // TokenTypeDefault is sent back by the mount, create Batch tokens + TokenTypeDefaultBatch +) + +func (t TokenType) String() string { + switch t { + case TokenTypeDefault: + return "default" + case TokenTypeService: + return "service" + case TokenTypeBatch: + return "batch" + case TokenTypeDefaultService: + return "default-service" + case TokenTypeDefaultBatch: + return "default-batch" + default: + panic("unreachable") + } +} + // TokenEntry is used to represent a given token type TokenEntry struct { + Type TokenType `json:"type" mapstructure:"type" structs:"type" sentinel:""` + // ID of this entry, generally a random UUID ID string `json:"id" mapstructure:"id" structs:"id" sentinel:""` @@ -107,6 +148,9 @@ func (te *TokenEntry) SentinelGet(key string) (interface{}, error) { case "meta", "metadata": return te.Meta, nil + + case "type": + return te.Type.String(), nil } return nil, nil @@ -124,5 +168,6 @@ func (te *TokenEntry) SentinelKeys() []string { "creation_time_unix", "meta", "metadata", + "type", } } diff --git a/logical/translate_response.go b/logical/translate_response.go index 84ed284a14..11714c22a2 100644 --- a/logical/translate_response.go +++ b/logical/translate_response.go @@ -36,6 +36,7 @@ func LogicalResponseToHTTPResponse(input *Response) *HTTPResponse { LeaseDuration: int(input.Auth.TTL.Seconds()), Renewable: input.Auth.Renewable, EntityID: input.Auth.EntityID, + TokenType: input.Auth.TokenType.String(), } } @@ -68,6 +69,12 @@ func HTTPResponseToLogicalResponse(input *HTTPResponse) *Response { } logicalResp.Auth.Renewable = input.Auth.Renewable logicalResp.Auth.TTL = time.Second * time.Duration(input.Auth.LeaseDuration) + switch input.Auth.TokenType { + case "service": + logicalResp.Auth.TokenType = TokenTypeService + case "batch": + logicalResp.Auth.TokenType = TokenTypeBatch + } } return logicalResp @@ -94,6 +101,7 @@ type HTTPAuth struct { LeaseDuration int `json:"lease_duration"` Renewable bool `json:"renewable"` EntityID string `json:"entity_id"` + TokenType string `json:"token_type"` } type HTTPWrapInfo struct { diff --git a/plugins/database/cassandra/test-fixtures/cassandra.yaml b/plugins/database/cassandra/test-fixtures/cassandra.yaml index e8535107c8..71fdead51f 100644 --- a/plugins/database/cassandra/test-fixtures/cassandra.yaml +++ b/plugins/database/cassandra/test-fixtures/cassandra.yaml @@ -572,7 +572,7 @@ ssl_storage_port: 7001 # # Setting listen_address to 0.0.0.0 is always wrong. # -listen_address: 172.17.0.2 +listen_address: 172.17.0.3 # Set listen_address OR listen_interface, not both. Interfaces must correspond # to a single address, IP aliasing is not supported. diff --git a/vault/auth.go b/vault/auth.go index 1d5b671964..bd67261c0d 100644 --- a/vault/auth.go +++ b/vault/auth.go @@ -661,6 +661,10 @@ func (c *Core) setupCredentials(ctx context.Context) error { if entry.Type == "token" { c.tokenStore = backend.(*TokenStore) + // At some point when this isn't beta we may persist this but for + // now always set it on mount + entry.Config.TokenType = logical.TokenTypeDefaultService + // this is loaded *after* the normal mounts, including cubbyhole c.router.tokenStoreSaltFunc = c.tokenStore.Salt if !c.IsDRSecondary() { diff --git a/vault/barrier_aes_gcm.go b/vault/barrier_aes_gcm.go index 7dfdc1b84a..0f44ef886a 100644 --- a/vault/barrier_aes_gcm.go +++ b/vault/barrier_aes_gcm.go @@ -845,7 +845,11 @@ func (b *AESGCMBarrier) encrypt(path string, term uint32, gcm cipher.AEAD, plain case AESGCMVersion1: out = gcm.Seal(out, nonce, plain, nil) case AESGCMVersion2: - out = gcm.Seal(out, nonce, plain, []byte(path)) + aad := []byte(nil) + if path != "" { + aad = []byte(path) + } + out = gcm.Seal(out, nonce, plain, aad) default: panic("Unknown AESGCM version") } @@ -865,7 +869,11 @@ func (b *AESGCMBarrier) decrypt(path string, gcm cipher.AEAD, cipher []byte) ([] case AESGCMVersion1: return gcm.Open(out, nonce, raw, nil) case AESGCMVersion2: - return gcm.Open(out, nonce, raw, []byte(path)) + aad := []byte(nil) + if path != "" { + aad = []byte(path) + } + return gcm.Open(out, nonce, raw, aad) default: return nil, fmt.Errorf("version bytes mis-match") } diff --git a/vault/core.go b/vault/core.go index 5fa03e76d3..3d041cdccb 100644 --- a/vault/core.go +++ b/vault/core.go @@ -1047,6 +1047,7 @@ func (c *Core) sealInitCommon(ctx context.Context, req *logical.Request) (retErr // Audit-log the request before going any further auth := &logical.Auth{ ClientToken: req.ClientToken, + Accessor: req.ClientTokenAccessor, } if te != nil { auth.IdentityPolicies = identityPolicies[te.NamespaceID] @@ -1057,6 +1058,7 @@ func (c *Core) sealInitCommon(ctx context.Context, req *logical.Request) (retErr auth.Metadata = te.Meta auth.DisplayName = te.DisplayName auth.EntityID = te.EntityID + auth.TokenType = te.Type } logInput := &audit.LogInput{ diff --git a/vault/core_test.go b/vault/core_test.go index 2bacc550cb..122804d53d 100644 --- a/vault/core_test.go +++ b/vault/core_test.go @@ -317,6 +317,7 @@ func TestCore_HandleRequest_Lease(t *testing.T) { // Read the key req.Operation = logical.ReadOperation req.Data = nil + req.SetTokenEntry(&logical.TokenEntry{ID: root, NamespaceID: "root", Policies: []string{"root"}}) resp, err = c.HandleRequest(ctx, req) if err != nil { t.Fatalf("err: %v", err) @@ -359,6 +360,7 @@ func TestCore_HandleRequest_Lease_MaxLength(t *testing.T) { // Read the key req.Operation = logical.ReadOperation req.Data = nil + req.SetTokenEntry(&logical.TokenEntry{ID: root, NamespaceID: "root", Policies: []string{"root"}}) resp, err = c.HandleRequest(ctx, req) if err != nil { t.Fatalf("err: %v", err) @@ -401,6 +403,7 @@ func TestCore_HandleRequest_Lease_DefaultLength(t *testing.T) { // Read the key req.Operation = logical.ReadOperation req.Data = nil + req.SetTokenEntry(&logical.TokenEntry{ID: root, NamespaceID: "root", Policies: []string{"root"}}) resp, err = c.HandleRequest(ctx, req) if err != nil { t.Fatalf("err: %v", err) @@ -409,7 +412,7 @@ func TestCore_HandleRequest_Lease_DefaultLength(t *testing.T) { t.Fatalf("bad: %#v", resp) } if resp.Secret.TTL != c.defaultLeaseTTL { - t.Fatalf("bad: %#v, %d", resp.Secret, c.defaultLeaseTTL) + t.Fatalf("bad: %d, %d", resp.Secret.TTL/time.Second, c.defaultLeaseTTL/time.Second) } if resp.Secret.LeaseID == "" { t.Fatalf("bad: %#v", resp.Secret) @@ -481,7 +484,7 @@ func TestCore_HandleRequest_NoSlash(t *testing.T) { // Test a root path is denied if non-root func TestCore_HandleRequest_RootPath(t *testing.T) { c, _, root := TestCoreUnsealed(t) - testMakeTokenViaCore(t, c, root, "child", "", []string{"test"}) + testMakeServiceTokenViaCore(t, c, root, "child", "", []string{"test"}) req := &logical.Request{ Operation: logical.ReadOperation, @@ -516,7 +519,7 @@ func TestCore_HandleRequest_RootPath_WithSudo(t *testing.T) { } // Child token (non-root) but with 'test' policy should have access - testMakeTokenViaCore(t, c, root, "child", "", []string{"test"}) + testMakeServiceTokenViaCore(t, c, root, "child", "", []string{"test"}) req = &logical.Request{ Operation: logical.ReadOperation, Path: "sys/policy", // root protected! @@ -534,7 +537,7 @@ func TestCore_HandleRequest_RootPath_WithSudo(t *testing.T) { // Check that standard permissions work func TestCore_HandleRequest_PermissionDenied(t *testing.T) { c, _, root := TestCoreUnsealed(t) - testMakeTokenViaCore(t, c, root, "child", "", []string{"test"}) + testMakeServiceTokenViaCore(t, c, root, "child", "", []string{"test"}) req := &logical.Request{ Operation: logical.UpdateOperation, @@ -554,7 +557,7 @@ func TestCore_HandleRequest_PermissionDenied(t *testing.T) { // Check that standard permissions work func TestCore_HandleRequest_PermissionAllowed(t *testing.T) { c, _, root := TestCoreUnsealed(t) - testMakeTokenViaCore(t, c, root, "child", "", []string{"test"}) + testMakeServiceTokenViaCore(t, c, root, "child", "", []string{"test"}) // Set the 'test' policy object to permit access to secret/ req := &logical.Request{ @@ -719,6 +722,7 @@ func TestCore_HandleLogin_Token(t *testing.T) { TTL: time.Hour * 24, CreationTime: te.CreationTime, NamespaceID: namespace.RootNamespaceID, + Type: logical.TokenTypeService, } if !reflect.DeepEqual(te, expect) { @@ -884,6 +888,7 @@ func TestCore_HandleRequest_AuditTrail_noHMACKeys(t *testing.T) { ClientToken: root, } req.ClientToken = root + req.SetTokenEntry(&logical.TokenEntry{ID: root, NamespaceID: "root", Policies: []string{"root"}}) if _, err := c.HandleRequest(namespace.RootContext(nil), req); err != nil { t.Fatalf("err: %v", err) } @@ -1020,6 +1025,7 @@ func TestCore_HandleRequest_CreateToken_Lease(t *testing.T) { CreationTime: te.CreationTime, TTL: time.Hour * 24 * 32, NamespaceID: namespace.RootNamespaceID, + Type: logical.TokenTypeService, } if !reflect.DeepEqual(te, expect) { t.Fatalf("Bad: %#v expect: %#v", te, expect) @@ -1066,6 +1072,7 @@ func TestCore_HandleRequest_CreateToken_NoDefaultPolicy(t *testing.T) { CreationTime: te.CreationTime, TTL: time.Hour * 24 * 32, NamespaceID: namespace.RootNamespaceID, + Type: logical.TokenTypeService, } if !reflect.DeepEqual(te, expect) { t.Fatalf("Bad: %#v expect: %#v", te, expect) @@ -1837,6 +1844,7 @@ func TestCore_HandleRequest_InternalData(t *testing.T) { Path: "foo/test", ClientToken: root, } + lreq.SetTokenEntry(&logical.TokenEntry{ID: root, NamespaceID: "root", Policies: []string{"root"}}) lresp, err := c.HandleRequest(namespace.RootContext(nil), lreq) if err != nil { t.Fatalf("err: %v", err) @@ -1909,6 +1917,7 @@ func TestCore_RenewSameLease(t *testing.T) { // Read the key req.Operation = logical.ReadOperation req.Data = nil + req.SetTokenEntry(&logical.TokenEntry{ID: root, NamespaceID: "root", Policies: []string{"root"}}) resp, err = c.HandleRequest(namespace.RootContext(nil), req) if err != nil { t.Fatalf("err: %v", err) @@ -2078,6 +2087,7 @@ path "secret/*" { // Read the key req.Operation = logical.ReadOperation req.Data = nil + req.SetTokenEntry(&logical.TokenEntry{ID: root, NamespaceID: "root", Policies: []string{"root"}}) resp, err = c.HandleRequest(namespace.RootContext(nil), req) if err != nil { t.Fatalf("err: %v", err) diff --git a/vault/expiration.go b/vault/expiration.go index 574c58e5b2..c2f6bb7334 100644 --- a/vault/expiration.go +++ b/vault/expiration.go @@ -873,6 +873,26 @@ func (m *ExpirationManager) Renew(ctx context.Context, leaseID string, increment le.ExpireTime = resp.Secret.ExpirationTime() le.LastRenewalTime = time.Now() + // If the token it's associated with is a batch token, constrain lease + // times + if le.ClientTokenType == logical.TokenTypeBatch { + te, err := m.tokenStore.Lookup(ctx, le.ClientToken) + if err != nil { + return nil, err + } + if te == nil { + return nil, errors.New("cannot renew lease, no valid associated token") + } + tokenLeaseTimes, err := m.FetchLeaseTimesByToken(ctx, te) + if err != nil { + return nil, err + } + if le.ExpireTime.After(tokenLeaseTimes.ExpireTime) { + resp.Secret.TTL = tokenLeaseTimes.ExpireTime.Sub(le.LastRenewalTime) + le.ExpireTime = tokenLeaseTimes.ExpireTime + } + } + { m.pendingLock.Lock() if err := m.persistEntry(ctx, le); err != nil { @@ -1012,7 +1032,8 @@ func (m *ExpirationManager) RenewToken(ctx context.Context, req *logical.Request func (m *ExpirationManager) Register(ctx context.Context, req *logical.Request, resp *logical.Response) (id string, retErr error) { defer metrics.MeasureSince([]string{"expire", "register"}, time.Now()) - if req.ClientToken == "" { + te := req.TokenEntry() + if te == nil { return "", fmt.Errorf("cannot register a lease with an empty client token") } @@ -1044,14 +1065,15 @@ func (m *ExpirationManager) Register(ctx context.Context, req *logical.Request, } le := &leaseEntry{ - LeaseID: leaseID, - ClientToken: req.ClientToken, - Path: req.Path, - Data: resp.Data, - Secret: resp.Secret, - IssueTime: time.Now(), - ExpireTime: resp.Secret.ExpirationTime(), - namespace: ns, + LeaseID: leaseID, + ClientToken: req.ClientToken, + ClientTokenType: te.Type, + Path: req.Path, + Data: resp.Data, + Secret: resp.Secret, + IssueTime: time.Now(), + ExpireTime: resp.Secret.ExpirationTime(), + namespace: ns, } defer func() { @@ -1078,14 +1100,35 @@ func (m *ExpirationManager) Register(ctx context.Context, req *logical.Request, } }() + // If the token is a batch token, we want to constrain the maximum lifetime + // by the token's lifetime + if te.Type == logical.TokenTypeBatch { + tokenLeaseTimes, err := m.FetchLeaseTimesByToken(ctx, te) + if err != nil { + return "", err + } + if le.ExpireTime.After(tokenLeaseTimes.ExpireTime) { + le.ExpireTime = tokenLeaseTimes.ExpireTime + } + } + // Encode the entry if err := m.persistEntry(ctx, le); err != nil { return "", err } - // Maintain secondary index by token - if err := m.createIndexByToken(ctx, le); err != nil { - return "", err + // Maintain secondary index by token, except for orphan batch tokens + switch { + case te.Type != logical.TokenTypeBatch: + if err := m.createIndexByToken(ctx, le, le.ClientToken); err != nil { + return "", err + } + case te.Parent != "": + // If it's a non-orphan batch token, assign the secondary index to its + // parent + if err := m.createIndexByToken(ctx, le, te.Parent); err != nil { + return "", err + } } // Setup revocation timer if there is a lease @@ -1101,8 +1144,12 @@ func (m *ExpirationManager) Register(ctx context.Context, req *logical.Request, func (m *ExpirationManager) RegisterAuth(ctx context.Context, te *logical.TokenEntry, auth *logical.Auth) error { defer metrics.MeasureSince([]string{"expire", "register-auth"}, time.Now()) + if te.Type == logical.TokenTypeBatch { + return errors.New("cannot register a lease for a batch token") + } + if auth.ClientToken == "" { - return fmt.Errorf("cannot register an auth lease with an empty token") + return errors.New("cannot register an auth lease with an empty token") } if strings.Contains(te.Path, "..") { @@ -1152,9 +1199,24 @@ func (m *ExpirationManager) RegisterAuth(ctx context.Context, te *logical.TokenE // FetchLeaseTimesByToken is a helper function to use token values to compute // the leaseID, rather than pushing that logic back into the token store. +// As a special case, for a batch token it simply returns the information +// encoded on it. func (m *ExpirationManager) FetchLeaseTimesByToken(ctx context.Context, te *logical.TokenEntry) (*leaseEntry, error) { defer metrics.MeasureSince([]string{"expire", "fetch-lease-times-by-token"}, time.Now()) + if te == nil { + return nil, errors.New("cannot fetch lease times for nil token") + } + + if te.Type == logical.TokenTypeBatch { + issueTime := time.Unix(te.CreationTime, 0) + return &leaseEntry{ + IssueTime: issueTime, + ExpireTime: issueTime.Add(te.TTL), + ClientTokenType: logical.TokenTypeBatch, + }, nil + } + tokenNS, err := NamespaceByID(ctx, te.NamespaceID, m.core) if err != nil { return nil, err @@ -1273,6 +1335,10 @@ func (m *ExpirationManager) revokeEntry(ctx context.Context, le *leaseEntry) err // Revocation of login tokens is special since we can by-pass the // backend and directly interact with the token store if le.Auth != nil { + if le.ClientTokenType == logical.TokenTypeBatch { + return errors.New("batch tokens cannot be revoked") + } + if err := m.tokenStore.revokeTree(ctx, le); err != nil { return errwrap.Wrapf("failed to revoke token: {{err}}", err) } @@ -1319,6 +1385,10 @@ func (m *ExpirationManager) renewEntry(ctx context.Context, le *leaseEntry, incr // renewAuthEntry is used to attempt renew of an auth entry. Only the token // store should get the actual token ID intact. func (m *ExpirationManager) renewAuthEntry(ctx context.Context, req *logical.Request, le *leaseEntry, increment time.Duration) (*logical.Response, error) { + if le.ClientTokenType == logical.TokenTypeBatch { + return logical.ErrorResponse("batch tokens cannot be renewed"), nil + } + auth := *le.Auth auth.IssueTime = le.IssueTime auth.Increment = increment @@ -1446,10 +1516,10 @@ func (m *ExpirationManager) deleteEntry(ctx context.Context, le *leaseEntry) err } // createIndexByToken creates a secondary index from the token to a lease entry -func (m *ExpirationManager) createIndexByToken(ctx context.Context, le *leaseEntry) error { +func (m *ExpirationManager) createIndexByToken(ctx context.Context, le *leaseEntry, token string) error { tokenNS := namespace.RootNamespace saltCtx := namespace.ContextWithNamespace(ctx, namespace.RootNamespace) - _, nsID := namespace.SplitIDFromString(le.ClientToken) + _, nsID := namespace.SplitIDFromString(token) if nsID != "" { tokenNS, err := NamespaceByID(ctx, nsID, m.core) if err != nil { @@ -1460,7 +1530,7 @@ func (m *ExpirationManager) createIndexByToken(ctx context.Context, le *leaseEnt } } - saltedID, err := m.tokenStore.SaltID(saltCtx, le.ClientToken) + saltedID, err := m.tokenStore.SaltID(saltCtx, token) if err != nil { return err } @@ -1674,6 +1744,7 @@ func (m *ExpirationManager) emitMetrics() { type leaseEntry struct { LeaseID string `json:"lease_id"` ClientToken string `json:"client_token"` + ClientTokenType logical.TokenType `json:"token_type"` Path string `json:"path"` Data map[string]interface{} `json:"data"` Secret *logical.Secret `json:"secret"` @@ -1691,24 +1762,29 @@ func (le *leaseEntry) encode() ([]byte, error) { } func (le *leaseEntry) renewable() (bool, error) { - var err error switch { - // If there is no entry, cannot review - case le == nil || le.ExpireTime.IsZero(): - err = fmt.Errorf("lease not found or lease is not renewable") + // If there is no entry, cannot review to renew + case le == nil: + return false, fmt.Errorf("lease not found") + + case le.ExpireTime.IsZero(): + return false, fmt.Errorf("lease is not renewable") + + case le.ClientTokenType == logical.TokenTypeBatch: + return false, nil + // Determine if the lease is expired case le.ExpireTime.Before(time.Now()): - err = fmt.Errorf("lease expired") + return false, fmt.Errorf("lease expired") + // Determine if the lease is renewable case le.Secret != nil && !le.Secret.Renewable: - err = fmt.Errorf("lease is not renewable") + return false, fmt.Errorf("lease is not renewable") + case le.Auth != nil && !le.Auth.Renewable: - err = fmt.Errorf("lease is not renewable") + return false, fmt.Errorf("lease is not renewable") } - if err != nil { - return false, err - } return true, nil } diff --git a/vault/expiration_test.go b/vault/expiration_test.go index 6af9bcb619..a089acf844 100644 --- a/vault/expiration_test.go +++ b/vault/expiration_test.go @@ -180,6 +180,7 @@ func TestExpiration_Tidy(t *testing.T) { Path: "invalid/lease/" + fmt.Sprintf("%d", i+1), ClientToken: "invalidtoken", } + req.SetTokenEntry(&logical.TokenEntry{ID: "invalidtoken", NamespaceID: "root"}) resp := &logical.Response{ Secret: &logical.Secret{ LeaseOptions: logical.LeaseOptions{ @@ -396,6 +397,7 @@ func TestExpiration_Restore(t *testing.T) { Path: path, ClientToken: "foobar", } + req.SetTokenEntry(&logical.TokenEntry{ID: "foobar", NamespaceID: "root"}) resp := &logical.Response{ Secret: &logical.Secret{ LeaseOptions: logical.LeaseOptions{ @@ -452,6 +454,7 @@ func TestExpiration_Register(t *testing.T) { Path: "prod/aws/foo", ClientToken: "foobar", } + req.SetTokenEntry(&logical.TokenEntry{ID: "foobar", NamespaceID: "root"}) resp := &logical.Response{ Secret: &logical.Secret{ LeaseOptions: logical.LeaseOptions{ @@ -539,7 +542,7 @@ func TestExpiration_RegisterAuth_NoLease(t *testing.T) { NamespaceID: namespace.RootNamespaceID, } resp, err := exp.RenewToken(namespace.TestContext(), &logical.Request{}, te, 0) - if err != nil && (err != logical.ErrInvalidRequest || (resp != nil && resp.IsError() && resp.Error().Error() != "lease not found or lease is not renewable")) { + if err != nil && (err != logical.ErrInvalidRequest || (resp != nil && resp.IsError() && resp.Error().Error() != "lease is not renewable")) { t.Fatalf("bad: err:%v resp:%#v", err, resp) } if resp == nil { @@ -578,6 +581,7 @@ func TestExpiration_Revoke(t *testing.T) { Path: "prod/aws/foo", ClientToken: "foobar", } + req.SetTokenEntry(&logical.TokenEntry{ID: "foobar", NamespaceID: "root"}) resp := &logical.Response{ Secret: &logical.Secret{ LeaseOptions: logical.LeaseOptions{ @@ -624,6 +628,7 @@ func TestExpiration_RevokeOnExpire(t *testing.T) { Path: "prod/aws/foo", ClientToken: "foobar", } + req.SetTokenEntry(&logical.TokenEntry{ID: "foobar", NamespaceID: "root"}) resp := &logical.Response{ Secret: &logical.Secret{ LeaseOptions: logical.LeaseOptions{ @@ -687,6 +692,7 @@ func TestExpiration_RevokePrefix(t *testing.T) { Path: path, ClientToken: "foobar", } + req.SetTokenEntry(&logical.TokenEntry{ID: "foobar", NamespaceID: "root"}) resp := &logical.Response{ Secret: &logical.Secret{ LeaseOptions: logical.LeaseOptions{ @@ -755,6 +761,7 @@ func TestExpiration_RevokeByToken(t *testing.T) { Path: path, ClientToken: "foobarbaz", } + req.SetTokenEntry(&logical.TokenEntry{ID: "foobarbaz", NamespaceID: "root"}) resp := &logical.Response{ Secret: &logical.Secret{ LeaseOptions: logical.LeaseOptions{ @@ -843,6 +850,7 @@ func TestExpiration_RevokeByToken_Blocking(t *testing.T) { Path: path, ClientToken: "foobarbaz", } + req.SetTokenEntry(&logical.TokenEntry{ID: "foobarbaz", NamespaceID: "root"}) resp := &logical.Response{ Secret: &logical.Secret{ LeaseOptions: logical.LeaseOptions{ @@ -1145,6 +1153,7 @@ func TestExpiration_Renew(t *testing.T) { Path: "prod/aws/foo", ClientToken: "foobar", } + req.SetTokenEntry(&logical.TokenEntry{ID: "foobar", NamespaceID: "root"}) resp := &logical.Response{ Secret: &logical.Secret{ LeaseOptions: logical.LeaseOptions{ @@ -1215,6 +1224,7 @@ func TestExpiration_Renew_NotRenewable(t *testing.T) { Path: "prod/aws/foo", ClientToken: "foobar", } + req.SetTokenEntry(&logical.TokenEntry{ID: "foobar", NamespaceID: "root"}) resp := &logical.Response{ Secret: &logical.Secret{ LeaseOptions: logical.LeaseOptions{ @@ -1265,6 +1275,7 @@ func TestExpiration_Renew_RevokeOnExpire(t *testing.T) { Path: "prod/aws/foo", ClientToken: "foobar", } + req.SetTokenEntry(&logical.TokenEntry{ID: "foobar", NamespaceID: "root"}) resp := &logical.Response{ Secret: &logical.Secret{ LeaseOptions: logical.LeaseOptions{ @@ -1407,7 +1418,7 @@ func TestExpiration_revokeEntry_token(t *testing.T) { if err := exp.persistEntry(namespace.TestContext(), le); err != nil { t.Fatalf("error persisting entry: %v", err) } - if err := exp.createIndexByToken(namespace.TestContext(), le); err != nil { + if err := exp.createIndexByToken(namespace.TestContext(), le, le.ClientToken); err != nil { t.Fatalf("error creating secondary index: %v", err) } exp.updatePending(le, le.Secret.LeaseTotal()) @@ -1802,6 +1813,7 @@ func TestExpiration_RevokeForce(t *testing.T) { Path: "badrenew/creds", ClientToken: root, } + req.SetTokenEntry(&logical.TokenEntry{ID: root, NamespaceID: "root", Policies: []string{"root"}}) resp, err := core.HandleRequest(namespace.TestContext(), req) if err != nil { @@ -1850,6 +1862,7 @@ func TestExpiration_RevokeForceSingle(t *testing.T) { Path: "badrenew/creds", ClientToken: root, } + req.SetTokenEntry(&logical.TokenEntry{ID: root, NamespaceID: "root", Policies: []string{"root"}}) resp, err := core.HandleRequest(namespace.TestContext(), req) if err != nil { diff --git a/vault/external_tests/token/batch_token_test.go b/vault/external_tests/token/batch_token_test.go new file mode 100644 index 0000000000..dc64732536 --- /dev/null +++ b/vault/external_tests/token/batch_token_test.go @@ -0,0 +1,420 @@ +package token + +import ( + "strings" + "testing" + "time" + + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/builtin/credential/approle" + vaulthttp "github.com/hashicorp/vault/http" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/vault" +) + +func TestBatchTokens(t *testing.T) { + coreConfig := &vault.CoreConfig{ + LogicalBackends: map[string]logical.Factory{ + "kv": vault.LeasedPassthroughBackendFactory, + }, + CredentialBackends: map[string]logical.Factory{ + "approle": approle.Factory, + }, + } + cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + core := cluster.Cores[0].Core + vault.TestWaitActive(t, core) + client := cluster.Cores[0].Client + rootToken := client.Token() + var err error + + // Set up a KV path + err = client.Sys().Mount("kv", &api.MountInput{ + Type: "kv", + }) + if err != nil { + t.Fatal(err) + } + + _, err = client.Logical().Write("kv/foo", map[string]interface{}{ + "foo": "bar", + "ttl": "5m", + }) + if err != nil { + t.Fatal(err) + } + + // Write the test policy + err = client.Sys().PutPolicy("test", ` +path "kv/*" { + capabilities = ["read"] +}`) + if err != nil { + t.Fatal(err) + } + + // Mount the auth backend + err = client.Sys().EnableAuthWithOptions("approle", &api.EnableAuthOptions{ + Type: "approle", + }) + if err != nil { + t.Fatal(err) + } + + // Tune the mount + if err = client.Sys().TuneMount("auth/approle", api.MountConfigInput{ + DefaultLeaseTTL: "5s", + MaxLeaseTTL: "5s", + }); err != nil { + t.Fatal(err) + } + + // Create role + resp, err := client.Logical().Write("auth/approle/role/test", map[string]interface{}{ + "policies": "test", + }) + if err != nil { + t.Fatal(err) + } + + // Get role_id + resp, err = client.Logical().Read("auth/approle/role/test/role-id") + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("expected a response for fetching the role-id") + } + roleID := resp.Data["role_id"] + + // Get secret_id + resp, err = client.Logical().Write("auth/approle/role/test/secret-id", map[string]interface{}{}) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("expected a response for fetching the secret-id") + } + secretID := resp.Data["secret_id"] + + // Login + testLogin := func(mountTuneType, roleType string, batch bool) string { + t.Helper() + if err = client.Sys().TuneMount("auth/approle", api.MountConfigInput{ + TokenType: mountTuneType, + }); err != nil { + t.Fatal(err) + } + _, err = client.Logical().Write("auth/approle/role/test", map[string]interface{}{ + "token_type": roleType, + }) + if err != nil { + t.Fatal(err) + } + + resp, err = client.Logical().Write("auth/approle/login", map[string]interface{}{ + "role_id": roleID, + "secret_id": secretID, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("expected a response for login") + } + if resp.Auth == nil { + t.Fatal("expected auth object from response") + } + if resp.Auth.ClientToken == "" { + t.Fatal("expected a client token") + } + if batch && !strings.HasPrefix(resp.Auth.ClientToken, "b.") { + t.Fatal("expected a batch token") + } + if !batch && strings.HasPrefix(resp.Auth.ClientToken, "b.") { + t.Fatal("expected a non-batch token") + } + return resp.Auth.ClientToken + } + testLogin("service", "default", false) + testLogin("service", "batch", false) + testLogin("service", "service", false) + testLogin("batch", "default", true) + testLogin("batch", "batch", true) + testLogin("batch", "service", true) + testLogin("default-service", "default", false) + testLogin("default-service", "batch", true) + testLogin("default-service", "service", false) + testLogin("default-batch", "default", true) + testLogin("default-batch", "batch", true) + testLogin("default-batch", "service", false) + + finalToken := testLogin("batch", "batch", true) + + client.SetToken(finalToken) + resp, err = client.Logical().Read("kv/foo") + if err != nil { + t.Fatal(err) + } + if resp.Data["foo"].(string) != "bar" { + t.Fatal("bad") + } + if resp.LeaseID == "" { + t.Fatal("expected lease") + } + if !resp.Renewable { + t.Fatal("expected renewable") + } + if resp.LeaseDuration > 5 { + t.Fatalf("lease duration too big: %d", resp.LeaseDuration) + } + leaseID := resp.LeaseID + + lastDuration := resp.LeaseDuration + for i := 0; i < 3; i++ { + time.Sleep(time.Second) + resp, err = client.Sys().Renew(leaseID, 0) + if err != nil { + t.Fatal(err) + } + if resp.LeaseDuration >= lastDuration { + t.Fatal("expected duration to go down") + } + lastDuration = resp.LeaseDuration + } + + client.SetToken(rootToken) + time.Sleep(2 * time.Second) + resp, err = client.Logical().Write("sys/leases/lookup", map[string]interface{}{ + "lease_id": leaseID, + }) + if err == nil { + t.Fatal("expected error") + } +} + +func TestBatchToken_ParentLeaseRevoke(t *testing.T) { + coreConfig := &vault.CoreConfig{ + LogicalBackends: map[string]logical.Factory{ + "kv": vault.LeasedPassthroughBackendFactory, + }, + CredentialBackends: map[string]logical.Factory{ + "approle": approle.Factory, + }, + } + cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + core := cluster.Cores[0].Core + vault.TestWaitActive(t, core) + client := cluster.Cores[0].Client + rootToken := client.Token() + var err error + + // Set up a KV path + err = client.Sys().Mount("kv", &api.MountInput{ + Type: "kv", + }) + if err != nil { + t.Fatal(err) + } + + _, err = client.Logical().Write("kv/foo", map[string]interface{}{ + "foo": "bar", + "ttl": "5m", + }) + if err != nil { + t.Fatal(err) + } + + // Write the test policy + err = client.Sys().PutPolicy("test", ` +path "kv/*" { + capabilities = ["read"] +}`) + if err != nil { + t.Fatal(err) + } + + // Create a second root token + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"root"}, + }) + if err != nil { + t.Fatal(err) + } + rootToken2 := secret.Auth.ClientToken + + // Use this new token to create a batch token + client.SetToken(rootToken2) + secret, err = client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"test"}, + Type: "batch", + }) + if err != nil { + t.Fatal(err) + } + batchToken := secret.Auth.ClientToken + client.SetToken(batchToken) + _, err = client.Auth().Token().LookupSelf() + if err != nil { + t.Fatal(err) + } + if secret.Auth.ClientToken[0:2] != "b." { + t.Fatal(secret.Auth.ClientToken) + } + + // Get a lease with the batch token + resp, err := client.Logical().Read("kv/foo") + if err != nil { + t.Fatal(err) + } + if resp.Data["foo"].(string) != "bar" { + t.Fatal("bad") + } + if resp.LeaseID == "" { + t.Fatal("expected lease") + } + leaseID := resp.LeaseID + + // Check the lease + resp, err = client.Logical().Write("sys/leases/lookup", map[string]interface{}{ + "lease_id": leaseID, + }) + if err != nil { + t.Fatal(err) + } + + // Revoke the parent + client.SetToken(rootToken2) + err = client.Auth().Token().RevokeSelf("") + if err != nil { + t.Fatal(err) + } + + // Verify the batch token is not usable anymore + client.SetToken(rootToken) + _, err = client.Auth().Token().Lookup(batchToken) + if err == nil { + t.Fatal("expected error") + } + + // Verify the lease has been revoked + resp, err = client.Logical().Write("sys/leases/lookup", map[string]interface{}{ + "lease_id": leaseID, + }) + if err == nil { + t.Fatal("expected error") + } +} + +func TestTokenStore_Roles_Batch(t *testing.T) { + cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + core := cluster.Cores[0].Core + vault.TestWaitActive(t, core) + client := cluster.Cores[0].Client + rootToken := client.Token() + + var err error + var secret *api.Secret + + _, err = client.Logical().Write("auth/token/roles/testrole", map[string]interface{}{ + "bound_cidrs": []string{}, + }) + if err != nil { + t.Fatal(err) + } + secret, err = client.Auth().Token().CreateWithRole(&api.TokenCreateRequest{ + Policies: []string{"default"}, + Type: "batch", + }, "testrole") + if err != nil { + t.Fatal(err) + } + client.SetToken(secret.Auth.ClientToken) + _, err = client.Auth().Token().LookupSelf() + if err != nil { + t.Fatal(err) + } + if secret.Auth.ClientToken[0:2] == "b." { + t.Fatal(secret.Auth.ClientToken) + } + + // Test batch + client.SetToken(rootToken) + _, err = client.Logical().Write("auth/token/roles/testrole", map[string]interface{}{ + "token_type": "batch", + }) + // Orphan not set so we should error + if err == nil { + t.Fatal("expected error") + } + _, err = client.Logical().Write("auth/token/roles/testrole", map[string]interface{}{ + "token_type": "batch", + "orphan": true, + }) + // Renewable set so we should error + if err == nil { + t.Fatal("expected error") + } + _, err = client.Logical().Write("auth/token/roles/testrole", map[string]interface{}{ + "token_type": "batch", + "orphan": true, + "renewable": false, + }) + if err != nil { + t.Fatal(err) + } + secret, err = client.Auth().Token().CreateWithRole(&api.TokenCreateRequest{ + Policies: []string{"default"}, + Type: "service", + }, "testrole") + if err != nil { + t.Fatal(err) + } + client.SetToken(secret.Auth.ClientToken) + _, err = client.Auth().Token().LookupSelf() + if err != nil { + t.Fatal(err) + } + if secret.Auth.ClientToken[0:2] != "b." { + t.Fatal(secret.Auth.ClientToken) + } + + // Back to normal + client.SetToken(rootToken) + _, err = client.Logical().Write("auth/token/roles/testrole", map[string]interface{}{ + "token_type": "service", + }) + if err != nil { + t.Fatal(err) + } + secret, err = client.Auth().Token().CreateWithRole(&api.TokenCreateRequest{ + Policies: []string{"default"}, + Type: "batch", + }, "testrole") + if err != nil { + t.Fatal(err) + } + client.SetToken(secret.Auth.ClientToken) + _, err = client.Auth().Token().LookupSelf() + if err != nil { + t.Fatal(err) + } + if secret.Auth.ClientToken[0:2] == "b." { + t.Fatal(secret.Auth.ClientToken) + } +} diff --git a/vault/ha.go b/vault/ha.go index adb4f0e5fe..cd0d1467a1 100644 --- a/vault/ha.go +++ b/vault/ha.go @@ -223,6 +223,7 @@ func (c *Core) StepDown(httpCtx context.Context, req *logical.Request) (retErr e // Audit-log the request before going any further auth := &logical.Auth{ ClientToken: req.ClientToken, + Accessor: req.ClientTokenAccessor, } if te != nil { auth.IdentityPolicies = identityPolicies[te.NamespaceID] @@ -233,6 +234,7 @@ func (c *Core) StepDown(httpCtx context.Context, req *logical.Request) (retErr e auth.Metadata = te.Meta auth.DisplayName = te.DisplayName auth.EntityID = te.EntityID + auth.TokenType = te.Type } logInput := &audit.LogInput{ diff --git a/vault/logical_system.go b/vault/logical_system.go index a8fae20d33..d279d9fa39 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -601,6 +601,9 @@ func mountInfo(entry *MountEntry) map[string]interface{} { if rawVal, ok := entry.synthesizedConfigCache.Load("passthrough_request_headers"); ok { entryConfig["passthrough_request_headers"] = rawVal.([]string) } + if entry.Table == credentialTableType { + entryConfig["token_type"] = entry.Config.TokenType.String() + } info["config"] = entryConfig @@ -951,6 +954,10 @@ func (b *SystemBackend) handleTuneReadCommon(ctx context.Context, path string) ( }, } + if mountEntry.Table == credentialTableType { + resp.Data["token_type"] = mountEntry.Config.TokenType.String() + } + if rawVal, ok := mountEntry.synthesizedConfigCache.Load("audit_non_hmac_request_keys"); ok { resp.Data["audit_non_hmac_request_keys"] = rawVal.([]string) } @@ -1192,6 +1199,44 @@ func (b *SystemBackend) handleTuneWriteCommon(ctx context.Context, path string, } } + if rawVal, ok := data.GetOk("token_type"); ok { + if !strings.HasPrefix(path, "auth/") { + return logical.ErrorResponse(fmt.Sprintf("'token_type' can only be modified on auth mounts")), logical.ErrInvalidRequest + } + if mountEntry.Type == "token" || mountEntry.Type == "ns_token" { + return logical.ErrorResponse(fmt.Sprintf("'token_type' cannot be set for 'token' or 'ns_token' auth mounts")), logical.ErrInvalidRequest + } + + tokenType := logical.TokenTypeDefaultService + ttString := rawVal.(string) + + switch ttString { + case "", "default-service": + case "default-batch": + tokenType = logical.TokenTypeDefaultBatch + case "service": + tokenType = logical.TokenTypeService + case "batch": + tokenType = logical.TokenTypeBatch + default: + return logical.ErrorResponse(fmt.Sprintf( + "invalid value for 'token_type'")), logical.ErrInvalidRequest + } + + oldVal := mountEntry.Config.TokenType + mountEntry.Config.TokenType = tokenType + + // Update the mount table + if err := b.Core.persistAuth(ctx, b.Core.auth, &mountEntry.Local); err != nil { + mountEntry.Config.TokenType = oldVal + return handleError(err) + } + + if b.Core.logger.IsInfo() { + b.Core.logger.Info("mount tuning of token_type successful", "path", path, "token_type", ttString) + } + } + if rawVal, ok := data.GetOk("passthrough_request_headers"); ok { headers := rawVal.([]string) @@ -1467,37 +1512,10 @@ func (b *SystemBackend) handleAuthTable(ctx context.Context, req *logical.Reques continue } - info := map[string]interface{}{ - "type": entry.Type, - "description": entry.Description, - "accessor": entry.Accessor, - "local": entry.Local, - "seal_wrap": entry.SealWrap, - "options": entry.Options, - } - entryConfig := map[string]interface{}{ - "default_lease_ttl": int64(entry.Config.DefaultLeaseTTL.Seconds()), - "max_lease_ttl": int64(entry.Config.MaxLeaseTTL.Seconds()), - "plugin_name": entry.Config.PluginName, - } - if rawVal, ok := entry.synthesizedConfigCache.Load("audit_non_hmac_request_keys"); ok { - entryConfig["audit_non_hmac_request_keys"] = rawVal.([]string) - } - if rawVal, ok := entry.synthesizedConfigCache.Load("audit_non_hmac_response_keys"); ok { - entryConfig["audit_non_hmac_response_keys"] = rawVal.([]string) - } - // Even though empty value is valid for ListingVisibility, we can ignore - // this case during mount since there's nothing to unset/hide. - if len(entry.Config.ListingVisibility) > 0 { - entryConfig["listing_visibility"] = entry.Config.ListingVisibility - } - if rawVal, ok := entry.synthesizedConfigCache.Load("passthrough_request_headers"); ok { - entryConfig["passthrough_request_headers"] = rawVal.([]string) - } - - info["config"] = entryConfig + info := mountInfo(entry) resp.Data[entry.Path] = info } + return resp, nil } @@ -1569,6 +1587,20 @@ func (b *SystemBackend) handleEnableAuth(ctx context.Context, req *logical.Reque logical.ErrInvalidRequest } + switch apiConfig.TokenType { + case "", "default-service": + config.TokenType = logical.TokenTypeDefaultService + case "default-batch": + config.TokenType = logical.TokenTypeDefaultBatch + case "service": + config.TokenType = logical.TokenTypeService + case "batch": + config.TokenType = logical.TokenTypeBatch + default: + return logical.ErrorResponse(fmt.Sprintf( + "invalid value for 'token_type'")), logical.ErrInvalidRequest + } + switch logicalType { case "": return logical.ErrorResponse( @@ -3581,6 +3613,10 @@ This path responds to the following HTTP methods. "A list of headers to whitelist and pass from the request to the backend.", "", }, + "token_type": { + "The type of token to issue (service or batch).", + "", + }, "raw": { "Write, Read, and Delete data directly in the Storage backend.", "", diff --git a/vault/logical_system_paths.go b/vault/logical_system_paths.go index 3f9c2f0d86..e44412ccf4 100644 --- a/vault/logical_system_paths.go +++ b/vault/logical_system_paths.go @@ -1010,6 +1010,10 @@ func (b *SystemBackend) mountPaths() []*framework.Path { Type: framework.TypeCommaStringSlice, Description: strings.TrimSpace(sysHelp["passthrough_request_headers"][0]), }, + "token_type": &framework.FieldSchema{ + Type: framework.TypeString, + Description: strings.TrimSpace(sysHelp["token_type"][0]), + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index 8a1001e6bf..017327d9fb 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -15,6 +15,7 @@ import ( "time" "github.com/fatih/structs" + "github.com/go-test/deep" hclog "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/audit" "github.com/hashicorp/vault/helper/builtinplugins" @@ -447,7 +448,7 @@ func TestSystemBackend_PathCapabilities(t *testing.T) { rootCheckFunc(t, resp) // Create a non-root token - testMakeTokenViaBackend(t, core.tokenStore, rootToken, "tokenid", "", []string{"test"}) + testMakeServiceTokenViaBackend(t, core.tokenStore, rootToken, "tokenid", "", []string{"test"}) nonRootCheckFunc := func(t *testing.T, resp *logical.Response) { expected1 := []string{"create", "sudo", "update"} @@ -549,7 +550,7 @@ func testCapabilities(t *testing.T, endpoint string) { t.Fatalf("err: %v", err) } - testMakeTokenViaBackend(t, core.tokenStore, rootToken, "tokenid", "", []string{"test"}) + testMakeServiceTokenViaBackend(t, core.tokenStore, rootToken, "tokenid", "", []string{"test"}) req = logical.TestRequest(t, logical.UpdateOperation, endpoint) if endpoint == "capabilities-self" { req.ClientToken = "tokenid" @@ -605,7 +606,7 @@ func TestSystemBackend_CapabilitiesAccessor_BC(t *testing.T) { t.Fatalf("err: %v", err) } - testMakeTokenViaBackend(t, core.tokenStore, rootToken, "tokenid", "", []string{"test"}) + testMakeServiceTokenViaBackend(t, core.tokenStore, rootToken, "tokenid", "", []string{"test"}) te, err = core.tokenStore.Lookup(namespace.TestContext(), "tokenid") if err != nil { @@ -696,6 +697,7 @@ func TestSystemBackend_leases(t *testing.T) { // Read a key with a LeaseID req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") req.ClientToken = root + req.SetTokenEntry(&logical.TokenEntry{ID: root, NamespaceID: "root", Policies: []string{"root"}}) resp, err = core.HandleRequest(namespace.TestContext(), req) if err != nil { t.Fatalf("err: %v", err) @@ -742,6 +744,7 @@ func TestSystemBackend_leases_list(t *testing.T) { // Read a key with a LeaseID req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") req.ClientToken = root + req.SetTokenEntry(&logical.TokenEntry{ID: root, NamespaceID: "root", Policies: []string{"root"}}) resp, err = core.HandleRequest(namespace.TestContext(), req) if err != nil { t.Fatalf("err: %v", err) @@ -790,6 +793,7 @@ func TestSystemBackend_leases_list(t *testing.T) { // Generate multiple leases req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") req.ClientToken = root + req.SetTokenEntry(&logical.TokenEntry{ID: root, NamespaceID: "root", Policies: []string{"root"}}) resp, err = core.HandleRequest(namespace.TestContext(), req) if err != nil { t.Fatalf("err: %v", err) @@ -800,6 +804,7 @@ func TestSystemBackend_leases_list(t *testing.T) { req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") req.ClientToken = root + req.SetTokenEntry(&logical.TokenEntry{ID: root, NamespaceID: "root", Policies: []string{"root"}}) resp, err = core.HandleRequest(namespace.TestContext(), req) if err != nil { t.Fatalf("err: %v", err) @@ -839,6 +844,7 @@ func TestSystemBackend_leases_list(t *testing.T) { // Read a key with a LeaseID req = logical.TestRequest(t, logical.ReadOperation, "secret/bar") req.ClientToken = root + req.SetTokenEntry(&logical.TokenEntry{ID: root, NamespaceID: "root", Policies: []string{"root"}}) resp, err = core.HandleRequest(namespace.TestContext(), req) if err != nil { t.Fatalf("err: %v", err) @@ -886,6 +892,7 @@ func TestSystemBackend_renew(t *testing.T) { // Read a key with a LeaseID req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") req.ClientToken = root + req.SetTokenEntry(&logical.TokenEntry{ID: root, NamespaceID: "root", Policies: []string{"root"}}) resp, err = core.HandleRequest(namespace.TestContext(), req) if err != nil { t.Fatalf("err: %v", err) @@ -922,6 +929,7 @@ func TestSystemBackend_renew(t *testing.T) { // Read a key with a LeaseID req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") req.ClientToken = root + req.SetTokenEntry(&logical.TokenEntry{ID: root, NamespaceID: "root", Policies: []string{"root"}}) resp, err = core.HandleRequest(namespace.TestContext(), req) if err != nil { t.Fatalf("err: %v", err) @@ -976,7 +984,7 @@ func TestSystemBackend_renew(t *testing.T) { if resp2.Data == nil { t.Fatal("nil data") } - if resp.Secret.TTL != 180*time.Second { + if resp.Secret.TTL != time.Second*180 { t.Fatalf("bad lease duration: %v", resp.Secret.TTL) } } @@ -990,7 +998,7 @@ func TestSystemBackend_renew_invalidID(t *testing.T) { if err != logical.ErrInvalidRequest { t.Fatalf("err: %v", err) } - if resp.Data["error"] != "lease not found or lease is not renewable" { + if resp.Data["error"] != "lease not found" { t.Fatalf("bad: %v", resp) } @@ -1001,7 +1009,7 @@ func TestSystemBackend_renew_invalidID(t *testing.T) { if err != logical.ErrInvalidRequest { t.Fatalf("err: %v", err) } - if resp.Data["error"] != "lease not found or lease is not renewable" { + if resp.Data["error"] != "lease not found" { t.Fatalf("bad: %v", resp) } } @@ -1015,7 +1023,7 @@ func TestSystemBackend_renew_invalidID_origUrl(t *testing.T) { if err != logical.ErrInvalidRequest { t.Fatalf("err: %v", err) } - if resp.Data["error"] != "lease not found or lease is not renewable" { + if resp.Data["error"] != "lease not found" { t.Fatalf("bad: %v", resp) } @@ -1026,7 +1034,7 @@ func TestSystemBackend_renew_invalidID_origUrl(t *testing.T) { if err != logical.ErrInvalidRequest { t.Fatalf("err: %v", err) } - if resp.Data["error"] != "lease not found or lease is not renewable" { + if resp.Data["error"] != "lease not found" { t.Fatalf("bad: %v", resp) } } @@ -1050,6 +1058,7 @@ func TestSystemBackend_revoke(t *testing.T) { // Read a key with a LeaseID req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") req.ClientToken = root + req.SetTokenEntry(&logical.TokenEntry{ID: root, NamespaceID: "root", Policies: []string{"root"}}) resp, err = core.HandleRequest(namespace.TestContext(), req) if err != nil { t.Fatalf("err: %v", err) @@ -1074,13 +1083,14 @@ func TestSystemBackend_revoke(t *testing.T) { if err != logical.ErrInvalidRequest { t.Fatalf("err: %v", err) } - if resp3.Data["error"] != "lease not found or lease is not renewable" { - t.Fatalf("bad: %v", resp) + if resp3.Data["error"] != "lease not found" { + t.Fatalf("bad: %v", *resp3) } // Read a key with a LeaseID req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") req.ClientToken = root + req.SetTokenEntry(&logical.TokenEntry{ID: root, NamespaceID: "root", Policies: []string{"root"}}) resp, err = core.HandleRequest(namespace.TestContext(), req) if err != nil { t.Fatalf("err: %v", err) @@ -1103,6 +1113,7 @@ func TestSystemBackend_revoke(t *testing.T) { // Read a key with a LeaseID req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") req.ClientToken = root + req.SetTokenEntry(&logical.TokenEntry{ID: root, NamespaceID: "root", Policies: []string{"root"}}) resp, err = core.HandleRequest(namespace.TestContext(), req) if err != nil { t.Fatalf("err: %v", err) @@ -1192,6 +1203,7 @@ func TestSystemBackend_revokePrefix(t *testing.T) { // Read a key with a LeaseID req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") req.ClientToken = root + req.SetTokenEntry(&logical.TokenEntry{ID: root, NamespaceID: "root", Policies: []string{"root"}}) resp, err = core.HandleRequest(namespace.TestContext(), req) if err != nil { t.Fatalf("err: %v", err) @@ -1216,8 +1228,8 @@ func TestSystemBackend_revokePrefix(t *testing.T) { if err != logical.ErrInvalidRequest { t.Fatalf("err: %v", err) } - if resp3.Data["error"] != "lease not found or lease is not renewable" { - t.Fatalf("bad: %v", resp) + if resp3.Data["error"] != "lease not found" { + t.Fatalf("bad: %v", *resp3) } } @@ -1240,6 +1252,7 @@ func TestSystemBackend_revokePrefix_origUrl(t *testing.T) { // Read a key with a LeaseID req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") req.ClientToken = root + req.SetTokenEntry(&logical.TokenEntry{ID: root, NamespaceID: "root", Policies: []string{"root"}}) resp, err = core.HandleRequest(namespace.TestContext(), req) if err != nil { t.Fatalf("err: %v", err) @@ -1264,8 +1277,8 @@ func TestSystemBackend_revokePrefix_origUrl(t *testing.T) { if err != logical.ErrInvalidRequest { t.Fatalf("err: %v", err) } - if resp3.Data["error"] != "lease not found or lease is not renewable" { - t.Fatalf("bad: %v", resp) + if resp3.Data["error"] != "lease not found" { + t.Fatalf("bad: %#v", *resp3) } } @@ -1415,14 +1428,16 @@ func TestSystemBackend_authTable(t *testing.T) { "default_lease_ttl": int64(0), "max_lease_ttl": int64(0), "plugin_name": "", + "force_no_cache": false, + "token_type": "default-service", }, "local": false, "seal_wrap": false, "options": map[string]string(nil), }, } - if !reflect.DeepEqual(resp.Data, exp) { - t.Fatalf("got: %#v expect: %#v", resp.Data, exp) + if diff := deep.Equal(resp.Data, exp); diff != nil { + t.Fatal(diff) } } @@ -1466,7 +1481,9 @@ func TestSystemBackend_enableAuth(t *testing.T) { "config": map[string]interface{}{ "default_lease_ttl": int64(2100), "max_lease_ttl": int64(2700), + "force_no_cache": false, "plugin_name": "", + "token_type": "default-service", }, "local": true, "seal_wrap": true, @@ -1480,14 +1497,16 @@ func TestSystemBackend_enableAuth(t *testing.T) { "default_lease_ttl": int64(0), "max_lease_ttl": int64(0), "plugin_name": "", + "force_no_cache": false, + "token_type": "default-service", }, "local": false, "seal_wrap": false, "options": map[string]string(nil), }, } - if !reflect.DeepEqual(resp.Data, exp) { - t.Fatalf("got: %#v expect: %#v", resp.Data, exp) + if diff := deep.Equal(resp.Data, exp); diff != nil { + t.Fatal(diff) } } @@ -2300,6 +2319,7 @@ func TestSystemBackend_InternalUIMounts(t *testing.T) { "max_lease_ttl": int64(0), "force_no_cache": false, "plugin_name": "", + "token_type": "default-service", }, "type": "token", "description": "token based credentials", @@ -2309,8 +2329,8 @@ func TestSystemBackend_InternalUIMounts(t *testing.T) { }, }, } - if !reflect.DeepEqual(resp.Data, exp) { - t.Fatalf("got: %#v \n\n expect: %#v", resp.Data, exp) + if diff := deep.Equal(resp.Data, exp); diff != nil { + t.Fatal(diff) } // Mount-tune an auth mount @@ -2391,7 +2411,7 @@ func TestSystemBackend_InternalUIMount(t *testing.T) { t.Fatalf("Bad Response: %#v", resp) } - testMakeTokenViaBackend(t, core.tokenStore, rootToken, "tokenid", "", []string{"secret"}) + testMakeServiceTokenViaBackend(t, core.tokenStore, rootToken, "tokenid", "", []string{"secret"}) req = logical.TestRequest(t, logical.ReadOperation, "internal/ui/mounts/kv") req.ClientToken = "tokenid" diff --git a/vault/mount.go b/vault/mount.go index 4e3b3cb36a..fcf481a2d3 100644 --- a/vault/mount.go +++ b/vault/mount.go @@ -226,6 +226,7 @@ type MountConfig struct { AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"` ListingVisibility ListingVisibilityType `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"` PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" structs:"passthrough_request_headers" mapstructure:"passthrough_request_headers"` + TokenType logical.TokenType `json:"token_type" structs:"token_type" mapstructure:"token_type"` } // APIMountConfig is an embedded struct of api.MountConfigInput @@ -238,6 +239,7 @@ type APIMountConfig struct { AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"` ListingVisibility ListingVisibilityType `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"` PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" structs:"passthrough_request_headers" mapstructure:"passthrough_request_headers"` + TokenType string `json:"token_type" structs:"token_type" mapstructure:"token_type"` } // Clone returns a deep copy of the mount entry diff --git a/vault/mount_test.go b/vault/mount_test.go index d334e144fc..2978c743e9 100644 --- a/vault/mount_test.go +++ b/vault/mount_test.go @@ -295,6 +295,7 @@ func TestCore_Unmount_Cleanup(t *testing.T) { Path: "test/foo", ClientToken: root, } + r.SetTokenEntry(&logical.TokenEntry{ID: root, NamespaceID: "root", Policies: []string{"root"}}) resp, err := c.HandleRequest(namespace.TestContext(), r) if err != nil { t.Fatalf("err: %v", err) @@ -415,6 +416,7 @@ func TestCore_Remount_Cleanup(t *testing.T) { Path: "test/foo", ClientToken: root, } + r.SetTokenEntry(&logical.TokenEntry{ID: root, NamespaceID: "root", Policies: []string{"root"}}) resp, err := c.HandleRequest(namespace.TestContext(), r) if err != nil { t.Fatalf("err: %v", err) diff --git a/vault/request_handling.go b/vault/request_handling.go index 461aedd086..eb43266607 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -269,19 +269,22 @@ func (c *Core) checkToken(ctx context.Context, req *logical.Request, unauth bool // whether a particular resource exists. Then we can mark it as an update // or creation as appropriate. if req.Operation == logical.CreateOperation || req.Operation == logical.UpdateOperation { - checkExists, resourceExists, err := c.router.RouteExistenceCheck(ctx, req) + existsResp, checkExists, resourceExists, err := c.router.RouteExistenceCheck(ctx, req) switch err { case logical.ErrUnsupportedPath: // fail later via bad path to avoid confusing items in the log checkExists = false case nil: - // Continue on + if existsResp != nil && existsResp.IsError() { + return nil, te, existsResp.Error() + } + // Otherwise, continue on default: c.logger.Error("failed to run existence check", "error", err) if _, ok := err.(errutil.UserError); ok { - return nil, nil, err + return nil, te, err } else { - return nil, nil, ErrInternalError + return nil, te, ErrInternalError } } @@ -316,6 +319,7 @@ func (c *Core) checkToken(ctx context.Context, req *logical.Request, unauth bool auth.ExternalNamespacePolicies = identityPolicies // Store the entity ID in the request object req.EntityID = te.EntityID + auth.TokenType = te.Type } // Check the standard non-root ACLs. Return the token entry if it's not @@ -744,6 +748,19 @@ func (c *Core) handleRequest(ctx context.Context, req *logical.Request) (retResp return nil, auth, retErr } resp.Secret.LeaseID = leaseID + + // Get the actual time of the lease + le, err := c.expiration.FetchLeaseTimes(ctx, leaseID) + if err != nil { + c.logger.Error("failed to fetch updated lease time", "request_path", req.Path, "error", err) + retErr = multierror.Append(retErr, ErrInternalError) + return nil, auth, retErr + } + // We round here because the clock will have already started + // ticking, so we'll end up always returning 299 instead of 300 or + // 26399 instead of 26400, say, even if it's just a few + // microseconds. This provides a nicer UX. + resp.Secret.TTL = le.ExpireTime.Sub(time.Now()).Round(time.Second) } } @@ -777,14 +794,18 @@ func (c *Core) handleRequest(ctx context.Context, req *logical.Request) (retResp } resp.Auth.TokenPolicies = policyutil.SanitizePolicies(resp.Auth.Policies, policyutil.DoNotAddDefaultPolicy) - if err := c.expiration.RegisterAuth(ctx, &logical.TokenEntry{ - Path: resp.Auth.CreationPath, - NamespaceID: ns.ID, - }, resp.Auth); err != nil { - c.tokenStore.revokeOrphan(ctx, te.ID) - c.logger.Error("failed to register token lease", "request_path", req.Path, "error", err) - retErr = multierror.Append(retErr, ErrInternalError) - return nil, auth, retErr + switch resp.Auth.TokenType { + case logical.TokenTypeBatch: + case logical.TokenTypeService: + if err := c.expiration.RegisterAuth(ctx, &logical.TokenEntry{ + Path: resp.Auth.CreationPath, + NamespaceID: ns.ID, + }, resp.Auth); err != nil { + c.tokenStore.revokeOrphan(ctx, te.ID) + c.logger.Error("failed to register token lease", "request_path", req.Path, "error", err) + retErr = multierror.Append(retErr, ErrInternalError) + return nil, auth, retErr + } } // We do these later since it's not meaningful for backends/expmgr to @@ -1031,7 +1052,14 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re } } - registerFunc, funcGetErr := getAuthRegisterFunc(c) + var registerFunc RegisterAuthFunc + var funcGetErr error + // Batch tokens should not be forwarded to perf standby + if auth.TokenType == logical.TokenTypeBatch { + registerFunc = c.RegisterAuth + } else { + registerFunc, funcGetErr = getAuthRegisterFunc(c) + } if funcGetErr != nil { retErr = multierror.Append(retErr, funcGetErr) return nil, auth, retErr @@ -1083,6 +1111,7 @@ func (c *Core) RegisterAuth(ctx context.Context, tokenTTL time.Duration, path st Policies: auth.TokenPolicies, NamespaceID: ns.ID, ExplicitMaxTTL: auth.ExplicitMaxTTL, + Type: auth.TokenType, } if err := c.tokenStore.create(ctx, &te); err != nil { @@ -1095,11 +1124,17 @@ func (c *Core) RegisterAuth(ctx context.Context, tokenTTL time.Duration, path st auth.Accessor = te.Accessor auth.TTL = te.TTL - // Register with the expiration manager - if err := c.expiration.RegisterAuth(ctx, &te, auth); err != nil { - c.tokenStore.revokeOrphan(ctx, te.ID) - c.logger.Error("failed to register token lease", "request_path", path, "error", err) - return ErrInternalError + switch auth.TokenType { + case logical.TokenTypeBatch: + // Ensure it's not marked renewable since it isn't + auth.Renewable = false + case logical.TokenTypeService: + // Register with the expiration manager + if err := c.expiration.RegisterAuth(ctx, &te, auth); err != nil { + c.tokenStore.revokeOrphan(ctx, te.ID) + c.logger.Error("failed to register token lease", "request_path", path, "error", err) + return ErrInternalError + } } return nil diff --git a/vault/router.go b/vault/router.go index 1444d872ab..18d07fdfb5 100644 --- a/vault/router.go +++ b/vault/router.go @@ -468,9 +468,9 @@ func (r *Router) Route(ctx context.Context, req *logical.Request) (*logical.Resp } // RouteExistenceCheck is used to route a given existence check request -func (r *Router) RouteExistenceCheck(ctx context.Context, req *logical.Request) (bool, bool, error) { - _, ok, exists, err := r.routeCommon(ctx, req, true) - return ok, exists, err +func (r *Router) RouteExistenceCheck(ctx context.Context, req *logical.Request) (*logical.Response, bool, bool, error) { + resp, ok, exists, err := r.routeCommon(ctx, req, true) + return resp, ok, exists, err } func (r *Router) routeCommon(ctx context.Context, req *logical.Request, existenceCheck bool) (*logical.Response, bool, bool, error) { @@ -547,11 +547,17 @@ func (r *Router) routeCommon(ctx context.Context, req *logical.Request, existenc return nil, false, false, nil } - if req.TokenEntry() == nil { + te := req.TokenEntry() + + if te == nil { return nil, false, false, fmt.Errorf("nil token entry") } - switch req.TokenEntry().NamespaceID { + if te.Type != logical.TokenTypeService { + return logical.ErrorResponse(`cubbyhole operations are only supported by "service" type tokens`), false, false, nil + } + + switch te.NamespaceID { case namespace.RootNamespaceID: // In order for the token store to revoke later, we need to have the same // salted ID, so we double-salt what's going to the cubbyhole backend @@ -562,10 +568,10 @@ func (r *Router) routeCommon(ctx context.Context, req *logical.Request, existenc req.ClientToken = re.SaltID(salt.SaltID(req.ClientToken)) default: - if req.TokenEntry().CubbyholeID == "" { + if te.CubbyholeID == "" { return nil, false, false, fmt.Errorf("empty cubbyhole id") } - req.ClientToken = req.TokenEntry().CubbyholeID + req.ClientToken = te.CubbyholeID } default: @@ -644,25 +650,45 @@ func (r *Router) routeCommon(ctx context.Context, req *logical.Request, existenc return nil, ok, exists, err } else { resp, err := re.backend.HandleRequest(ctx, req) - // When a token gets renewed, the request hits this path and reaches - // token store. Token store delegates the renewal to the expiration - // manager. Expiration manager in-turn creates a different logical - // request and forwards the request to the auth backend that had - // initially authenticated the login request. The forwarding to auth - // backend will make this code path hit for the second time for the - // same renewal request. The accessors in the Alias structs should be - // of the auth backend and not of the token store. Therefore, avoiding - // the overwriting of accessors by having a check for path prefix - // having "renew". This gets applied for "renew" and "renew-self" - // requests. if resp != nil && - resp.Auth != nil && - !strings.HasPrefix(req.Path, "renew") { - if resp.Auth.Alias != nil { - resp.Auth.Alias.MountAccessor = re.mountEntry.Accessor + resp.Auth != nil { + // When a token gets renewed, the request hits this path and + // reaches token store. Token store delegates the renewal to the + // expiration manager. Expiration manager in-turn creates a + // different logical request and forwards the request to the auth + // backend that had initially authenticated the login request. The + // forwarding to auth backend will make this code path hit for the + // second time for the same renewal request. The accessors in the + // Alias structs should be of the auth backend and not of the token + // store. Therefore, avoiding the overwriting of accessors by + // having a check for path prefix having "renew". This gets applied + // for "renew" and "renew-self" requests. + if !strings.HasPrefix(req.Path, "renew") { + if resp.Auth.Alias != nil { + resp.Auth.Alias.MountAccessor = re.mountEntry.Accessor + } + for _, alias := range resp.Auth.GroupAliases { + alias.MountAccessor = re.mountEntry.Accessor + } } - for _, alias := range resp.Auth.GroupAliases { - alias.MountAccessor = re.mountEntry.Accessor + + switch re.mountEntry.Type { + case "token", "ns_token": + // Nothing; we respect what the token store is telling us and + // we don't allow tuning + default: + switch re.mountEntry.Config.TokenType { + case logical.TokenTypeService, logical.TokenTypeBatch: + resp.Auth.TokenType = re.mountEntry.Config.TokenType + case logical.TokenTypeDefault, logical.TokenTypeDefaultService: + if resp.Auth.TokenType == logical.TokenTypeDefault { + resp.Auth.TokenType = logical.TokenTypeService + } + case logical.TokenTypeDefaultBatch: + if resp.Auth.TokenType == logical.TokenTypeDefault { + resp.Auth.TokenType = logical.TokenTypeBatch + } + } } } diff --git a/vault/token_store.go b/vault/token_store.go index 6c51493f99..49c1f4c1dc 100644 --- a/vault/token_store.go +++ b/vault/token_store.go @@ -2,6 +2,7 @@ package vault import ( "context" + "encoding/base64" "encoding/json" "errors" "fmt" @@ -13,6 +14,7 @@ import ( "strings" "time" + proto "github.com/golang/protobuf/proto" "github.com/hashicorp/errwrap" log "github.com/hashicorp/go-hclog" sockaddr "github.com/hashicorp/go-sockaddr" @@ -31,6 +33,7 @@ import ( "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" + "github.com/hashicorp/vault/logical/plugin/pb" "github.com/mitchellh/mapstructure" ) @@ -183,6 +186,12 @@ func (ts *TokenStore) paths() []*framework.Path { Type: framework.TypeCommaStringSlice, Description: `Comma separated string or JSON list of CIDR blocks. If set, specifies the blocks of IP addresses which are allowed to use the generated token.`, }, + + "token_type": &framework.FieldSchema{ + Type: framework.TypeString, + Default: "service", + Description: "The type of token to generate, service or batch", + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -473,6 +482,8 @@ type TokenStore struct { core *Core + batchTokenEncryptor BarrierEncryptor + baseBarrierView *BarrierView idBarrierView *BarrierView accessorBarrierView *BarrierView @@ -515,6 +526,7 @@ func NewTokenStore(ctx context.Context, logger log.Logger, core *Core, config *l t := &TokenStore{ activeContext: ctx, core: core, + batchTokenEncryptor: core.barrier, baseBarrierView: view, idBarrierView: view.SubView(idPrefix), accessorBarrierView: view.SubView(accessorPrefix), @@ -630,6 +642,9 @@ type tsRoleEntry struct { // The set of CIDRs that tokens generated using this role will be bound to BoundCIDRs []*sockaddr.SockAddrMarshaler `json:"bound_cidrs"` + + // The type of token this role should issue + TokenType logical.TokenType `json:"token_type" mapstructure:"token_type"` } type accessorEntry struct { @@ -673,6 +688,7 @@ func (ts *TokenStore) rootToken(ctx context.Context) (*logical.TokenEntry, error DisplayName: "root", CreationTime: time.Now().Unix(), NamespaceID: namespace.RootNamespaceID, + Type: logical.TokenTypeService, } if err := ts.create(ctx, te); err != nil { return nil, err @@ -771,14 +787,6 @@ func (ts *TokenStore) createAccessor(ctx context.Context, entry *logical.TokenEn // a newly generated ID if not provided. func (ts *TokenStore) create(ctx context.Context, entry *logical.TokenEntry) error { defer metrics.MeasureSince([]string{"token", "create"}, time.Now()) - // Generate an ID if necessary - if entry.ID == "" { - var err error - entry.ID, err = base62.Random(TokenLength, true) - if err != nil { - return err - } - } tokenNS, err := NamespaceByID(ctx, entry.NamespaceID, ts.core) if err != nil { @@ -788,30 +796,99 @@ func (ts *TokenStore) create(ctx context.Context, entry *logical.TokenEntry) err return namespace.ErrNoNamespace } - if tokenNS.ID != namespace.RootNamespaceID { - entry.ID = fmt.Sprintf("%s.%s", entry.ID, tokenNS.ID) - if entry.CubbyholeID == "" { - cubbyholeID, err := base62.Random(TokenLength, true) + entry.Policies = policyutil.SanitizePolicies(entry.Policies, policyutil.DoNotAddDefaultPolicy) + + switch entry.Type { + case logical.TokenTypeDefault, logical.TokenTypeService: + // In case it was default, force to service + entry.Type = logical.TokenTypeService + + // Generate an ID if necessary + userSelectedID := true + if entry.ID == "" { + userSelectedID = false + var err error + entry.ID, err = base62.Random(TokenLength, true) if err != nil { return err } - entry.CubbyholeID = cubbyholeID } + + if tokenNS.ID != namespace.RootNamespaceID { + entry.ID = fmt.Sprintf("%s.%s", entry.ID, tokenNS.ID) + if entry.CubbyholeID == "" { + cubbyholeID, err := base62.Random(TokenLength, true) + if err != nil { + return err + } + entry.CubbyholeID = cubbyholeID + } + } + + // If the user didn't specifically pick the ID, e.g. because they were + // sudo/root, check for collision; otherwise trust the process + if userSelectedID { + exist, _ := ts.lookupInternal(ctx, entry.ID, false, true) + if exist != nil { + return fmt.Errorf("cannot create a token with a duplicate ID") + } + } + + err = ts.createAccessor(ctx, entry) + if err != nil { + return err + } + + return ts.storeCommon(ctx, entry, true) + + case logical.TokenTypeBatch: + // Ensure fields we don't support/care about are nilled, proto marshal, + // encrypt, skip persistence + entry.ID = "" + pEntry := &pb.TokenEntry{ + Parent: entry.Parent, + Policies: entry.Policies, + Path: entry.Path, + Meta: entry.Meta, + DisplayName: entry.DisplayName, + CreationTime: entry.CreationTime, + TTL: int64(entry.TTL), + Role: entry.Role, + EntityID: entry.EntityID, + NamespaceID: entry.NamespaceID, + Type: uint32(entry.Type), + } + + boundCIDRs := make([]string, len(entry.BoundCIDRs)) + for i, cidr := range entry.BoundCIDRs { + boundCIDRs[i] = cidr.String() + } + pEntry.BoundCIDRs = boundCIDRs + + mEntry, err := proto.Marshal(pEntry) + if err != nil { + return err + } + + eEntry, err := ts.batchTokenEncryptor.Encrypt(ctx, "", mEntry) + if err != nil { + return err + } + + bEntry := base64.RawURLEncoding.EncodeToString(eEntry) + entry.ID = fmt.Sprintf("b.%s", bEntry) + + if tokenNS.ID != namespace.RootNamespaceID { + entry.ID = fmt.Sprintf("%s.%s", entry.ID, tokenNS.ID) + } + + return nil + + default: + return fmt.Errorf("cannot create a token of type %d", entry.Type) } - exist, _ := ts.lookupInternal(ctx, entry.ID, false, true) - if exist != nil { - return fmt.Errorf("cannot create a token with a duplicate ID") - } - - entry.Policies = policyutil.SanitizePolicies(entry.Policies, policyutil.DoNotAddDefaultPolicy) - - err = ts.createAccessor(ctx, entry) - if err != nil { - return err - } - - return ts.storeCommon(ctx, entry, true) + return errors.New("unreachable code") } // Store is used to store an updated token entry without writing the @@ -975,6 +1052,11 @@ func (ts *TokenStore) Lookup(ctx context.Context, id string) (*logical.TokenEntr return nil, fmt.Errorf("cannot lookup blank token") } + // If it starts with "b-" it's a batch token + if len(id) > 2 && id[0:2] == "b." { + return ts.lookupBatchToken(ctx, id) + } + lock := locksutil.LockForKey(ts.tokenLocks, id) lock.RLock() defer lock.RUnlock() @@ -982,8 +1064,8 @@ func (ts *TokenStore) Lookup(ctx context.Context, id string) (*logical.TokenEntr return ts.lookupInternal(ctx, id, false, false) } -// lookupTainted is used to find a token that may or maynot be tainted given its -// ID. It acquires a read lock, then calls lookupInternal. +// lookupTainted is used to find a token that may or may not be tainted given +// its ID. It acquires a read lock, then calls lookupInternal. func (ts *TokenStore) lookupTainted(ctx context.Context, id string) (*logical.TokenEntry, error) { defer metrics.MeasureSince([]string{"token", "lookup"}, time.Now()) if id == "" { @@ -997,6 +1079,48 @@ func (ts *TokenStore) lookupTainted(ctx context.Context, id string) (*logical.To return ts.lookupInternal(ctx, id, false, true) } +func (ts *TokenStore) lookupBatchToken(ctx context.Context, id string) (*logical.TokenEntry, error) { + // Strip the b. from the front and namespace ID from the back + bEntry, _ := namespace.SplitIDFromString(id[2:]) + + eEntry, err := base64.RawURLEncoding.DecodeString(bEntry) + if err != nil { + return nil, err + } + + mEntry, err := ts.batchTokenEncryptor.Decrypt(ctx, "", eEntry) + if err != nil { + return nil, nil + } + + pEntry := new(pb.TokenEntry) + if err := proto.Unmarshal(mEntry, pEntry); err != nil { + return nil, err + } + + te, err := pb.ProtoTokenEntryToLogicalTokenEntry(pEntry) + if err != nil { + return nil, err + } + + if time.Now().After(time.Unix(te.CreationTime, 0).Add(te.TTL)) { + return nil, nil + } + + if te.Parent != "" { + pte, err := ts.Lookup(ctx, te.Parent) + if err != nil { + return nil, err + } + if pte == nil { + return nil, nil + } + } + + te.ID = id + return te, nil +} + // lookupInternal is used to find a token given its (possibly salted) ID. If // tainted is true, entries that are in some revocation state (currently, // indicated by num uses < 0), the entry will be returned anyways @@ -1006,6 +1130,11 @@ func (ts *TokenStore) lookupInternal(ctx context.Context, id string, salted, tai return nil, errwrap.Wrapf("failed to find namespace in context: {{err}}", err) } + // If it starts with "b." it's a batch token + if len(id) > 2 && id[0:2] == "b." { + return ts.lookupBatchToken(ctx, id) + } + var raw *logical.StorageEntry lookupID := id @@ -1063,6 +1192,11 @@ func (ts *TokenStore) lookupInternal(ctx context.Context, id string, salted, tai entry.NamespaceID = namespace.RootNamespaceID } + // This will be the upgrade case + if entry.Type == logical.TokenTypeDefault { + entry.Type = logical.TokenTypeService + } + persistNeeded := false // Upgrade the deprecated fields @@ -1490,6 +1624,15 @@ func (ts *TokenStore) revokeTreeInternal(ctx context.Context, id string) error { return nil } +func (c *Core) IsBatchTokenCreationRequest(ctx context.Context, path string) (bool, error) { + name := strings.TrimPrefix(path, "auth/token/create/") + roleEntry, err := c.tokenStore.tokenStoreRole(ctx, name) + if err != nil { + return false, err + } + return roleEntry.TokenType == logical.TokenTypeBatch, nil +} + // handleCreateAgainstRole handles the auth/token/create path for a role func (ts *TokenStore) handleCreateAgainstRole(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { name := d.Get("role_name").(string) @@ -1924,6 +2067,9 @@ func (ts *TokenStore) handleCreateCommon(ctx context.Context, req *logical.Reque if parent == nil { return logical.ErrorResponse("parent token lookup failed: no parent found"), logical.ErrInvalidRequest } + if parent.Type == logical.TokenTypeBatch { + return logical.ErrorResponse("batch tokens cannot create more tokens"), nil + } // A token with a restricted number of uses cannot create a new token // otherwise it could escape the restriction count. @@ -1949,6 +2095,7 @@ func (ts *TokenStore) handleCreateCommon(ctx context.Context, req *logical.Reque DisplayName string `mapstructure:"display_name"` NumUses int `mapstructure:"num_uses"` Period string + Type string `mapstructure:"type"` } if err := mapstructure.WeakDecode(req.Data, &data); err != nil { return logical.ErrorResponse(fmt.Sprintf( @@ -1982,6 +2129,56 @@ func (ts *TokenStore) handleCreateCommon(ctx context.Context, req *logical.Reque } } + renewable := true + if data.Renewable != nil { + renewable = *data.Renewable + } + + tokenType := logical.TokenTypeService + tokenTypeStr := data.Type + if role != nil { + switch role.TokenType { + case logical.TokenTypeDefault, logical.TokenTypeService: + tokenTypeStr = logical.TokenTypeService.String() + case logical.TokenTypeBatch: + tokenTypeStr = logical.TokenTypeBatch.String() + default: + return logical.ErrorResponse(fmt.Sprintf("role being used for token creation contains invalid token type %q", role.TokenType.String())), nil + } + } + switch tokenTypeStr { + case "", "service": + case "batch": + var badReason string + switch { + case data.ExplicitMaxTTL != "": + dur, err := parseutil.ParseDurationSecond(data.ExplicitMaxTTL) + if err != nil { + return logical.ErrorResponse(`"explicit_max_ttl" value could not be parsed`), nil + } + if dur != 0 { + badReason = "explicit_max_ttl" + } + case data.NumUses != 0: + badReason = "num_uses" + case data.Period != "": + dur, err := parseutil.ParseDurationSecond(data.Period) + if err != nil { + return logical.ErrorResponse(`"period" value could not be parsed`), nil + } + if dur != 0 { + badReason = "period" + } + } + if badReason != "" { + return logical.ErrorResponse(fmt.Sprintf("batch tokens cannot have %q set", badReason)), nil + } + tokenType = logical.TokenTypeBatch + renewable = false + default: + return logical.ErrorResponse("invalid 'token_type' value"), logical.ErrInvalidRequest + } + // Verify the number of uses is positive if data.NumUses < 0 { return logical.ErrorResponse("number of uses cannot be negative"), @@ -2002,11 +2199,7 @@ func (ts *TokenStore) handleCreateCommon(ctx context.Context, req *logical.Reque NumUses: data.NumUses, CreationTime: time.Now().Unix(), NamespaceID: ns.ID, - } - - renewable := true - if data.Renewable != nil { - renewable = *data.Renewable + Type: tokenType, } // If the role is not nil, we add the role name as part of the token's @@ -2169,10 +2362,17 @@ func (ts *TokenStore) handleCreateCommon(ctx context.Context, req *logical.Reque } } - // Prevent attempts to create a root token without an actual root token as parent. - // This is to thwart privilege escalation by tokens having 'sudo' privileges. - if strutil.StrListContains(data.Policies, "root") && !strutil.StrListContains(parent.Policies, "root") { - return logical.ErrorResponse("root tokens may not be created without parent token being root"), logical.ErrInvalidRequest + if strutil.StrListContains(te.Policies, "root") { + // Prevent attempts to create a root token without an actual root token as parent. + // This is to thwart privilege escalation by tokens having 'sudo' privileges. + if !strutil.StrListContains(parent.Policies, "root") { + return logical.ErrorResponse("root tokens may not be created without parent token being root"), logical.ErrInvalidRequest + } + + if te.Type == logical.TokenTypeBatch { + // Batch tokens cannot be revoked so we should never have root batch tokens + return logical.ErrorResponse("batch tokens cannot be root tokens"), nil + } } // @@ -2208,7 +2408,8 @@ func (ts *TokenStore) handleCreateCommon(ctx context.Context, req *logical.Reque // At this point, it is clear whether the token is going to be an orphan or // not. If the token is not going to be an orphan, inherit the parent's - // entity identifier into the child token. + // entity identifier into the child token. We must also verify that, if + // it's not an orphan, the parent isn't a batch token. if te.Parent != "" { te.EntityID = parent.EntityID } @@ -2269,8 +2470,10 @@ func (ts *TokenStore) handleCreateCommon(ctx context.Context, req *logical.Reque te.TTL = dur } - // Set the lesser period/explicit max TTL if defined both in arguments and in role - if role != nil { + // Set the lesser period/explicit max TTL if defined both in arguments and + // in role. Batch tokens will error out if not set via role, but here we + // need to explicitly check + if role != nil && te.Type != logical.TokenTypeBatch { if role.ExplicitMaxTTL != 0 { switch { case explicitMaxTTLToUse == 0: @@ -2345,6 +2548,7 @@ func (ts *TokenStore) handleCreateCommon(ctx context.Context, req *logical.Reque Period: periodToUse, ExplicitMaxTTL: explicitMaxTTLToUse, CreationPath: te.Path, + TokenType: te.Type, } for _, p := range te.Policies { @@ -2364,34 +2568,7 @@ func (ts *TokenStore) handleCreateCommon(ctx context.Context, req *logical.Reque // in a way that revokes all child tokens. Normally, using sys/revoke/leaseID will revoke // the token and all children anyways, but that is only available when there is a lease. func (ts *TokenStore) handleRevokeSelf(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - te, err := ts.Lookup(ctx, req.ClientToken) - if err != nil { - return nil, err - } - if te == nil { - return nil, nil - } - - tokenNS, err := NamespaceByID(ctx, te.NamespaceID, ts.core) - if err != nil { - return nil, err - } - if tokenNS == nil { - return nil, namespace.ErrNoNamespace - } - - revokeCtx := namespace.ContextWithNamespace(ts.quitContext, tokenNS) - leaseID, err := ts.expiration.CreateOrFetchRevocationLeaseByToken(revokeCtx, te) - if err != nil { - return nil, err - } - - err = ts.expiration.Revoke(revokeCtx, leaseID) - if err != nil { - return nil, err - } - - return nil, nil + return ts.revokeCommon(ctx, req, data, req.ClientToken) } // handleRevokeTree handles the auth/token/revoke/id path for revocation of tokens @@ -2408,6 +2585,20 @@ func (ts *TokenStore) handleRevokeTree(ctx context.Context, req *logical.Request urltoken = true } + if resp, err := ts.revokeCommon(ctx, req, data, id); resp != nil || err != nil { + return resp, err + } + + if urltoken { + resp := &logical.Response{} + resp.AddWarning(`Using a token in the path is unsafe as the token can be logged in many places. Please use POST or PUT with the token passed in via the "token" parameter.`) + return resp, nil + } + + return nil, nil +} + +func (ts *TokenStore) revokeCommon(ctx context.Context, req *logical.Request, data *framework.FieldData, id string) (*logical.Response, error) { te, err := ts.Lookup(ctx, id) if err != nil { return nil, err @@ -2416,6 +2607,10 @@ func (ts *TokenStore) handleRevokeTree(ctx context.Context, req *logical.Request return nil, nil } + if te.Type == logical.TokenTypeBatch { + return logical.ErrorResponse("batch tokens cannot be revoked"), nil + } + tokenNS, err := NamespaceByID(ctx, te.NamespaceID, ts.core) if err != nil { return nil, err @@ -2435,12 +2630,6 @@ func (ts *TokenStore) handleRevokeTree(ctx context.Context, req *logical.Request return nil, err } - if urltoken { - resp := &logical.Response{} - resp.AddWarning(`Using a token in the path is unsafe as the token can be logged in many places. Please use POST or PUT with the token passed in via the "token" parameter.`) - return resp, nil - } - return nil, nil } @@ -2477,6 +2666,10 @@ func (ts *TokenStore) handleRevokeOrphan(ctx context.Context, req *logical.Reque return logical.ErrorResponse("token to revoke not found"), logical.ErrInvalidRequest } + if te.Type == logical.TokenTypeBatch { + return logical.ErrorResponse("batch tokens cannot be revoked"), nil + } + // Revoke and orphan if err := ts.revokeOrphan(ctx, id); err != nil { return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest @@ -2545,6 +2738,7 @@ func (ts *TokenStore) handleLookup(ctx context.Context, req *logical.Request, da "ttl": int64(0), "explicit_max_ttl": int64(out.ExplicitMaxTTL.Seconds()), "entity_id": out.EntityID, + "type": out.Type.String(), }, } @@ -2645,8 +2839,14 @@ func (ts *TokenStore) handleRenew(ctx context.Context, req *logical.Request, dat return logical.ErrorResponse("token not found"), logical.ErrInvalidRequest } + var resp *logical.Response + + if te.Type == logical.TokenTypeBatch { + return logical.ErrorResponse("batch tokens cannot be renewed"), nil + } + // Renew the token and its children - resp, err := ts.expiration.RenewToken(ctx, req, te, increment) + resp, err = ts.expiration.RenewToken(ctx, req, te, increment) if urltoken { resp.AddWarning(`Using a token in the path is unsafe as the token can be logged in many places. Please use POST or PUT with the token passed in via the "token" parameter.`) @@ -2761,6 +2961,7 @@ func (ts *TokenStore) tokenStoreRoleRead(ctx context.Context, req *logical.Reque "orphan": role.Orphan, "path_suffix": role.PathSuffix, "renewable": role.Renewable, + "token_type": role.TokenType.String(), }, } @@ -2898,6 +3099,38 @@ func (ts *TokenStore) tokenStoreRoleCreateUpdate(ctx context.Context, req *logic entry.DisallowedPolicies = strutil.RemoveDuplicates(data.Get("disallowed_policies").([]string), true) } + tokenType := entry.TokenType + tokenTypeRaw, ok := data.GetOk("token_type") + if ok { + tokenTypeStr := tokenTypeRaw.(string) + switch tokenTypeStr { + case "", "service": + tokenType = logical.TokenTypeService + case "batch": + tokenType = logical.TokenTypeBatch + default: + return logical.ErrorResponse(fmt.Sprintf("invalid 'token_type' value %q", tokenTypeStr)), nil + } + } else if req.Operation == logical.CreateOperation { + tokenType = logical.TokenTypeService + } + entry.TokenType = tokenType + + if entry.TokenType == logical.TokenTypeBatch { + if !entry.Orphan { + return logical.ErrorResponse("'token_type' cannot be 'batch' when role is set to generate non-orphan tokens"), nil + } + if entry.Period != 0 { + return logical.ErrorResponse("'token_type' cannot be 'batch' when role is set to generate periodic tokens"), nil + } + if entry.Renewable { + return logical.ErrorResponse("'token_type' cannot be 'batch' when role is set to generate renewable tokens"), nil + } + if entry.ExplicitMaxTTL != 0 { + return logical.ErrorResponse("'token_type' cannot be 'batch' when role is set to generate tokens with an explicit max TTL"), nil + } + } + ns, err := namespace.FromContext(ctx) if err != nil { return nil, err diff --git a/vault/token_store_test.go b/vault/token_store_test.go index 26de3a0051..693a4f02db 100644 --- a/vault/token_store_test.go +++ b/vault/token_store_test.go @@ -13,6 +13,8 @@ import ( "testing" "time" + "github.com/go-test/deep" + "github.com/hashicorp/errwrap" hclog "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/helper/locksutil" @@ -286,10 +288,22 @@ func getBackendConfig(c *Core) *logical.BackendConfig { } } -func testMakeTokenViaBackend(t testing.TB, ts *TokenStore, root, client, ttl string, policy []string) { +func testMakeBatchTokenViaBackend(t testing.TB, ts *TokenStore, root, client, ttl string, policy []string) { + testMakeTokenViaBackend(t, ts, root, client, ttl, policy, true) +} + +func testMakeServiceTokenViaBackend(t testing.TB, ts *TokenStore, root, client, ttl string, policy []string) { + testMakeTokenViaBackend(t, ts, root, client, ttl, policy, false) +} + +func testMakeTokenViaBackend(t testing.TB, ts *TokenStore, root, client, ttl string, policy []string, batch bool) { req := logical.TestRequest(t, logical.UpdateOperation, "create") req.ClientToken = root - req.Data["id"] = client + if batch { + req.Data["type"] = "batch" + } else { + req.Data["id"] = client + } req.Data["policies"] = policy req.Data["ttl"] = ttl resp := testMakeTokenViaRequest(t, ts, req) @@ -301,21 +315,26 @@ func testMakeTokenViaBackend(t testing.TB, ts *TokenStore, root, client, ttl str func testMakeTokenViaRequest(t testing.TB, ts *TokenStore, req *logical.Request) *logical.Response { resp, err := ts.HandleRequest(namespace.TestContext(), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err: %v\nresp: %#v", err, resp) + if err != nil { + t.Fatal(err) } - if resp == nil { t.Fatalf("got nil token from create call") } + // Let the caller handle the error + if resp.IsError() { + return resp + } te := &logical.TokenEntry{ Path: resp.Auth.CreationPath, NamespaceID: namespace.RootNamespaceID, } - if err := ts.expiration.RegisterAuth(namespace.TestContext(), te, resp.Auth); err != nil { - t.Fatal(err) + if resp.Auth.TokenType != logical.TokenTypeBatch { + if err := ts.expiration.RegisterAuth(namespace.TestContext(), te, resp.Auth); err != nil { + t.Fatal(err) + } } te, err = ts.Lookup(namespace.TestContext(), resp.Auth.ClientToken) @@ -333,9 +352,15 @@ func testMakeTokenDirectly(t testing.TB, ts *TokenStore, te *logical.TokenEntry) if te.NamespaceID == "" { te.NamespaceID = namespace.RootNamespaceID } + if te.CreationTime == 0 { + te.CreationTime = time.Now().Unix() + } if err := ts.create(namespace.RootContext(nil), te); err != nil { t.Fatal(err) } + if te.Type == logical.TokenTypeDefault { + te.Type = logical.TokenTypeService + } auth := &logical.Auth{ NumUses: te.NumUses, DisplayName: te.DisplayName, @@ -351,16 +376,37 @@ func testMakeTokenDirectly(t testing.TB, ts *TokenStore, te *logical.TokenEntry) Period: te.Period, ExplicitMaxTTL: te.ExplicitMaxTTL, CreationPath: te.Path, + TokenType: te.Type, } - if err := ts.expiration.RegisterAuth(namespace.TestContext(), te, auth); err != nil { - t.Fatal(err) + err := ts.expiration.RegisterAuth(namespace.TestContext(), te, auth) + switch err { + case nil: + if te.Type == logical.TokenTypeBatch { + t.Fatal("expected error from trying to register auth with batch token") + } + default: + if te.Type != logical.TokenTypeBatch { + t.Fatal(err) + } } } -func testMakeTokenViaCore(t testing.TB, c *Core, root, client, ttl string, policy []string) { +func testMakeServiceTokenViaCore(t testing.TB, c *Core, root, client, ttl string, policy []string) { + testMakeTokenViaCore(t, c, root, client, ttl, policy, false, nil) +} + +func testMakeBatchTokenViaCore(t testing.TB, c *Core, root, client, ttl string, policy []string) { + testMakeTokenViaCore(t, c, root, client, ttl, policy, true, nil) +} + +func testMakeTokenViaCore(t testing.TB, c *Core, root, client, ttl string, policy []string, batch bool, outAuth *logical.Auth) { req := logical.TestRequest(t, logical.UpdateOperation, "auth/token/create") req.ClientToken = root - req.Data["id"] = client + if batch { + req.Data["type"] = "batch" + } else { + req.Data["id"] = client + } req.Data["policies"] = policy req.Data["ttl"] = ttl @@ -368,8 +414,13 @@ func testMakeTokenViaCore(t testing.TB, c *Core, root, client, ttl string, polic if err != nil || (resp != nil && resp.IsError()) { t.Fatalf("err: %v\nresp: %#v", err, resp) } - if resp.Auth.ClientToken != client { - t.Fatalf("bad: %#v", *resp) + if !batch { + if resp.Auth.ClientToken != client { + t.Fatalf("bad: %#v", *resp) + } + } + if outAuth != nil && resp != nil && resp.Auth != nil { + *outAuth = *resp.Auth } } @@ -404,13 +455,27 @@ func TestTokenStore_AccessorIndex(t *testing.T) { if aEntry.TokenID != ent.ID { t.Fatalf("bad: got\n%s\nexpected\n%s\n", aEntry.TokenID, ent.ID) } + + // Make sure a batch token doesn't get an accessor + ent.Type = logical.TokenTypeBatch + testMakeTokenDirectly(t, ts, ent) + + out, err = ts.Lookup(namespace.TestContext(), ent.ID) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Ensure that accessor is created + if out == nil || out.Accessor != "" { + t.Fatalf("bad: %#v", out) + } } func TestTokenStore_HandleRequest_LookupAccessor(t *testing.T) { c, _, root := TestCoreUnsealed(t) ts := c.tokenStore - testMakeTokenViaBackend(t, ts, root, "tokenid", "", []string{"foo"}) + testMakeServiceTokenViaBackend(t, ts, root, "tokenid", "", []string{"foo"}) out, err := ts.Lookup(namespace.TestContext(), "tokenid") if err != nil { t.Fatalf("err: %s", err) @@ -448,7 +513,7 @@ func TestTokenStore_HandleRequest_ListAccessors(t *testing.T) { testKeys := []string{"token1", "token2", "token3", "token4"} for _, key := range testKeys { - testMakeTokenViaBackend(t, ts, root, key, "", []string{"foo"}) + testMakeServiceTokenViaBackend(t, ts, root, key, "", []string{"foo"}) } // Revoke root to make the number of accessors match @@ -534,7 +599,7 @@ func TestTokenStore_HandleRequest_RevokeAccessor(t *testing.T) { rootToken, err := ts.rootToken(namespace.TestContext()) root := rootToken.ID - testMakeTokenViaBackend(t, ts, root, "tokenid", "", []string{"foo"}) + testMakeServiceTokenViaBackend(t, ts, root, "tokenid", "", []string{"foo"}) auth := &logical.Auth{ ClientToken: "tokenid", @@ -587,7 +652,7 @@ func TestTokenStore_HandleRequest_RevokeAccessor(t *testing.T) { } // Now test without registering the token through the expiration manager - testMakeTokenViaBackend(t, ts, root, "tokenid", "", []string{"foo"}) + testMakeServiceTokenViaBackend(t, ts, root, "tokenid", "", []string{"foo"}) out, err = ts.Lookup(namespace.TestContext(), "tokenid") if err != nil { t.Fatalf("err: %s", err) @@ -639,6 +704,27 @@ func TestTokenStore_RootToken(t *testing.T) { } } +func TestTokenStore_NoRootBatch(t *testing.T) { + c, _, root := TestCoreUnsealed(t) + + req := logical.TestRequest(t, logical.UpdateOperation, "auth/token/create") + req.ClientToken = root + req.Data["type"] = "batch" + req.Data["policies"] = "root" + req.Data["ttl"] = "5m" + + resp, err := c.HandleRequest(namespace.TestContext(), req) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("expected response") + } + if !resp.IsError() { + t.Fatalf("expected error, got %#v", *resp) + } +} + func TestTokenStore_CreateLookup(t *testing.T) { c, _, _ := TestCoreUnsealed(t) ts := c.tokenStore @@ -930,6 +1016,7 @@ func TestTokenStore_Revoke_Leases(t *testing.T) { Path: "noop/foo", ClientToken: ent.ID, } + req.SetTokenEntry(ent) resp := &logical.Response{ Secret: &logical.Secret{ LeaseOptions: logical.LeaseOptions{ @@ -1226,6 +1313,19 @@ func TestTokenStore_HandleRequest_NonAssignable(t *testing.T) { if !resp.IsError() { t.Fatalf("expected error; response is %#v", *resp) } + + // Batch tokens too + req.Data["type"] = "batch" + resp, err = ts.HandleRequest(namespace.TestContext(), req) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("got a nil response") + } + if !resp.IsError() { + t.Fatalf("expected error; response is %#v", *resp) + } } func TestTokenStore_HandleRequest_CreateToken_DisplayName(t *testing.T) { @@ -1250,6 +1350,7 @@ func TestTokenStore_HandleRequest_CreateToken_DisplayName(t *testing.T) { Path: "auth/token/create", DisplayName: "token-foo-bar-baz", TTL: 0, + Type: logical.TokenTypeService, } out, err := ts.Lookup(namespace.TestContext(), resp.Auth.ClientToken) if err != nil { @@ -1269,7 +1370,15 @@ func TestTokenStore_HandleRequest_CreateToken_NumUses(t *testing.T) { req.ClientToken = root req.Data["num_uses"] = "1" + // Make sure batch tokens can't do limited use counts + req.Data["type"] = "batch" resp, err := ts.HandleRequest(namespace.TestContext(), req) + if resp == nil || !resp.IsError() { + t.Fatalf("expected error: resp: %#v", resp) + } + + delete(req.Data, "type") + resp, err = ts.HandleRequest(namespace.TestContext(), req) if err != nil || (resp != nil && resp.IsError()) { t.Fatalf("err: %v\nresp: %#v", err, resp) } @@ -1284,6 +1393,7 @@ func TestTokenStore_HandleRequest_CreateToken_NumUses(t *testing.T) { DisplayName: "token", NumUses: 1, TTL: 0, + Type: logical.TokenTypeService, } out, err := ts.Lookup(namespace.TestContext(), resp.Auth.ClientToken) if err != nil { @@ -1337,7 +1447,15 @@ func TestTokenStore_HandleRequest_CreateToken_NoPolicy(t *testing.T) { req := logical.TestRequest(t, logical.UpdateOperation, "create") req.ClientToken = root + // Make sure batch tokens won't automatically assign root + req.Data["type"] = "batch" resp, err := ts.HandleRequest(namespace.TestContext(), req) + if resp == nil || !resp.IsError() { + t.Fatalf("expected error: resp: %#v", resp) + } + + delete(req.Data, "type") + resp, err = ts.HandleRequest(namespace.TestContext(), req) if err != nil || (resp != nil && resp.IsError()) { t.Fatalf("err: %v\nresp: %#v", err, resp) } @@ -1351,6 +1469,7 @@ func TestTokenStore_HandleRequest_CreateToken_NoPolicy(t *testing.T) { Path: "auth/token/create", DisplayName: "token", TTL: 0, + Type: logical.TokenTypeService, } out, err := ts.Lookup(namespace.TestContext(), resp.Auth.ClientToken) if err != nil { @@ -1408,16 +1527,26 @@ func TestTokenStore_HandleRequest_CreateToken_RootID(t *testing.T) { if err != nil || (resp != nil && resp.IsError()) { t.Fatalf("err: %v\nresp: %#v", err, resp) } - if resp.Auth.ClientToken != "foobar" { t.Fatalf("bad: %#v", resp) } + + // Retry with batch; batch should not actually accept a custom ID + req.Data["type"] = "batch" + resp, err = ts.HandleRequest(namespace.TestContext(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err: %v\nresp: %#v", err, resp) + } + out, _ := ts.Lookup(namespace.TestContext(), resp.Auth.ClientToken) + if out.ID == "foobar" { + t.Fatalf("bad: %#v", out) + } } func TestTokenStore_HandleRequest_CreateToken_NonRootID(t *testing.T) { c, _, root := TestCoreUnsealed(t) ts := c.tokenStore - testMakeTokenViaBackend(t, ts, root, "client", "", []string{"foo"}) + testMakeServiceTokenViaBackend(t, ts, root, "client", "", []string{"foo"}) req := logical.TestRequest(t, logical.UpdateOperation, "create") req.ClientToken = "client" @@ -1431,12 +1560,22 @@ func TestTokenStore_HandleRequest_CreateToken_NonRootID(t *testing.T) { if resp.Data["error"] != "root or sudo privileges required to specify token id" { t.Fatalf("bad: %#v", resp) } + + // Retry with batch + req.Data["type"] = "batch" + resp, err = ts.HandleRequest(namespace.TestContext(), req) + if err != logical.ErrInvalidRequest { + t.Fatalf("err: %v resp: %#v", err, resp) + } + if resp.Data["error"] != "root or sudo privileges required to specify token id" { + t.Fatalf("bad: %#v", resp) + } } func TestTokenStore_HandleRequest_CreateToken_NonRoot_Subset(t *testing.T) { c, _, root := TestCoreUnsealed(t) ts := c.tokenStore - testMakeTokenViaBackend(t, ts, root, "client", "", []string{"foo", "bar"}) + testMakeServiceTokenViaBackend(t, ts, root, "client", "", []string{"foo", "bar"}) req := logical.TestRequest(t, logical.UpdateOperation, "create") req.ClientToken = "client" @@ -1449,12 +1588,28 @@ func TestTokenStore_HandleRequest_CreateToken_NonRoot_Subset(t *testing.T) { if resp.Auth.ClientToken == "" { t.Fatalf("bad: %#v", resp) } + + ent := &logical.TokenEntry{ + NamespaceID: namespace.RootNamespaceID, + Path: "test", + Policies: []string{"foo", "bar"}, + TTL: time.Hour, + } + testMakeTokenDirectly(t, ts, ent) + req.ClientToken = ent.ID + resp, err = ts.HandleRequest(namespace.TestContext(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err: %v\nresp: %#v", err, resp) + } + if resp.Auth.ClientToken == "" { + t.Fatalf("bad: %#v", resp) + } } func TestTokenStore_HandleRequest_CreateToken_NonRoot_InvalidSubset(t *testing.T) { c, _, root := TestCoreUnsealed(t) ts := c.tokenStore - testMakeTokenViaBackend(t, ts, root, "client", "", []string{"foo", "bar"}) + testMakeServiceTokenViaBackend(t, ts, root, "client", "", []string{"foo", "bar"}) req := logical.TestRequest(t, logical.UpdateOperation, "create") req.ClientToken = "client" @@ -1480,7 +1635,7 @@ func TestTokenStore_HandleRequest_CreateToken_NonRoot_RootChild(t *testing.T) { t.Fatal(err) } - testMakeTokenViaBackend(t, ts, root, "sudoClient", "", []string{"test1"}) + testMakeServiceTokenViaBackend(t, ts, root, "sudoClient", "", []string{"test1"}) req := logical.TestRequest(t, logical.UpdateOperation, "create") req.ClientToken = "sudoClient" @@ -1555,7 +1710,7 @@ func TestTokenStore_HandleRequest_CreateToken_Root_RootChild(t *testing.T) { func TestTokenStore_HandleRequest_CreateToken_NonRoot_NoParent(t *testing.T) { c, _, root := TestCoreUnsealed(t) ts := c.tokenStore - testMakeTokenViaBackend(t, ts, root, "client", "", []string{"foo"}) + testMakeServiceTokenViaBackend(t, ts, root, "client", "", []string{"foo"}) req := logical.TestRequest(t, logical.UpdateOperation, "create") req.ClientToken = "client" @@ -1633,6 +1788,18 @@ func TestTokenStore_HandleRequest_CreateToken_Metadata(t *testing.T) { if !reflect.DeepEqual(out.Meta, meta) { t.Fatalf("bad: expected:%#v\nactual:%#v", meta, out.Meta) } + + // Test with batch tokens + req.Data["type"] = "batch" + resp = testMakeTokenViaRequest(t, ts, req) + if resp.Auth.ClientToken == "" { + t.Fatalf("bad: %#v", resp) + } + + out, _ = ts.Lookup(namespace.TestContext(), resp.Auth.ClientToken) + if !reflect.DeepEqual(out.Meta, meta) { + t.Fatalf("bad: expected:%#v\nactual:%#v", meta, out.Meta) + } } func TestTokenStore_HandleRequest_CreateToken_Lease(t *testing.T) { @@ -1675,6 +1842,19 @@ func TestTokenStore_HandleRequest_CreateToken_TTL(t *testing.T) { if !resp.Auth.Renewable { t.Fatalf("bad: %#v", resp) } + + // Test batch tokens + req.Data["type"] = "batch" + resp = testMakeTokenViaRequest(t, ts, req) + if resp.Auth.ClientToken == "" { + t.Fatalf("bad: %#v", resp) + } + if resp.Auth.TTL != time.Hour { + t.Fatalf("bad: %#v", resp) + } + if resp.Auth.Renewable { + t.Fatalf("bad: %#v", resp) + } } func TestTokenStore_HandleRequest_Revoke(t *testing.T) { @@ -1684,7 +1864,7 @@ func TestTokenStore_HandleRequest_Revoke(t *testing.T) { rootToken, err := ts.rootToken(namespace.TestContext()) root := rootToken.ID - testMakeTokenViaBackend(t, ts, root, "child", "", []string{"root", "foo"}) + testMakeServiceTokenViaBackend(t, ts, root, "child", "", []string{"root", "foo"}) te, err := ts.Lookup(namespace.TestContext(), "child") if err != nil { @@ -1706,7 +1886,7 @@ func TestTokenStore_HandleRequest_Revoke(t *testing.T) { t.Fatalf("err: %v", err) } - testMakeTokenViaBackend(t, ts, "child", "sub-child", "", []string{"foo"}) + testMakeServiceTokenViaBackend(t, ts, "child", "sub-child", "", []string{"foo"}) te, err = ts.Lookup(namespace.TestContext(), "sub-child") if err != nil { @@ -1760,8 +1940,8 @@ func TestTokenStore_HandleRequest_Revoke(t *testing.T) { } // Now test without registering the tokens through the expiration manager - testMakeTokenViaBackend(t, ts, root, "child", "", []string{"root", "foo"}) - testMakeTokenViaBackend(t, ts, "child", "sub-child", "", []string{"foo"}) + testMakeServiceTokenViaBackend(t, ts, root, "child", "", []string{"root", "foo"}) + testMakeServiceTokenViaBackend(t, ts, "child", "sub-child", "", []string{"foo"}) req = logical.TestRequest(t, logical.UpdateOperation, "revoke") req.Data = map[string]interface{}{ @@ -1798,8 +1978,8 @@ func TestTokenStore_HandleRequest_Revoke(t *testing.T) { func TestTokenStore_HandleRequest_RevokeOrphan(t *testing.T) { c, _, root := TestCoreUnsealed(t) ts := c.tokenStore - testMakeTokenViaBackend(t, ts, root, "child", "", []string{"root", "foo"}) - testMakeTokenViaBackend(t, ts, "child", "sub-child", "", []string{"foo"}) + testMakeServiceTokenViaBackend(t, ts, root, "child", "", []string{"root", "foo"}) + testMakeServiceTokenViaBackend(t, ts, "child", "sub-child", "", []string{"foo"}) req := logical.TestRequest(t, logical.UpdateOperation, "revoke-orphan") req.Data = map[string]interface{}{ @@ -1850,7 +2030,7 @@ func TestTokenStore_HandleRequest_RevokeOrphan(t *testing.T) { func TestTokenStore_HandleRequest_RevokeOrphan_NonRoot(t *testing.T) { c, _, root := TestCoreUnsealed(t) ts := c.tokenStore - testMakeTokenViaBackend(t, ts, root, "child", "", []string{"foo"}) + testMakeServiceTokenViaBackend(t, ts, root, "child", "", []string{"foo"}) out, err := ts.Lookup(namespace.TestContext(), "child") if err != nil { @@ -1883,6 +2063,11 @@ func TestTokenStore_HandleRequest_RevokeOrphan_NonRoot(t *testing.T) { } func TestTokenStore_HandleRequest_Lookup(t *testing.T) { + testTokenStore_HandleRequest_Lookup(t, false) + testTokenStore_HandleRequest_Lookup(t, true) +} + +func testTokenStore_HandleRequest_Lookup(t *testing.T, batch bool) { c, _, root := TestCoreUnsealed(t) ts := c.tokenStore req := logical.TestRequest(t, logical.UpdateOperation, "lookup") @@ -1911,6 +2096,7 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) { "explicit_max_ttl": int64(0), "expire_time": nil, "entity_id": "", + "type": "service", } if resp.Data["creation_time"].(int64) == 0 { @@ -1922,63 +2108,20 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) { t.Fatalf("bad: expected:%#v\nactual:%#v", exp, resp.Data) } - testMakeTokenViaCore(t, c, root, "client", "3600s", []string{"foo"}) + outAuth := new(logical.Auth) + testMakeTokenViaCore(t, c, root, "client", "3600s", []string{"foo"}, batch, outAuth) - // Test via GET - req = logical.TestRequest(t, logical.UpdateOperation, "lookup") - req.Data = map[string]interface{}{ - "token": "client", - } - resp, err = ts.HandleRequest(namespace.TestContext(), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err: %v\nresp: %#v", err, resp) - } - if resp == nil { - t.Fatalf("bad: %#v", resp) - } - - exp = map[string]interface{}{ - "id": "client", - "accessor": resp.Data["accessor"], - "policies": []string{"default", "foo"}, - "path": "auth/token/create", - "meta": map[string]string(nil), - "display_name": "token", - "orphan": false, - "num_uses": 0, - "creation_ttl": int64(3600), - "ttl": int64(3600), - "explicit_max_ttl": int64(0), - "renewable": true, - "entity_id": "", - } - - if resp.Data["creation_time"].(int64) == 0 { - t.Fatalf("creation time was zero") - } - delete(resp.Data, "creation_time") - if resp.Data["issue_time"].(time.Time).IsZero() { - t.Fatal("issue time is default time") - } - delete(resp.Data, "issue_time") - if resp.Data["expire_time"].(time.Time).IsZero() { - t.Fatal("expire time is default time") - } - delete(resp.Data, "expire_time") - - // Depending on timing of the test this may have ticked down, so accept 3599 - if resp.Data["ttl"].(int64) == 3599 { - resp.Data["ttl"] = int64(3600) - } - - if !reflect.DeepEqual(resp.Data, exp) { - t.Fatalf("bad: expected:%#v\nactual:%#v", exp, resp.Data) + tokenType := "service" + expID := "client" + if batch { + tokenType = "batch" + expID = outAuth.ClientToken } // Test via POST req = logical.TestRequest(t, logical.UpdateOperation, "lookup") req.Data = map[string]interface{}{ - "token": "client", + "token": expID, } resp, err = ts.HandleRequest(namespace.TestContext(), req) if err != nil || (resp != nil && resp.IsError()) { @@ -1989,7 +2132,7 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) { } exp = map[string]interface{}{ - "id": "client", + "id": expID, "accessor": resp.Data["accessor"], "policies": []string{"default", "foo"}, "path": "auth/token/create", @@ -2000,8 +2143,9 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) { "creation_ttl": int64(3600), "ttl": int64(3600), "explicit_max_ttl": int64(0), - "renewable": true, + "renewable": !batch, "entity_id": "", + "type": tokenType, } if resp.Data["creation_time"].(int64) == 0 { @@ -2022,16 +2166,80 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) { resp.Data["ttl"] = int64(3600) } - if !reflect.DeepEqual(resp.Data, exp) { - t.Fatalf("bad: expected:%#v\nactual:%#v", exp, resp.Data) + if diff := deep.Equal(resp.Data, exp); diff != nil { + t.Fatal(diff) + } + + // Test via POST + req = logical.TestRequest(t, logical.UpdateOperation, "lookup") + req.Data = map[string]interface{}{ + "token": expID, + } + resp, err = ts.HandleRequest(namespace.TestContext(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err: %v\nresp: %#v", err, resp) + } + if resp == nil { + t.Fatalf("bad: %#v", resp) + } + + exp = map[string]interface{}{ + "id": expID, + "accessor": resp.Data["accessor"], + "policies": []string{"default", "foo"}, + "path": "auth/token/create", + "meta": map[string]string(nil), + "display_name": "token", + "orphan": false, + "num_uses": 0, + "creation_ttl": int64(3600), + "ttl": int64(3600), + "explicit_max_ttl": int64(0), + "renewable": !batch, + "entity_id": "", + "type": tokenType, + } + + if resp.Data["creation_time"].(int64) == 0 { + t.Fatalf("creation time was zero") + } + delete(resp.Data, "creation_time") + if resp.Data["issue_time"].(time.Time).IsZero() { + t.Fatal("issue time is default time") + } + delete(resp.Data, "issue_time") + if resp.Data["expire_time"].(time.Time).IsZero() { + t.Fatal("expire time is default time") + } + delete(resp.Data, "expire_time") + + // Depending on timing of the test this may have ticked down, so accept 3599 + if resp.Data["ttl"].(int64) == 3599 { + resp.Data["ttl"] = int64(3600) + } + + if diff := deep.Equal(resp.Data, exp); diff != nil { + t.Fatal(diff) } // Test last_renewal_time functionality req = logical.TestRequest(t, logical.UpdateOperation, "renew") req.Data = map[string]interface{}{ - "token": "client", + "token": expID, } resp, err = ts.HandleRequest(namespace.TestContext(), req) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatalf("bad: %#v", resp) + } + if batch && !resp.IsError() || !batch && resp.IsError() { + t.Fatalf("err: %v\nresp: %#v", err, resp) + } + + req.Path = "lookup" + resp, err = ts.HandleRequest(namespace.TestContext(), req) if err != nil || (resp != nil && resp.IsError()) { t.Fatalf("err: %v\nresp: %#v", err, resp) } @@ -2039,27 +2247,19 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) { t.Fatalf("bad: %#v", resp) } - req = logical.TestRequest(t, logical.UpdateOperation, "lookup") - req.Data = map[string]interface{}{ - "token": "client", - } - resp, err = ts.HandleRequest(namespace.TestContext(), req) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err: %v\nresp: %#v", err, resp) - } - if resp == nil { - t.Fatalf("bad: %#v", resp) - } - - if resp.Data["last_renewal_time"].(int64) == 0 { - t.Fatalf("last_renewal_time was zero") + if !batch { + if resp.Data["last_renewal_time"].(int64) == 0 { + t.Fatalf("last_renewal_time was zero") + } + } else if _, ok := resp.Data["last_renewal_time"]; ok { + t.Fatal("expected zero last renewal time") } } func TestTokenStore_HandleRequest_LookupSelf(t *testing.T) { c, _, root := TestCoreUnsealed(t) ts := c.tokenStore - testMakeTokenViaCore(t, c, root, "client", "3600s", []string{"foo"}) + testMakeServiceTokenViaCore(t, c, root, "client", "3600s", []string{"foo"}) req := logical.TestRequest(t, logical.ReadOperation, "lookup-self") req.ClientToken = "client" @@ -2085,6 +2285,7 @@ func TestTokenStore_HandleRequest_LookupSelf(t *testing.T) { "ttl": int64(3600), "explicit_max_ttl": int64(0), "entity_id": "", + "type": "service", } if resp.Data["creation_time"].(int64) == 0 { @@ -2257,6 +2458,7 @@ func TestTokenStore_RoleCRUD(t *testing.T) { "path_suffix": "happenin", "explicit_max_ttl": int64(0), "renewable": true, + "token_type": "service", } if !reflect.DeepEqual(expected, resp.Data) { @@ -2302,6 +2504,7 @@ func TestTokenStore_RoleCRUD(t *testing.T) { "path_suffix": "happenin", "explicit_max_ttl": int64(0), "renewable": false, + "token_type": "service", } if !reflect.DeepEqual(expected, resp.Data) { @@ -2339,6 +2542,7 @@ func TestTokenStore_RoleCRUD(t *testing.T) { "path_suffix": "happenin", "period": int64(0), "renewable": false, + "token_type": "service", } if !reflect.DeepEqual(expected, resp.Data) { @@ -2675,7 +2879,7 @@ func TestTokenStore_RolePathSuffix(t *testing.T) { c, _, root := TestCoreUnsealed(t) ts := c.tokenStore - req := logical.TestRequest(t, logical.UpdateOperation, "roles/test") + req := logical.TestRequest(t, logical.CreateOperation, "roles/test") req.ClientToken = root req.Data = map[string]interface{}{ "path_suffix": "happenin", @@ -2690,6 +2894,7 @@ func TestTokenStore_RolePathSuffix(t *testing.T) { } req.Path = "create/test" + req.Operation = logical.UpdateOperation resp, err = ts.HandleRequest(namespace.TestContext(), req) if err != nil || (resp != nil && resp.IsError()) { t.Fatalf("err: %v\nresp: %#v", err, resp) @@ -4138,6 +4343,8 @@ func TestTokenStore_TidyLeaseRevocation(t *testing.T) { Path: "prod/aws/foo", ClientToken: tut, } + req.SetTokenEntry(testTokenEntry) + resp = &logical.Response{ Secret: &logical.Secret{ LeaseOptions: logical.LeaseOptions{ @@ -4224,3 +4431,101 @@ func TestTokenStore_TidyLeaseRevocation(t *testing.T) { t.Fatal("found leases") } } + +func TestTokenStore_Batch_CannotCreateChildren(t *testing.T) { + var resp *logical.Response + + core, _, root := TestCoreUnsealed(t) + ts := core.tokenStore + + req := &logical.Request{ + Path: "create", + ClientToken: root, + Operation: logical.UpdateOperation, + Data: map[string]interface{}{ + "policies": []string{"policy1"}, + "type": "batch", + }, + } + resp = testMakeTokenViaRequest(t, ts, req) + if !reflect.DeepEqual(resp.Auth.Policies, []string{"default", "policy1"}) { + t.Fatalf("bad: policies: expected: [policy, default]; actual: %s", resp.Auth.Policies) + } + + req.ClientToken = resp.Auth.ClientToken + resp = testMakeTokenViaRequest(t, ts, req) + if !resp.IsError() { + t.Fatalf("bad: expected error, got %#v", *resp) + } +} + +func TestTokenStore_Batch_CannotRevoke(t *testing.T) { + var resp *logical.Response + var err error + + core, _, root := TestCoreUnsealed(t) + ts := core.tokenStore + + req := &logical.Request{ + Path: "create", + ClientToken: root, + Operation: logical.UpdateOperation, + Data: map[string]interface{}{ + "policies": []string{"policy1"}, + "type": "batch", + }, + } + resp = testMakeTokenViaRequest(t, ts, req) + if !reflect.DeepEqual(resp.Auth.Policies, []string{"default", "policy1"}) { + t.Fatalf("bad: policies: expected: [policy, default]; actual: %s", resp.Auth.Policies) + } + + req.Path = "revoke" + req.Data["token"] = resp.Auth.ClientToken + resp, err = ts.HandleRequest(namespace.TestContext(), req) + if err != nil { + t.Fatal(err) + } + if !resp.IsError() { + t.Fatalf("bad: expected error, got %#v", *resp) + } +} + +func TestTokenStore_Batch_NoCubbyhole(t *testing.T) { + var resp *logical.Response + var err error + + core, _, root := TestCoreUnsealed(t) + ts := core.tokenStore + + req := &logical.Request{ + Path: "create", + ClientToken: root, + Operation: logical.UpdateOperation, + Data: map[string]interface{}{ + "policies": []string{"policy1"}, + "type": "batch", + }, + } + resp = testMakeTokenViaRequest(t, ts, req) + if !reflect.DeepEqual(resp.Auth.Policies, []string{"default", "policy1"}) { + t.Fatalf("bad: policies: expected: [policy, default]; actual: %s", resp.Auth.Policies) + } + + te, err := ts.Lookup(namespace.TestContext(), resp.Auth.ClientToken) + if err != nil { + t.Fatal(err) + } + + req.Path = "cubbyhole/foo" + req.Operation = logical.CreateOperation + req.ClientToken = te.ID + req.SetTokenEntry(te) + resp, err = core.HandleRequest(namespace.TestContext(), req) + if err != nil && !errwrap.Contains(err, logical.ErrInvalidRequest.Error()) { + t.Fatal(err) + } + if !resp.IsError() { + t.Fatalf("bad: expected error, got %#v", *resp) + } +} diff --git a/website/source/docs/concepts/dev-server.html.md b/website/source/docs/concepts/dev-server.html.md index 262b9b0a88..84c265c84f 100644 --- a/website/source/docs/concepts/dev-server.html.md +++ b/website/source/docs/concepts/dev-server.html.md @@ -21,7 +21,8 @@ in-memory). It is only made for development or experimentation. ## Properties -The properties of the dev server: +The properties of the dev server (some can be overridden with command line +flags or by specifying a configuration file): * **Initialized and unsealed** - The server will be automatically initialized and unsealed. You don't need to use `vault operator unseal`. It is ready diff --git a/website/source/docs/concepts/index.html.md b/website/source/docs/concepts/index.html.md index 37063f5bd2..849479a56c 100644 --- a/website/source/docs/concepts/index.html.md +++ b/website/source/docs/concepts/index.html.md @@ -1,16 +1,15 @@ --- layout: "docs" -page_title: "Basic Concepts" +page_title: "Concepts" sidebar_current: "docs-concepts" description: |- - Basic concepts that are important to understand for Vault usage. + Concepts that are important to understand for Vault usage. --- -# Basic Concepts +# Concepts -This section covers some high level basic concepts that are important -to understand for day to day Vault usage and operation. Every page in -this section is recommended reading for anyone consuming or operating -Vault. +This section covers some concepts that are important to understand for day to +day Vault usage and operation. Every page in this section is recommended +reading for anyone consuming or operating Vault. Please use the navigation to the left to learn more about a topic. diff --git a/website/source/docs/concepts/lease.html.md b/website/source/docs/concepts/lease.html.md index 25ea3caa2f..be4494f6cc 100644 --- a/website/source/docs/concepts/lease.html.md +++ b/website/source/docs/concepts/lease.html.md @@ -8,12 +8,12 @@ description: |- # Lease, Renew, and Revoke -With every dynamic secret and authentication token, Vault creates a _lease_: -metadata containing information such as a time duration, renewability, and -more. Vault promises that the data will be valid for the given duration, or -Time To Live (TTL). Once the lease is expired, Vault can automatically revoke -the data, and the consumer of the secret can no longer be certain that it is -valid. +With every dynamic secret and `service` type authentication token, Vault +creates a _lease_: metadata containing information such as a time duration, +renewability, and more. Vault promises that the data will be valid for the +given duration, or Time To Live (TTL). Once the lease is expired, Vault can +automatically revoke the data, and the consumer of the secret can no longer be +certain that it is valid. The benefit should be clear: consumers of secrets need to check in with Vault routinely to either renew the lease (if allowed) or request a @@ -35,7 +35,8 @@ or automatically by Vault. When a lease is expired, Vault will automatically revoke that lease. **Note**: The [Key/Value Backend](/docs/secrets/kv/index.html) which stores -arbitrary secrets does not issue leases. +arbitrary secrets does not issue leases although it will sometimes return a +lease duration; see the documentation for more information. ## Lease IDs @@ -61,14 +62,6 @@ so often. As a result, the return value of renewals should be carefully inspected to determine what the new lease is. -**Note**: Prior to version 0.3, Vault documentation and help text did not -distinguish sufficiently between a _lease_ and a _lease duration_. Starting -with version 0.3, Vault will start migrating to the term _ttl_ to describe -lease durations, at least for user-facing text. As _lease duration_ is still a -legitimate (but more verbose) description, there are currently no plans to -change the JSON key used in responses, in order to retain -backwards-compatibility. - ## Prefix-based Revocation In addition to revoking a single secret, operators with proper access control diff --git a/website/source/docs/concepts/tokens.html.md b/website/source/docs/concepts/tokens.html.md index e5bd9b445e..78e6001b88 100644 --- a/website/source/docs/concepts/tokens.html.md +++ b/website/source/docs/concepts/tokens.html.md @@ -31,9 +31,15 @@ renewal time, and more. Read on for a deeper dive into token concepts. -## Token Concepts +## Token Types -### The Token Store +As of Vault 1.0, there are two types of tokens: `service` tokens and `batch` +tokens. A section near the bottom of this page contains detailed information +about their differences, but it is useful to understand other token concepts +first. The features in the following sections all apply to service tokens, and +their applicability to batch tokens is discussed later. + +## The Token Store Often in documentation or in help channels, the "token store" is referenced. This is the same as the [`token` authentication @@ -42,13 +48,13 @@ backend in that it is responsible for creating and storing tokens, and cannot be disabled. It is also the only auth method that has no login capability -- all actions require existing authenticated tokens. -### Root Tokens +## Root Tokens Root tokens are tokens that have the `root` policy attached to them. Root tokens can do anything in Vault. _Anything_. In addition, they are the only type of token within Vault that can be set to never expire without any renewal -needed. As a result, it is purposefully hard to create root tokens; in fact, as -of version 0.6.1, there are only three ways to create root tokens: +needed. As a result, it is purposefully hard to create root tokens; in fact +there are only three ways to create root tokens: 1. The initial root token generated at `vault operator init` time -- this token has no expiration @@ -70,7 +76,7 @@ whenever a root token is live. This way multiple people can verify as to the tasks performed with the root token, and that the token was revoked immediately after these tasks were completed. -### Token Hierarchies and Orphan Tokens +## Token Hierarchies and Orphan Tokens Normally, when a token holder creates new tokens, these tokens will be created as children of the original token; tokens they create will be children of them; @@ -93,7 +99,7 @@ endpoint, which revokes the given token but rather than revoke the rest of the tree, it instead sets the tokens' immediate children to be orphans. Use with caution! -### Token Accessors +## Token Accessors When tokens are created, a token accessor is also created and returned. This accessor is a value that acts as a reference to a token and can only be used to @@ -121,7 +127,7 @@ dangerous endpoint (since listing all of the accessors means that they can then be used to revoke all tokens), it also provides a way to audit and revoke the currently-active set of tokens. -### Token Time-To-Live, Periodic Tokens, and Explicit Max TTLs +## Token Time-To-Live, Periodic Tokens, and Explicit Max TTLs Every non-root token has a time-to-live (TTL) associated with it, which is a current period of validity since either the token's creation time or last @@ -137,7 +143,7 @@ token is a periodic token (available for creation by `root`/`sudo` users, token store roles, or some auth methods), has an explicit maximum TTL attached, or neither. -#### The General Case +### The General Case In the general case, where there is neither a period nor explicit maximum TTL value set on the token, the token's lifetime since it was created will be @@ -165,7 +171,7 @@ current value and the client may want to reauthenticate and acquire a new token. However, outside of direct operator interaction, Vault will never revoke a token before the returned TTL has expired. -#### Explicit Max TTLs +### Explicit Max TTLs Tokens can have an explicit max TTL set on them. This value becomes a hard limit on the token's lifetime -- no matter what the values in (1), (2), and (3) @@ -173,7 +179,7 @@ from the general case are, the token cannot live past this explicitly-set value. This has an effect even when using periodic tokens to escape the normal TTL mechanism. -#### Periodic Tokens +### Periodic Tokens In some cases, having a token be revoked would be problematic -- for instance, if a long-running service needs to maintain its SQL connection pool over a long @@ -209,9 +215,65 @@ There are a few important things to know when using periodic tokens: * A token with both a period and an explicit max TTL will act like a periodic token but will be revoked when the explicit max TTL is reached -### CIDR-Bound Tokens +## CIDR-Bound Tokens Some tokens are able to be bound to CIDR(s) that restrict the range of client IPs allowed to use them. These affect all tokens except for non-expiring root tokens (those with a TTL of zero). If a root token has an expiration, it also is affected by CIDR-binding. + +## Token Types in Detail + +There are currently two types of tokens. + +### Service Tokens + +Service tokens are what users will generally think of as "normal" Vault tokens. +They support all features, such as renewal, revocation, creating child tokens, +and more. The are correspondingly heavyweight to create and track. + +### Batch Tokens + +Batch tokens are are encrypted blobs that carry enough information for them to +be used for Vault actions, but they require no storage on disk to track them. +As a result they are extremely lightweight and scalable, but lack most of the +flexibility and features of service tokens. + +### Token Type Comparison + +This reference chart describes the difference in behavior between service and +batch tokens. + +| | Service Tokens | Batch Tokens | +|---|---:|---:| +| Can Be Root Tokens | Yes | No | +| Can Create Child Tokens | Yes | No | +| Can be Renewable | Yes | No | +| Can be Periodic | Yes | No | +| Can have Explicit Max TTL | Yes | No (always uses a fixed TTL) | +| Has Accessors | Yes | No | +| Has Cubbyhole | Yes | No | +| Revoked with Parent (if not orphan) | Yes | Stops Working | +| Dynamic Secrets Lease Assignment | Self | Parent (if not orphan) | +| Can be Used Across Performance Replication Clusters | No | Yes (if orphan) | +| Creation Scales with Performance Standby Node Count | No | Yes | +| Cost | Heavyweight; multiple storage writes per token creation | Lightweight; no storage cost for token creation | + +### Service vs. Batch Token Lease Handling + +#### Service Tokens + +Leases created by service tokens (including child tokens' leases) are tracked +along with the service token and revoked when the token expires. + +#### Batch Tokens + +Leases created by batch tokens are constrained to the remaining TTL of the +batch tokens and, if the batch token is not an orphan, are tracked by the +parent. They are revoked when the batch token's TTL expires, or when the batch +token's parent is revoked (at which point the batch token is also denied access +to Vault). + +As a corollary, batch tokens can be used across performance replication +clusters, but only if they are orphan, since non-orphan tokens will not be able +to ensure the validity of the parent token. diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 08d1afeafc..c6db92abbc 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -43,7 +43,7 @@ > - Basic Concepts + Concepts