[WIP] Support custom max Nomad token name length [supersedes https://github.com/hashicorp/vault/pull/4361] (#5117)
* Nomad: updating max token length to 256 * Initial support for supporting custom max token name length for Nomad * simplify/correct tests * document nomad max_token_name_length * removed support for max token length env var. Rename field for clarity * cleanups after removing env var support * move RandomWithPrefix to testhelpers * fix spelling * Remove default 256 value. Use zero as a sentinel value and ignore it * update docs
This commit is contained in:
parent
a8e81ce393
commit
96d8bd4bf7
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
// Factory returns a Nomad backend that satisfies the logical.Backend interface
|
||||
func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
|
||||
b := Backend()
|
||||
if err := b.Setup(ctx, conf); err != nil {
|
||||
|
@ -16,6 +17,7 @@ func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend,
|
|||
return b, nil
|
||||
}
|
||||
|
||||
// Backend returns the configured Nomad backend
|
||||
func Backend() *backend {
|
||||
var b backend
|
||||
b.Backend = &framework.Backend{
|
||||
|
@ -66,5 +68,6 @@ func (b *backend) client(ctx context.Context, s logical.Storage) (*api.Client, e
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"time"
|
||||
|
||||
nomadapi "github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/vault/helper/testhelpers"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/ory/dockertest"
|
||||
|
@ -29,8 +30,8 @@ func prepareTestContainer(t *testing.T) (cleanup func(), retAddress string, noma
|
|||
}
|
||||
|
||||
dockerOptions := &dockertest.RunOptions{
|
||||
Repository: "djenriquez/nomad",
|
||||
Tag: "latest",
|
||||
Repository: "catsby/nomad",
|
||||
Tag: "0.8.4",
|
||||
Cmd: []string{"agent", "-dev"},
|
||||
Env: []string{`NOMAD_LOCAL_CONFIG=bind_addr = "0.0.0.0" acl { enabled = true }`},
|
||||
}
|
||||
|
@ -142,7 +143,8 @@ func TestBackend_config_access(t *testing.T) {
|
|||
}
|
||||
|
||||
expected := map[string]interface{}{
|
||||
"address": connData["address"].(string),
|
||||
"address": connData["address"].(string),
|
||||
"max_token_name_length": 0,
|
||||
}
|
||||
if !reflect.DeepEqual(expected, resp.Data) {
|
||||
t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data)
|
||||
|
@ -300,3 +302,153 @@ func TestBackend_CredsCreateEnvVar(t *testing.T) {
|
|||
t.Fatalf("resp is error: %v", resp.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackend_max_token_name_length(t *testing.T) {
|
||||
config := logical.TestBackendConfig()
|
||||
config.StorageView = &logical.InmemStorage{}
|
||||
b, err := Factory(context.Background(), config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cleanup, connURL, connToken := prepareTestContainer(t)
|
||||
defer cleanup()
|
||||
|
||||
testCases := []struct {
|
||||
title string
|
||||
roleName string
|
||||
tokenLength int
|
||||
}{
|
||||
{
|
||||
title: "Default",
|
||||
},
|
||||
{
|
||||
title: "ConfigOverride",
|
||||
tokenLength: 64,
|
||||
},
|
||||
{
|
||||
title: "ConfigOverride-LongName",
|
||||
roleName: "testlongerrolenametoexceed64charsdddddddddddddddddddddddd",
|
||||
tokenLength: 64,
|
||||
},
|
||||
{
|
||||
title: "Notrim",
|
||||
roleName: "testlongersubrolenametoexceed64charsdddddddddddddddddddddddd",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.title, func(t *testing.T) {
|
||||
// setup config/access
|
||||
connData := map[string]interface{}{
|
||||
"address": connURL,
|
||||
"token": connToken,
|
||||
"max_token_name_length": tc.tokenLength,
|
||||
}
|
||||
expected := map[string]interface{}{
|
||||
"address": connURL,
|
||||
"max_token_name_length": tc.tokenLength,
|
||||
}
|
||||
|
||||
expectedMaxTokenNameLength := maxTokenNameLength
|
||||
if tc.tokenLength != 0 {
|
||||
expectedMaxTokenNameLength = tc.tokenLength
|
||||
}
|
||||
|
||||
confReq := logical.Request{
|
||||
Operation: logical.UpdateOperation,
|
||||
Path: "config/access",
|
||||
Storage: config.StorageView,
|
||||
Data: connData,
|
||||
}
|
||||
|
||||
resp, err := b.HandleRequest(context.Background(), &confReq)
|
||||
if err != nil || (resp != nil && resp.IsError()) || resp != nil {
|
||||
t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err)
|
||||
}
|
||||
confReq.Operation = logical.ReadOperation
|
||||
resp, err = b.HandleRequest(context.Background(), &confReq)
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err)
|
||||
}
|
||||
|
||||
// verify token length is returned in the config/access query
|
||||
if !reflect.DeepEqual(expected, resp.Data) {
|
||||
t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data)
|
||||
}
|
||||
// verify token is not returned
|
||||
if resp.Data["token"] != nil {
|
||||
t.Fatalf("token should not be set in the response")
|
||||
}
|
||||
|
||||
// create a role to create nomad credentials with
|
||||
// Seeds random with current timestamp
|
||||
|
||||
if tc.roleName == "" {
|
||||
tc.roleName = "test"
|
||||
}
|
||||
roleTokenName := testhelpers.RandomWithPrefix(tc.roleName)
|
||||
|
||||
confReq.Path = "role/" + roleTokenName
|
||||
confReq.Operation = logical.UpdateOperation
|
||||
confReq.Data = map[string]interface{}{
|
||||
"policies": []string{"policy"},
|
||||
"lease": "6h",
|
||||
}
|
||||
resp, err = b.HandleRequest(context.Background(), &confReq)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
confReq.Operation = logical.ReadOperation
|
||||
confReq.Path = "creds/" + roleTokenName
|
||||
resp, err = b.HandleRequest(context.Background(), &confReq)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp == nil {
|
||||
t.Fatal("resp nil")
|
||||
}
|
||||
if resp.IsError() {
|
||||
t.Fatalf("resp is error: %v", resp.Error())
|
||||
}
|
||||
|
||||
// extract the secret, so we can query nomad directly
|
||||
generatedSecret := resp.Secret
|
||||
generatedSecret.TTL = 6 * time.Hour
|
||||
|
||||
var d struct {
|
||||
Token string `mapstructure:"secret_id"`
|
||||
Accessor string `mapstructure:"accessor_id"`
|
||||
}
|
||||
if err := mapstructure.Decode(resp.Data, &d); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Build a client and verify that the credentials work
|
||||
nomadapiConfig := nomadapi.DefaultConfig()
|
||||
nomadapiConfig.Address = connData["address"].(string)
|
||||
nomadapiConfig.SecretID = d.Token
|
||||
client, err := nomadapi.NewClient(nomadapiConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// default query options for Nomad queries ... not sure if needed
|
||||
qOpts := &nomadapi.QueryOptions{
|
||||
Namespace: "default",
|
||||
}
|
||||
|
||||
// connect to Nomad and verify the token name does not exceed the
|
||||
// max_token_name_length
|
||||
token, _, err := client.ACLTokens().Self(qOpts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(token.Name) > expectedMaxTokenNameLength {
|
||||
t.Fatalf("token name exceeds max length (%d): %s (%d)", expectedMaxTokenNameLength, token.Name, len(token.Name))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,11 @@ func pathConfigAccess(b *backend) *framework.Path {
|
|||
Type: framework.TypeString,
|
||||
Description: "Token for API calls",
|
||||
},
|
||||
|
||||
"max_token_name_length": &framework.FieldSchema{
|
||||
Type: framework.TypeInt,
|
||||
Description: "Max length for name of generated Nomad tokens",
|
||||
},
|
||||
},
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
|
@ -73,7 +78,8 @@ func (b *backend) pathConfigAccessRead(ctx context.Context, req *logical.Request
|
|||
|
||||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"address": conf.Address,
|
||||
"address": conf.Address,
|
||||
"max_token_name_length": conf.MaxTokenNameLength,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
@ -96,6 +102,8 @@ func (b *backend) pathConfigAccessWrite(ctx context.Context, req *logical.Reques
|
|||
conf.Token = token.(string)
|
||||
}
|
||||
|
||||
conf.MaxTokenNameLength = data.Get("max_token_name_length").(int)
|
||||
|
||||
entry, err := logical.StorageEntryJSON("config/access", conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -115,6 +123,7 @@ func (b *backend) pathConfigAccessDelete(ctx context.Context, req *logical.Reque
|
|||
}
|
||||
|
||||
type accessConfig struct {
|
||||
Address string `json:"address"`
|
||||
Token string `json:"token"`
|
||||
Address string `json:"address"`
|
||||
Token string `json:"token"`
|
||||
MaxTokenNameLength int `json:"max_token_name_length"`
|
||||
}
|
||||
|
|
|
@ -11,6 +11,10 @@ import (
|
|||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
// maxTokenNameLength is the maximum length for the name of a Nomad access
|
||||
// token
|
||||
const maxTokenNameLength = 256
|
||||
|
||||
func pathCredsCreate(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "creds/" + framework.GenericNameRegex("name"),
|
||||
|
@ -29,6 +33,12 @@ func pathCredsCreate(b *backend) *framework.Path {
|
|||
|
||||
func (b *backend) pathTokenRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
name := d.Get("name").(string)
|
||||
conf, _ := b.readConfigAccess(ctx, req.Storage)
|
||||
// establish a default
|
||||
tokenNameLength := maxTokenNameLength
|
||||
if conf != nil && conf.MaxTokenNameLength > 0 {
|
||||
tokenNameLength = conf.MaxTokenNameLength
|
||||
}
|
||||
|
||||
role, err := b.Role(ctx, req.Storage, name)
|
||||
if err != nil {
|
||||
|
@ -56,10 +66,11 @@ func (b *backend) pathTokenRead(ctx context.Context, req *logical.Request, d *fr
|
|||
// Generate a name for the token
|
||||
tokenName := fmt.Sprintf("vault-%s-%s-%d", name, req.DisplayName, time.Now().UnixNano())
|
||||
|
||||
// Handling nomad maximum token length
|
||||
// https://github.com/hashicorp/nomad/blob/d9276e22b3b74674996fb548cdb6bc4c70d5b0e4/nomad/structs/structs.go#L115
|
||||
if len(tokenName) > 64 {
|
||||
tokenName = tokenName[0:63]
|
||||
// Note: if the given role name is sufficiently long, the UnixNano() portion
|
||||
// of the pseudo randomized token name is the part that gets trimmed off,
|
||||
// weakening it's randomness.
|
||||
if len(tokenName) > tokenNameLength {
|
||||
tokenName = tokenName[:tokenNameLength]
|
||||
}
|
||||
|
||||
// Create it
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
uuid "github.com/hashicorp/go-uuid"
|
||||
"github.com/hashicorp/vault/helper/xor"
|
||||
|
@ -80,3 +81,9 @@ func GenerateRootWithError(t testing.T, cluster *vault.TestCluster, drToken bool
|
|||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// RandomWithPrefix is used to generate a unique name with a prefix, for
|
||||
// randomizing names in acceptance tests
|
||||
func RandomWithPrefix(name string) string {
|
||||
return fmt.Sprintf("%s-%d", name, rand.New(rand.NewSource(time.Now().UnixNano())).Int())
|
||||
}
|
||||
|
|
|
@ -37,12 +37,20 @@ Nomad tokens.
|
|||
This value can also be provided on individual calls with the NOMAD_TOKEN
|
||||
environment variable.
|
||||
|
||||
- `max_token_name_length` `(int: <optional>)` – Specifies the maximum length to
|
||||
use for the name of the Nomad token generated with [Generate
|
||||
Credential](#generate-credential). If omitted, `0` is used and ignored,
|
||||
defaulting to the max value allowed by the Nomad version. For Nomad versions
|
||||
0.8.3 and earlier, the default is `64`. For Nomad version 0.8.4 and later, the default is
|
||||
`256`.
|
||||
|
||||
### Sample Payload
|
||||
|
||||
```json
|
||||
{
|
||||
"address": "http://127.0.0.1:4646",
|
||||
"token": "adha..."
|
||||
"token": "adha...",
|
||||
"max_token_name_length": 256
|
||||
}
|
||||
```
|
||||
|
||||
|
|
Loading…
Reference in New Issue