Prefix Search ACL enforcement

This commit is contained in:
Michael Schurter 2017-10-10 15:04:23 -07:00
parent 5391896e37
commit be69374ecd
3 changed files with 164 additions and 5 deletions

View File

@ -4,6 +4,7 @@ import (
"strings"
memdb "github.com/hashicorp/go-memdb"
"github.com/hashicorp/nomad/acl"
"github.com/hashicorp/nomad/nomad/state"
"github.com/hashicorp/nomad/nomad/structs"
)
@ -107,8 +108,36 @@ func roundUUIDDownIfOdd(prefix string, context structs.Context) string {
// PrefixSearch is used to list matches for a given prefix, and returns
// matching jobs, evaluations, allocations, and/or nodes.
func (s *Search) PrefixSearch(args *structs.SearchRequest,
reply *structs.SearchResponse) error {
func (s *Search) PrefixSearch(args *structs.SearchRequest, reply *structs.SearchResponse) error {
aclObj, err := s.srv.ResolveToken(args.SecretID)
if err != nil {
return err
}
// Require either node:read or namespace:read-job
nodeRead := true
jobRead := true
if aclObj != nil {
nodeRead = aclObj.AllowNodeRead()
jobRead = aclObj.AllowNsOp(args.RequestNamespace(), acl.NamespaceCapabilityReadJob)
if !nodeRead && !jobRead {
return structs.ErrPermissionDenied
}
// Reject requests that explicitly specify a disallowed context. This
// should give the user better feedback then simply filtering out all
// results and returning an empty list.
if !nodeRead && args.Context == structs.Nodes {
return structs.ErrPermissionDenied
}
if !jobRead {
switch args.Context {
case structs.Allocs, structs.Deployments, structs.Evals, structs.Jobs:
return structs.ErrPermissionDenied
}
}
}
reply.Matches = make(map[structs.Context][]string)
reply.Truncations = make(map[structs.Context]bool)
@ -126,6 +155,19 @@ func (s *Search) PrefixSearch(args *structs.SearchRequest,
}
for _, ctx := range contexts {
if ctx == structs.Nodes && !nodeRead {
// Not allowed to search nodes
continue
}
if !jobRead {
switch ctx {
case structs.Allocs, structs.Deployments, structs.Evals, structs.Jobs:
// Not allowed to read jobs
continue
}
}
iter, err := getResourceIter(ctx, args.RequestNamespace(), roundUUIDDownIfOdd(args.Prefix, args.Context), ws, state)
if err != nil {

View File

@ -6,6 +6,7 @@ import (
"testing"
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
"github.com/hashicorp/nomad/acl"
"github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/testutil"
@ -59,6 +60,117 @@ func TestSearch_PrefixSearch_Job(t *testing.T) {
assert.Equal(uint64(jobIndex), resp.Index)
}
func TestSearch_PrefixSearch_ACL(t *testing.T) {
assert := assert.New(t)
jobID := "aaaaaaaa-e8f7-fd38-c855-ab94ceb8970"
t.Parallel()
s, root := testACLServer(t, func(c *Config) {
c.NumSchedulers = 0
})
defer s.Shutdown()
codec := rpcClient(t, s)
testutil.WaitForLeader(t, s.RPC)
state := s.fsm.State()
job := registerAndVerifyJob(s, t, jobID, 0)
assert.Nil(state.UpsertNode(1001, mock.Node()))
req := &structs.SearchRequest{
Prefix: "",
Context: structs.Jobs,
QueryOptions: structs.QueryOptions{
Region: "global",
Namespace: job.Namespace,
},
}
// Try without a token and expect failure
{
var resp structs.SearchResponse
err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp)
assert.NotNil(err)
assert.Equal(err.Error(), structs.ErrPermissionDenied.Error())
}
// Try with an invalid token and expect failure
{
invalidToken := mock.CreatePolicyAndToken(t, state, 1003, "test-invalid",
mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListJobs}))
req.SecretID = invalidToken.SecretID
var resp structs.SearchResponse
err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp)
assert.NotNil(err)
assert.Equal(err.Error(), structs.ErrPermissionDenied.Error())
}
// Try with a node:read token and expect failure due to Jobs being the context
{
validToken := mock.CreatePolicyAndToken(t, state, 1005, "test-invalid2", mock.NodePolicy(acl.PolicyRead))
req.SecretID = validToken.SecretID
var resp structs.SearchResponse
err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp)
assert.NotNil(err)
assert.Equal(err.Error(), structs.ErrPermissionDenied.Error())
}
// Try with a node:read token and expect success due to All context
{
validToken := mock.CreatePolicyAndToken(t, state, 1007, "test-valid", mock.NodePolicy(acl.PolicyRead))
req.Context = structs.All
req.SecretID = validToken.SecretID
var resp structs.SearchResponse
assert.Nil(msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
assert.Equal(uint64(1001), resp.Index)
assert.Len(resp.Matches[structs.Nodes], 1)
// Jobs filtered out since token only has access to node:read
assert.Len(resp.Matches[structs.Jobs], 0)
}
// Try with a valid token for namespace:read-job
{
validToken := mock.CreatePolicyAndToken(t, state, 1009, "test-valid2",
mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}))
req.SecretID = validToken.SecretID
var resp structs.SearchResponse
assert.Nil(msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
assert.Equal(uint64(1001), resp.Index)
assert.Len(resp.Matches[structs.Jobs], 1)
assert.Equal(job.ID, resp.Matches[structs.Jobs][0])
// Nodes filtered out since token only has access to namespace:read-job
assert.Len(resp.Matches[structs.Nodes], 0)
}
// Try with a valid token for node:read and namespace:read-job
{
validToken := mock.CreatePolicyAndToken(t, state, 1011, "test-valid3", strings.Join([]string{
mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}),
mock.NodePolicy(acl.PolicyRead),
}, "\n"))
req.SecretID = validToken.SecretID
var resp structs.SearchResponse
assert.Nil(msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
assert.Equal(uint64(1001), resp.Index)
assert.Len(resp.Matches[structs.Jobs], 1)
assert.Equal(job.ID, resp.Matches[structs.Jobs][0])
assert.Len(resp.Matches[structs.Nodes], 1)
}
// Try with a management token
{
req.SecretID = root.SecretID
var resp structs.SearchResponse
assert.Nil(msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
assert.Equal(uint64(1001), resp.Index)
assert.Len(resp.Matches[structs.Jobs], 1)
assert.Equal(job.ID, resp.Matches[structs.Jobs][0])
assert.Len(resp.Matches[structs.Nodes], 1)
}
}
func TestSearch_PrefixSearch_All_JobWithHyphen(t *testing.T) {
assert := assert.New(t)
prefix := "example-test-------" // Assert that a job with more than 4 hyphens works

View File

@ -20,9 +20,14 @@ The table below shows this endpoint's support for
[blocking queries](/api/index.html#blocking-queries) and
[required ACLs](/api/index.html#acls).
| Blocking Queries | ACL Required |
| ---------------- | ------------ |
| `NO` | `none` |
| Blocking Queries | ACL Required |
| ---------------- | -------------------------------- |
| `NO` | `node:read, namespace:read-jobs` |
When ACLs are enabled, requests must have a token valid for `node:read` or
`namespace:read-jobs` roles. If the token is only valid for `node:read`, then
job related results will not be returned. If the token is only valid for
`namespace:read-jobs`, then node results will not be returned.
### Parameters