diff --git a/nomad/search_endpoint.go b/nomad/search_endpoint.go index e0ad2dee4..14aa98cb9 100644 --- a/nomad/search_endpoint.go +++ b/nomad/search_endpoint.go @@ -4,7 +4,6 @@ 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" ) @@ -18,8 +17,13 @@ const ( var ( // ossContexts are the oss contexts which are searched to find matches // for a given prefix - ossContexts = []structs.Context{structs.Allocs, structs.Jobs, structs.Nodes, - structs.Evals, structs.Deployments} + ossContexts = []structs.Context{ + structs.Allocs, + structs.Jobs, + structs.Nodes, + structs.Evals, + structs.Deployments, + } ) // Search endpoint is used to look up matches for a given prefix and context @@ -114,28 +118,11 @@ func (s *Search) PrefixSearch(args *structs.SearchRequest, reply *structs.Search 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 - } + namespace := args.RequestNamespace() - // 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 - } - } + // Require either node:read or namespace:read-job + if !anySearchPerms(aclObj, namespace, args.Context) { + return structs.ErrPermissionDenied } reply.Matches = make(map[structs.Context][]string) @@ -149,27 +136,10 @@ func (s *Search) PrefixSearch(args *structs.SearchRequest, reply *structs.Search iters := make(map[structs.Context]memdb.ResultIterator) - contexts := allContexts - if args.Context != structs.All { - contexts = []structs.Context{args.Context} - } + contexts := searchContexts(aclObj, namespace, args.Context) 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) - + iter, err := getResourceIter(ctx, namespace, roundUUIDDownIfOdd(args.Prefix, args.Context), ws, state) if err != nil { e := err.Error() switch { diff --git a/nomad/search_endpoint_oss.go b/nomad/search_endpoint_oss.go index 8eb7b859e..43abf38f5 100644 --- a/nomad/search_endpoint_oss.go +++ b/nomad/search_endpoint_oss.go @@ -6,6 +6,7 @@ import ( "fmt" memdb "github.com/hashicorp/go-memdb" + "github.com/hashicorp/nomad/acl" "github.com/hashicorp/nomad/nomad/state" "github.com/hashicorp/nomad/nomad/structs" ) @@ -28,3 +29,69 @@ func getEnterpriseResourceIter(context structs.Context, namespace, prefix string // open source contexts. return nil, fmt.Errorf("context must be one of %v or 'all' for all contexts; got %q", allContexts, context) } + +// anySearchPerms returns true if the provided ACL has access to any +// capabilities required for prefix searching. Returns true if aclObj is nil. +func anySearchPerms(aclObj *acl.ACL, namespace string, context structs.Context) bool { + if aclObj == nil { + return true + } + + nodeRead := aclObj.AllowNodeRead() + jobRead := aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadJob) + if !nodeRead && !jobRead { + return false + } + + // 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 && context == structs.Nodes { + return false + } + if !jobRead { + switch context { + case structs.Allocs, structs.Deployments, structs.Evals, structs.Jobs: + return false + } + } + + return true +} + +// searchContexts returns the contexts the aclObj is valid for. If aclObj is +// nil all contexts are returned. +func searchContexts(aclObj *acl.ACL, namespace string, context structs.Context) []structs.Context { + var all []structs.Context + + switch context { + case structs.All: + all = make([]structs.Context, len(allContexts)) + copy(all, allContexts) + default: + all = []structs.Context{context} + } + + // If ACLs aren't enabled return all contexts + if aclObj == nil { + return all + } + + jobRead := aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadJob) + + // Filter contexts down to those the ACL grants access to + available := make([]structs.Context, 0, len(all)) + for _, c := range all { + switch c { + case structs.Allocs, structs.Jobs, structs.Evals, structs.Deployments: + if jobRead { + available = append(available, c) + } + case structs.Nodes: + if aclObj.AllowNodeRead() { + available = append(available, c) + } + } + } + return available +} diff --git a/nomad/search_endpoint_test.go b/nomad/search_endpoint_test.go index 84b6a050f..6f55bba10 100644 --- a/nomad/search_endpoint_test.go +++ b/nomad/search_endpoint_test.go @@ -136,10 +136,12 @@ func TestSearch_PrefixSearch_ACL(t *testing.T) { 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]) + // Index of job - not node - because node context is filtered out + assert.Equal(uint64(1000), resp.Index) + // Nodes filtered out since token only has access to namespace:read-job assert.Len(resp.Matches[structs.Nodes], 0) } @@ -153,10 +155,10 @@ func TestSearch_PrefixSearch_ACL(t *testing.T) { 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) + assert.Equal(uint64(1001), resp.Index) } // Try with a management token