Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Support custom max Nomad token name length [supersedes https://github.com/hashicorp/vault/pull/4361] #5117

Merged
merged 10 commits into from
Aug 16, 2018
3 changes: 3 additions & 0 deletions builtin/logical/nomad/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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{
Expand Down Expand Up @@ -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
}
160 changes: 157 additions & 3 deletions builtin/logical/nomad/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 }`},
}
Expand Down Expand Up @@ -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": maxTokenNameLength,
}
if !reflect.DeepEqual(expected, resp.Data) {
t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data)
Expand Down Expand Up @@ -300,3 +302,155 @@ 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: "ConfigOverride-LongName-notrim",
roleName: "testlongerrolenametoexceed64charsdddddddddddddddddddddddd",
},
}

for _, tc := range testCases {
t.Run(tc.title, func(t *testing.T) {
// setup config/access
connData := map[string]interface{}{
"address": connURL,
"token": connToken,
}
expected := map[string]interface{}{
"address": connURL,
"max_token_name_length": maxTokenNameLength,
}

expectedTokenNameLength := maxTokenNameLength

if tc.tokenLength != 0 {
connData["max_token_name_length"] = tc.tokenLength
expected["max_token_name_length"] = tc.tokenLength
expectedTokenNameLength = 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) > expectedTokenNameLength {
t.Fatalf("token name exceeds max length (%d): %s (%d)", expectedTokenNameLength, token.Name, len(token.Name))
}
})
}
}
21 changes: 18 additions & 3 deletions builtin/logical/nomad/path_config_access.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import (

const configAccessKey = "config/access"

// maxTokenNameLength is the maximum length for the name of a Nomad access
// token
const maxTokenNameLength = 256

func pathConfigAccess(b *backend) *framework.Path {
return &framework.Path{
Pattern: "config/access",
Expand All @@ -23,6 +27,12 @@ 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",
Default: maxTokenNameLength,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more thing here. You should probably not set the default value in the path config. This will mean we will store this value in storage and if we change the default again, they will have an explicit value. You can just omit the Default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Omitting the default will leave a zero value in the struct and be serialized like that. We could change the accessConfig.MaxTokenNameLength to be an *int and use nil there, or just ignore the value when it's zero. I'll assume the later for now

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or set it the default to -1 maybe?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the zero value is fine, it is the same as default. I don't think we need to do any int pointer work. Does this api support create vs update? If so, you should be able to handle the case where they don't provide the value and just patch the updated fields.

},
},

Callbacks: map[logical.Operation]framework.OperationFunc{
Expand Down Expand Up @@ -73,7 +83,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
}
Expand All @@ -96,6 +107,9 @@ func (b *backend) pathConfigAccessWrite(ctx context.Context, req *logical.Reques
conf.Token = token.(string)
}

// max_token_name_length has default of 256
conf.MaxTokenNameLength = data.Get("max_token_name_length").(int)

entry, err := logical.StorageEntryJSON("config/access", conf)
if err != nil {
return nil, err
Expand All @@ -115,6 +129,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"`
}
15 changes: 11 additions & 4 deletions builtin/logical/nomad/path_creds_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,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 {
Expand Down Expand Up @@ -56,10 +62,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 suffeciently long, the UnixNano() portion
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: suffeciently

// of the pseudo randomized token name is the part that gets trimmed off,
// weaking it's randomness.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: weakening

if len(tokenName) > tokenNameLength {
tokenName = tokenName[:tokenNameLength]
}

// Create it
Expand Down
7 changes: 7 additions & 0 deletions helper/testhelpers/testhelpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"math/rand"
"time"

uuid "github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/xor"
Expand Down Expand Up @@ -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())
}
6 changes: 5 additions & 1 deletion website/source/api/secret/nomad/index.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,16 @@ 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). Default `256`.

### Sample Payload

```json
{
"address": "http://127.0.0.1:4646",
"token": "adha..."
"token": "adha...",
"max_token_name_length": 256
}
```

Expand Down