open-vault/builtin/logical/nomad/backend_test.go
Conor Mongey 9c294f1ef0
Bootstrap Nomad ACL system if no token is given (#12451)
* Bootstrap Nomad ACL system if no token is given

Similar to the [Bootstrap the Consul ACL system if no token is given][boostrap-consul]
it would be very useful to bootstrap Nomads ACL system and manage it in
Vault.

[boostrap-consul]:https://github.com/hashicorp/vault/pull/10751

* Add changelog entry

* Remove debug log line

* Remove redundant else

* Rename Nomad acl bootstrap param

* Replace sleep with attempt to list nomad leader, setup will retry until successful

* fmt
2022-04-20 11:06:25 -07:00

556 lines
14 KiB
Go

package nomad
import (
"context"
"fmt"
"os"
"reflect"
"strings"
"testing"
"time"
nomadapi "github.com/hashicorp/nomad/api"
"github.com/hashicorp/vault/helper/testhelpers"
"github.com/hashicorp/vault/helper/testhelpers/docker"
"github.com/hashicorp/vault/sdk/logical"
"github.com/mitchellh/mapstructure"
)
type Config struct {
docker.ServiceURL
Token string
}
func (c *Config) APIConfig() *nomadapi.Config {
apiConfig := nomadapi.DefaultConfig()
apiConfig.Address = c.URL().String()
apiConfig.SecretID = c.Token
return apiConfig
}
func (c *Config) Client() (*nomadapi.Client, error) {
apiConfig := c.APIConfig()
return nomadapi.NewClient(apiConfig)
}
func prepareTestContainer(t *testing.T, bootstrap bool) (func(), *Config) {
if retAddress := os.Getenv("NOMAD_ADDR"); retAddress != "" {
s, err := docker.NewServiceURLParse(retAddress)
if err != nil {
t.Fatal(err)
}
return func() {}, &Config{*s, os.Getenv("NOMAD_TOKEN")}
}
runner, err := docker.NewServiceRunner(docker.RunOptions{
ImageRepo: "multani/nomad",
ImageTag: "1.1.6",
ContainerName: "nomad",
Ports: []string{"4646/tcp"},
Cmd: []string{"agent", "-dev"},
Env: []string{`NOMAD_LOCAL_CONFIG=bind_addr = "0.0.0.0" acl { enabled = true }`},
})
if err != nil {
t.Fatalf("Could not start docker Nomad: %s", err)
}
var nomadToken string
svc, err := runner.StartService(context.Background(), func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) {
var err error
nomadapiConfig := nomadapi.DefaultConfig()
nomadapiConfig.Address = fmt.Sprintf("http://%s:%d/", host, port)
nomad, err := nomadapi.NewClient(nomadapiConfig)
if err != nil {
return nil, err
}
_, err = nomad.Status().Leader()
if err != nil {
t.Logf("[DEBUG] Nomad is not ready yet: %s", err)
return nil, err
}
if bootstrap {
aclbootstrap, _, err := nomad.ACLTokens().Bootstrap(nil)
if err != nil {
return nil, err
}
nomadToken = aclbootstrap.SecretID
t.Logf("[WARN] Generated Master token: %s", nomadToken)
}
nomadAuthConfig := nomadapi.DefaultConfig()
nomadAuthConfig.Address = nomad.Address()
if bootstrap {
nomadAuthConfig.SecretID = nomadToken
nomadAuth, err := nomadapi.NewClient(nomadAuthConfig)
if err != nil {
return nil, err
}
err = preprePolicies(nomadAuth)
if err != nil {
return nil, err
}
}
u, _ := docker.NewServiceURLParse(nomadapiConfig.Address)
return &Config{
ServiceURL: *u,
Token: nomadToken,
}, nil
})
if err != nil {
t.Fatalf("Could not start docker Nomad: %s", err)
}
return svc.Cleanup, svc.Config.(*Config)
}
func preprePolicies(nomadClient *nomadapi.Client) error {
policy := &nomadapi.ACLPolicy{
Name: "test",
Description: "test",
Rules: `namespace "default" {
policy = "read"
}
`,
}
anonPolicy := &nomadapi.ACLPolicy{
Name: "anonymous",
Description: "Deny all access for anonymous requests",
Rules: `namespace "default" {
policy = "deny"
}
agent {
policy = "deny"
}
node {
policy = "deny"
}
`,
}
_, err := nomadClient.ACLPolicies().Upsert(policy, nil)
if err != nil {
return err
}
_, err = nomadClient.ACLPolicies().Upsert(anonPolicy, nil)
if err != nil {
return err
}
return nil
}
func TestBackend_config_Bootstrap(t *testing.T) {
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
b, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
cleanup, svccfg := prepareTestContainer(t, false)
defer cleanup()
connData := map[string]interface{}{
"address": svccfg.URL().String(),
"token": "",
}
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)
}
expected := map[string]interface{}{
"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)
}
nomadClient, err := svccfg.Client()
if err != nil {
t.Fatalf("failed to construct nomaad client, %v", err)
}
token, _, err := nomadClient.ACLTokens().Bootstrap(nil)
if err == nil {
t.Fatalf("expected acl system to be bootstrapped already, but was able to get the bootstrap token : %v", token)
}
// NOTE: fragile test, but it's the only way, AFAIK, to check that nomad is
// bootstrapped
if !strings.Contains(err.Error(), "bootstrap already done") {
t.Fatalf("expected acl system to be bootstrapped already: err: %v", err)
}
}
func TestBackend_config_access(t *testing.T) {
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
b, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
cleanup, svccfg := prepareTestContainer(t, true)
defer cleanup()
connData := map[string]interface{}{
"address": svccfg.URL().String(),
"token": svccfg.Token,
}
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)
}
expected := map[string]interface{}{
"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)
}
if resp.Data["token"] != nil {
t.Fatalf("token should not be set in the response")
}
}
func TestBackend_renew_revoke(t *testing.T) {
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
b, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
cleanup, svccfg := prepareTestContainer(t, true)
defer cleanup()
connData := map[string]interface{}{
"address": svccfg.URL().String(),
"token": svccfg.Token,
}
req := &logical.Request{
Storage: config.StorageView,
Operation: logical.UpdateOperation,
Path: "config/access",
Data: connData,
}
resp, err := b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
req.Path = "role/test"
req.Data = map[string]interface{}{
"policies": []string{"policy"},
"lease": "6h",
}
resp, err = b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
req.Operation = logical.ReadOperation
req.Path = "creds/test"
resp, err = b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("resp nil")
}
if resp.IsError() {
t.Fatalf("resp is error: %v", resp.Error())
}
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)
}
t.Logf("[WARN] Generated token: %s with accessor %s", d.Token, d.Accessor)
// 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)
}
t.Log("[WARN] Verifying that the generated token works...")
_, err = client.Agent().Members, nil
if err != nil {
t.Fatal(err)
}
req.Operation = logical.RenewOperation
req.Secret = generatedSecret
resp, err = b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("got nil response from renew")
}
req.Operation = logical.RevokeOperation
resp, err = b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
// Build a management client and verify that the token does not exist anymore
nomadmgmtConfig := nomadapi.DefaultConfig()
nomadmgmtConfig.Address = connData["address"].(string)
nomadmgmtConfig.SecretID = connData["token"].(string)
mgmtclient, err := nomadapi.NewClient(nomadmgmtConfig)
if err != nil {
t.Fatal(err)
}
q := &nomadapi.QueryOptions{
Namespace: "default",
}
t.Log("[WARN] Verifying that the generated token does not exist...")
_, _, err = mgmtclient.ACLTokens().Info(d.Accessor, q)
if err == nil {
t.Fatal("err: expected error")
}
}
func TestBackend_CredsCreateEnvVar(t *testing.T) {
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
b, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
cleanup, svccfg := prepareTestContainer(t, true)
defer cleanup()
req := logical.TestRequest(t, logical.UpdateOperation, "role/test")
req.Data = map[string]interface{}{
"policies": []string{"policy"},
"lease": "6h",
}
resp, err := b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
os.Setenv("NOMAD_TOKEN", svccfg.Token)
defer os.Unsetenv("NOMAD_TOKEN")
os.Setenv("NOMAD_ADDR", svccfg.URL().String())
defer os.Unsetenv("NOMAD_ADDR")
req.Operation = logical.ReadOperation
req.Path = "creds/test"
resp, err = b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("resp nil")
}
if resp.IsError() {
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, svccfg := prepareTestContainer(t, true)
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": svccfg.URL().String(),
"token": svccfg.Token,
"max_token_name_length": tc.tokenLength,
}
expected := map[string]interface{}{
"address": svccfg.URL().String(),
"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))
}
})
}
}