Add the ability to use multiple paths for capability checking (#3663)

* Add the ability to use multiple paths for capability checking. WIP
(tests, docs).

Fixes #3336

* Added tests

* added 'paths' field

* Update docs

* return error if paths is not supplied
This commit is contained in:
Jeff Mitchell 2018-03-01 11:14:56 -05:00 committed by Vishal Nayak
parent ff99c8420e
commit 5034ae2dcb
5 changed files with 271 additions and 58 deletions

View File

@ -101,8 +101,12 @@ func NewSystemBackend(core *Core) *SystemBackend {
Description: "Accessor of the token for which capabilities are being queried.",
},
"path": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Path on which capabilities are being queried.",
Type: framework.TypeCommaStringSlice,
Description: "(DEPRECATED) Path on which capabilities are being queried. Use 'paths' instead.",
},
"paths": &framework.FieldSchema{
Type: framework.TypeCommaStringSlice,
Description: "Paths on which capabilities are being queried.",
},
},
@ -151,8 +155,12 @@ func NewSystemBackend(core *Core) *SystemBackend {
Description: "Token for which capabilities are being queried.",
},
"path": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Path on which capabilities are being queried.",
Type: framework.TypeCommaStringSlice,
Description: "(DEPRECATED) Path on which capabilities are being queried. Use 'paths' instead.",
},
"paths": &framework.FieldSchema{
Type: framework.TypeCommaStringSlice,
Description: "Paths on which capabilities are being queried.",
},
},
@ -173,8 +181,12 @@ func NewSystemBackend(core *Core) *SystemBackend {
Description: "Token for which capabilities are being queried.",
},
"path": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Path on which capabilities are being queried.",
Type: framework.TypeCommaStringSlice,
Description: "(DEPRECATED) Path on which capabilities are being queried. Use 'paths' instead.",
},
"paths": &framework.FieldSchema{
Type: framework.TypeCommaStringSlice,
Description: "Paths on which capabilities are being queried.",
},
},
@ -1264,24 +1276,6 @@ func (b *SystemBackend) handleAuditedHeadersRead(ctx context.Context, req *logic
}, nil
}
// handleCapabilities returns the ACL capabilities of the token for a given path
func (b *SystemBackend) handleCapabilities(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
token := d.Get("token").(string)
if token == "" {
token = req.ClientToken
}
capabilities, err := b.Core.Capabilities(ctx, token, d.Get("path").(string))
if err != nil {
return nil, err
}
return &logical.Response{
Data: map[string]interface{}{
"capabilities": capabilities,
},
}, nil
}
// handleCapabilitiesAccessor returns the ACL capabilities of the
// token associted with the given accessor for a given path.
func (b *SystemBackend) handleCapabilitiesAccessor(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
@ -1295,16 +1289,53 @@ func (b *SystemBackend) handleCapabilitiesAccessor(ctx context.Context, req *log
return nil, err
}
capabilities, err := b.Core.Capabilities(ctx, aEntry.TokenID, d.Get("path").(string))
if err != nil {
return nil, err
d.Raw["token"] = aEntry.TokenID
return b.handleCapabilities(ctx, req, d)
}
// handleCapabilities returns the ACL capabilities of the token for a given path
func (b *SystemBackend) handleCapabilities(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
var token string
if strings.HasSuffix(req.Path, "capabilities-self") {
token = req.ClientToken
} else {
tokenRaw, ok := d.Raw["token"]
if ok {
token, _ = tokenRaw.(string)
}
}
if token == "" {
return nil, fmt.Errorf("no token found")
}
return &logical.Response{
Data: map[string]interface{}{
"capabilities": capabilities,
},
}, nil
ret := &logical.Response{
Data: map[string]interface{}{},
}
paths := d.Get("paths").([]string)
if len(paths) == 0 {
// Read from the deprecated field
paths = d.Get("path").([]string)
}
if len(paths) == 0 {
return logical.ErrorResponse("paths must be supplied"), nil
}
for _, path := range paths {
pathCap, err := b.Core.Capabilities(ctx, token, path)
if err != nil {
return nil, err
}
ret.Data[path] = pathCap
}
// This is only here for backwards compatibility
if len(paths) == 1 {
ret.Data["capabilities"] = ret.Data[paths[0]]
}
return ret, nil
}
// handleRekeyRetrieve returns backed-up, PGP-encrypted unseal keys from a

View File

@ -337,9 +337,159 @@ path "foo/bar*" {
path "sys/capabilities*" {
capabilities = ["update"]
}
path "bar/baz" {
capabilities = ["read", "update"]
}
path "bar/baz" {
capabilities = ["delete"]
}
`
func TestSystemBackend_Capabilities(t *testing.T) {
func TestSystemBackend_PathCapabilities(t *testing.T) {
var resp *logical.Response
var err error
core, b, rootToken := testCoreSystemBackend(t)
policy, _ := ParseACLPolicy(capabilitiesPolicy)
err = core.policyStore.SetPolicy(context.Background(), policy)
if err != nil {
t.Fatalf("err: %v", err)
}
path1 := "foo/bar"
path2 := "foo/bar/sample"
path3 := "sys/capabilities"
path4 := "bar/baz"
rootCheckFunc := func(t *testing.T, resp *logical.Response) {
// All the paths should have "root" as the capability
expectedRoot := []string{"root"}
if !reflect.DeepEqual(resp.Data[path1], expectedRoot) ||
!reflect.DeepEqual(resp.Data[path2], expectedRoot) ||
!reflect.DeepEqual(resp.Data[path3], expectedRoot) ||
!reflect.DeepEqual(resp.Data[path4], expectedRoot) {
t.Fatalf("bad: capabilities; expected: %#v, actual: %#v", expectedRoot, resp.Data)
}
}
// Check the capabilities using the root token
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Path: "capabilities",
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
"paths": []string{path1, path2, path3, path4},
"token": rootToken,
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
}
rootCheckFunc(t, resp)
// Check the capabilities using capabilities-self
resp, err = b.HandleRequest(context.Background(), &logical.Request{
ClientToken: rootToken,
Path: "capabilities-self",
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
"paths": []string{path1, path2, path3, path4},
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
}
rootCheckFunc(t, resp)
// Lookup the accessor of the root token
te, err := core.tokenStore.Lookup(context.Background(), rootToken)
if err != nil {
t.Fatal(err)
}
// Check the capabilities using capabilities-accessor endpoint
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Path: "capabilities-accessor",
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
"paths": []string{path1, path2, path3, path4},
"accessor": te.Accessor,
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
}
rootCheckFunc(t, resp)
// Create a non-root token
testMakeToken(t, core.tokenStore, rootToken, "tokenid", "", []string{"test"})
nonRootCheckFunc := func(t *testing.T, resp *logical.Response) {
expected1 := []string{"create", "sudo", "update"}
expected2 := expected1
expected3 := []string{"update"}
expected4 := []string{"delete", "read", "update"}
if !reflect.DeepEqual(resp.Data[path1], expected1) ||
!reflect.DeepEqual(resp.Data[path2], expected2) ||
!reflect.DeepEqual(resp.Data[path3], expected3) ||
!reflect.DeepEqual(resp.Data[path4], expected4) {
t.Fatalf("bad: capabilities; actual: %#v", resp.Data)
}
}
// Check the capabilities using a non-root token
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Path: "capabilities",
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
"paths": []string{path1, path2, path3, path4},
"token": "tokenid",
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
}
nonRootCheckFunc(t, resp)
// Check the capabilities of a non-root token using capabilities-self
// endpoint
resp, err = b.HandleRequest(context.Background(), &logical.Request{
ClientToken: "tokenid",
Path: "capabilities-self",
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
"paths": []string{path1, path2, path3, path4},
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
}
nonRootCheckFunc(t, resp)
// Lookup the accessor of the non-root token
te, err = core.tokenStore.Lookup(context.Background(), "tokenid")
if err != nil {
t.Fatal(err)
}
// Check the capabilities using a non-root token using
// capabilities-accessor endpoint
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Path: "capabilities-accessor",
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
"paths": []string{path1, path2, path3, path4},
"accessor": te.Accessor,
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
}
nonRootCheckFunc(t, resp)
}
func TestSystemBackend_Capabilities_BC(t *testing.T) {
testCapabilities(t, "capabilities")
testCapabilities(t, "capabilities-self")
}
@ -347,7 +497,11 @@ func TestSystemBackend_Capabilities(t *testing.T) {
func testCapabilities(t *testing.T, endpoint string) {
core, b, rootToken := testCoreSystemBackend(t)
req := logical.TestRequest(t, logical.UpdateOperation, endpoint)
req.Data["token"] = rootToken
if endpoint == "capabilities-self" {
req.ClientToken = rootToken
} else {
req.Data["token"] = rootToken
}
req.Data["path"] = "any_path"
resp, err := b.HandleRequest(context.Background(), req)
@ -372,7 +526,11 @@ func testCapabilities(t *testing.T, endpoint string) {
testMakeToken(t, core.tokenStore, rootToken, "tokenid", "", []string{"test"})
req = logical.TestRequest(t, logical.UpdateOperation, endpoint)
req.Data["token"] = "tokenid"
if endpoint == "capabilities-self" {
req.ClientToken = "tokenid"
} else {
req.Data["token"] = "tokenid"
}
req.Data["path"] = "foo/bar"
resp, err = b.HandleRequest(context.Background(), req)
@ -390,7 +548,7 @@ func testCapabilities(t *testing.T, endpoint string) {
}
}
func TestSystemBackend_CapabilitiesAccessor(t *testing.T) {
func TestSystemBackend_CapabilitiesAccessor_BC(t *testing.T) {
core, b, rootToken := testCoreSystemBackend(t)
te, err := core.tokenStore.Lookup(context.Background(), rootToken)
if err != nil {
@ -1747,22 +1905,22 @@ func TestSystemBackend_rotate(t *testing.T) {
func testSystemBackend(t *testing.T) logical.Backend {
c, _, _ := TestCoreUnsealed(t)
return testSystemBackendInternal(t, c)
return c.systemBackend
}
func testSystemBackendRaw(t *testing.T) logical.Backend {
c, _, _ := TestCoreUnsealedRaw(t)
return testSystemBackendInternal(t, c)
return c.systemBackend
}
func testCoreSystemBackend(t *testing.T) (*Core, logical.Backend, string) {
c, _, root := TestCoreUnsealed(t)
return c, testSystemBackendInternal(t, c), root
return c, c.systemBackend, root
}
func testCoreSystemBackendRaw(t *testing.T) (*Core, logical.Backend, string) {
c, _, root := TestCoreUnsealedRaw(t)
return c, testSystemBackendInternal(t, c), root
return c, c.systemBackend, root
}
func testSystemBackendInternal(t *testing.T, c *Core) logical.Backend {

View File

@ -26,18 +26,18 @@ for the given path.
### Parameters
- `accessor` `(string: <required>)` Specifies the accessor of the token to
check.
- `accessor` `(string: <required>)` Accessor of the token for which
capabilities are being queried.
- `path` `(string: <required>)` Specifies the path on which the token's
capabilities will be checked.
- `paths` `(list: <required>)` Paths on which capabilities are being
queried.
### Sample Payload
```json
{
"accessor": "abcd1234",
"path": "secret/foo"
"paths": ["secret/foo", "secret/bar"]
}
```
@ -55,6 +55,15 @@ $ curl \
```json
{
"capabilities": ["read", "list"]
"secret/bar": [
"sudo",
"update"
],
"secret/foo": [
"delete",
"list",
"read",
"update"
]
}
```

View File

@ -26,14 +26,13 @@ client token is the Vault token with which this API call is made.
### Parameters
- `path` `(string: <required>)`  Specifies the path on which the client token's
capabilities will be checked.
- `paths` `(list: <required>)`  Paths on which capabilities are being queried.
### Sample Payload
```json
{
"path": "secret/foo"
"paths": ["secret/foo", "secret/bar"]
}
```
@ -51,6 +50,14 @@ $ curl \
```json
{
"capabilities": ["read", "list"]
"secret/bar": [
"sudo",
"update"
],
"secret/foo": [
"delete",
"list",
"read",
"update"
]
}
```

View File

@ -24,18 +24,17 @@ This endpoint returns the list of capabilities for a provided token.
### Parameters
- `path` `(string: <required>)`  Specifies the path against which to check the
token's capabilities.
- `paths` `(list: <required>)`  Paths on which capabilities are being queried.
- `token` `(string: <required>)`  Specifies the token for which to check
capabilities.
- `token` `(string: <required>)`  Token for which capabilities are being
queried.
### Sample Payload
```json
{
"path": "secret/foo",
"token": "abcd1234"
"token": "abcd1234",
"paths": ["secret/foo", "secret/bar"]
}
```
@ -53,6 +52,15 @@ $ curl \
```json
{
"capabilities": ["read", "list"]
"secret/bar": [
"sudo",
"update"
],
"secret/foo": [
"delete",
"list",
"read",
"update"
]
}
```