Allow creating Consul management tokens

Fixes #714
This commit is contained in:
Jeff Mitchell 2015-11-02 16:16:51 -05:00
parent a4322afedb
commit 54d47957b5
4 changed files with 148 additions and 18 deletions

View File

@ -31,10 +31,11 @@ IMPROVEMENTS:
* logical: Allow `.` in path-based variables in many more locations [GH-244]
* logical: Responses now contain a "warnings" key containing a list of
warnings returned from the server. These are conditions that did not require
failing an operation, but of which the client should be aware. [GH-676]
failing an operation, but of which the client should be aware. [GH-676]
* physical/consul: Consul now uses a connection pool to limit the number of
outstanding operations, improving behavior when a lot of operations must
happen at once [GH-677]
* secret/consul: Management tokens can now be created [GH-714]
BUG FIXES:

View File

@ -1,6 +1,7 @@
package consul
import (
"bufio"
"encoding/base64"
"fmt"
"io/ioutil"
@ -21,9 +22,10 @@ func TestBackend_basic(t *testing.T) {
config, process := testStartConsulServer(t)
defer testStopConsulServer(t, process)
b, _ := Factory(logical.TestBackendConfig())
logicaltest.Test(t, logicaltest.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Backend: Backend(),
Backend: b,
Steps: []logicaltest.TestStep{
testAccStepConfig(t, config),
testAccStepWritePolicy(t, "test", testPolicy, ""),
@ -32,13 +34,30 @@ func TestBackend_basic(t *testing.T) {
})
}
func TestBackend_management(t *testing.T) {
config, process := testStartConsulServer(t)
defer testStopConsulServer(t, process)
b, _ := Factory(logical.TestBackendConfig())
logicaltest.Test(t, logicaltest.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Backend: b,
Steps: []logicaltest.TestStep{
testAccStepConfig(t, config),
testAccStepWriteManagementPolicy(t, "test", ""),
testAccStepReadManagementToken(t, "test", config),
},
})
}
func TestBackend_crud(t *testing.T) {
_, process := testStartConsulServer(t)
defer testStopConsulServer(t, process)
b, _ := Factory(logical.TestBackendConfig())
logicaltest.Test(t, logicaltest.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Backend: Backend(),
Backend: b,
Steps: []logicaltest.TestStep{
testAccStepWritePolicy(t, "test", testPolicy, ""),
testAccStepReadPolicy(t, "test", testPolicy, DefaultLeaseDuration),
@ -51,9 +70,10 @@ func TestBackend_role_lease(t *testing.T) {
_, process := testStartConsulServer(t)
defer testStopConsulServer(t, process)
b, _ := Factory(logical.TestBackendConfig())
logicaltest.Test(t, logicaltest.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Backend: Backend(),
Backend: b,
Steps: []logicaltest.TestStep{
testAccStepWritePolicy(t, "test", testPolicy, "6h"),
testAccStepReadPolicy(t, "test", testPolicy, 6*time.Hour),
@ -85,12 +105,31 @@ func testStartConsulServer(t *testing.T) (map[string]interface{}, *os.Process) {
"consul", "agent",
"-server",
"-bootstrap",
"-advertise", "127.0.0.1",
"-config-file", tf.Name(),
"-data-dir", td)
stdout, _ := cmd.StdoutPipe()
stderr, _ := cmd.StderrPipe()
stdoutScanner := bufio.NewScanner(stdout)
stderrScanner := bufio.NewScanner(stderr)
stdoutScanFunc := func() {
for stdoutScanner.Scan() {
t.Logf("Consul stdout: %s\n", stdoutScanner.Text())
}
}
stderrScanFunc := func() {
for stderrScanner.Scan() {
t.Logf("Consul stderr: %s\n", stderrScanner.Text())
}
}
if os.Getenv("VAULT_VERBOSE_ACC_TESTS") != "" {
go stdoutScanFunc()
go stderrScanFunc()
}
if err := cmd.Start(); err != nil {
t.Fatalf("error starting Consul: %s", err)
}
// Give Consul time to startup
time.Sleep(2 * time.Second)
@ -157,6 +196,43 @@ func testAccStepReadToken(
}
}
func testAccStepReadManagementToken(
t *testing.T, name string, conf map[string]interface{}) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.ReadOperation,
Path: "creds/" + name,
Check: func(resp *logical.Response) error {
var d struct {
Token string `mapstructure:"token"`
}
if err := mapstructure.Decode(resp.Data, &d); err != nil {
return err
}
log.Printf("[WARN] Generated token: %s", d.Token)
// Build a client and verify that the credentials work
config := api.DefaultConfig()
config.Address = conf["address"].(string)
config.Token = d.Token
client, err := api.NewClient(config)
if err != nil {
return err
}
log.Printf("[WARN] Verifying that the generated token works...")
_, _, err = client.ACL().Create(&api.ACLEntry{
Type: "management",
Name: "test2",
}, nil)
if err != nil {
return err
}
return nil
},
}
}
func testAccStepWritePolicy(t *testing.T, name string, policy string, lease string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.WriteOperation,
@ -168,6 +244,17 @@ func testAccStepWritePolicy(t *testing.T, name string, policy string, lease stri
}
}
func testAccStepWriteManagementPolicy(t *testing.T, name string, lease string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.WriteOperation,
Path: "roles/" + name,
Data: map[string]interface{}{
"token_type": "management",
"lease": lease,
},
}
}
func testAccStepReadPolicy(t *testing.T, name string, policy string, lease time.Duration) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.ReadOperation,

View File

@ -19,8 +19,18 @@ func pathRoles() *framework.Path {
},
"policy": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Policy document, base64 encoded.",
Type: framework.TypeString,
Description: `Policy document, base64 encoded. Required
for 'client' tokens.`,
},
"token_type": &framework.FieldSchema{
Type: framework.TypeString,
Default: "client",
Description: `Which type of token to create: 'client'
or 'management'. If a 'management' token,
the "policy" parameter is not required.
Defaults to 'client'.`,
},
"lease": &framework.FieldSchema{
@ -54,23 +64,49 @@ func pathRolesRead(
return nil, err
}
if result.TokenType == "" {
result.TokenType = "client"
}
// Generate the response
resp := &logical.Response{
Data: map[string]interface{}{
"policy": base64.StdEncoding.EncodeToString([]byte(result.Policy)),
"lease": result.Lease.String(),
"lease": result.Lease.String(),
"token_type": result.TokenType,
},
}
if result.Policy != "" {
resp.Data["policy"] = base64.StdEncoding.EncodeToString([]byte(result.Policy))
}
return resp, nil
}
func pathRolesWrite(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
tokenType := d.Get("token_type").(string)
switch tokenType {
case "client":
case "management":
default:
return logical.ErrorResponse(
"token_type must be \"client\" or \"management\""), nil
}
name := d.Get("name").(string)
policyRaw, err := base64.StdEncoding.DecodeString(d.Get("policy").(string))
if err != nil {
return logical.ErrorResponse(fmt.Sprintf(
"Error decoding policy base64: %s", err)), nil
policy := d.Get("policy").(string)
var policyRaw []byte
var err error
if tokenType != "management" {
if policy == "" {
return logical.ErrorResponse(
"policy cannot be empty when not using management tokens"), nil
}
policyRaw, err = base64.StdEncoding.DecodeString(d.Get("policy").(string))
if err != nil {
return logical.ErrorResponse(fmt.Sprintf(
"Error decoding policy base64: %s", err)), nil
}
}
lease, err := time.ParseDuration(d.Get("lease").(string))
if err != nil || lease == time.Duration(0) {
@ -78,8 +114,9 @@ func pathRolesWrite(
}
entry, err := logical.StorageEntryJSON("policy/"+name, roleConfig{
Policy: string(policyRaw),
Lease: lease,
Policy: string(policyRaw),
Lease: lease,
TokenType: tokenType,
})
if err != nil {
return nil, err
@ -102,6 +139,7 @@ func pathRolesDelete(
}
type roleConfig struct {
Policy string `json:"policy"`
Lease time.Duration `json:"lease"`
Policy string `json:"policy"`
Lease time.Duration `json:"lease"`
TokenType string `json:"token_type"`
}

View File

@ -42,6 +42,10 @@ func (b *backend) pathTokenRead(
return nil, err
}
if result.TokenType == "" {
result.TokenType = "client"
}
// Get the consul client
c, err := client(req.Storage)
if err != nil {
@ -53,7 +57,7 @@ func (b *backend) pathTokenRead(
// Create it
token, _, err := c.ACL().Create(&api.ACLEntry{
Name: tokenName,
Type: "client",
Type: result.TokenType,
Rules: result.Policy,
}, nil)
if err != nil {