diff --git a/nomad/node_endpoint.go b/nomad/node_endpoint.go index 30f7f4972..051be49d0 100644 --- a/nomad/node_endpoint.go +++ b/nomad/node_endpoint.go @@ -790,6 +790,13 @@ func (n *Node) List(args *structs.NodeListRequest, } defer metrics.MeasureSince([]string{"nomad", "client", "list"}, time.Now()) + // Check node read permissions + if aclObj, err := n.srv.resolveToken(args.SecretID); err != nil { + return err + } else if aclObj != nil && !aclObj.AllowNodeRead() { + return structs.ErrPermissionDenied + } + // Setup the blocking query opts := blockingOptions{ queryOpts: &args.QueryOptions, diff --git a/nomad/node_endpoint_test.go b/nomad/node_endpoint_test.go index dad2777f7..23242ccbc 100644 --- a/nomad/node_endpoint_test.go +++ b/nomad/node_endpoint_test.go @@ -1923,6 +1923,58 @@ func TestClientEndpoint_ListNodes(t *testing.T) { } } +func TestClientEndpoint_ListNodes_ACL(t *testing.T) { + t.Parallel() + s1, root := testACLServer(t, nil) + defer s1.Shutdown() + codec := rpcClient(t, s1) + testutil.WaitForLeader(t, s1.RPC) + assert := assert.New(t) + + // Create the node + node := mock.Node() + state := s1.fsm.State() + assert.Nil(state.UpsertNode(1, node), "UpsertNode") + + // Create the namespace policy and tokens + validToken := CreatePolicyAndToken(t, state, 1001, "test-valid", NodePolicy(acl.PolicyRead)) + invalidToken := CreatePolicyAndToken(t, state, 1003, "test-invalid", NodePolicy(acl.PolicyDeny)) + + // Lookup the node without a token and expect failure + req := &structs.NodeListRequest{ + QueryOptions: structs.QueryOptions{Region: "global"}, + } + { + var resp structs.NodeListResponse + assert.NotNil(msgpackrpc.CallWithCodec(codec, "Node.List", req, &resp), "RPC") + } + + // Try with a valid token + req.SecretID = validToken.SecretID + { + var resp structs.NodeListResponse + assert.Nil(msgpackrpc.CallWithCodec(codec, "Node.List", req, &resp), "RPC") + assert.Equal(node.ID, resp.Nodes[0].ID) + } + + // Try with a invalid token + req.SecretID = invalidToken.SecretID + { + var resp structs.NodeListResponse + err := msgpackrpc.CallWithCodec(codec, "Node.List", req, &resp) + assert.NotNil(err, "RPC") + assert.Equal(err.Error(), structs.ErrPermissionDenied.Error()) + } + + // Try with a root token + req.SecretID = root.SecretID + { + var resp structs.NodeListResponse + assert.Nil(msgpackrpc.CallWithCodec(codec, "Node.List", req, &resp), "RPC") + assert.Equal(node.ID, resp.Nodes[0].ID) + } +} + func TestClientEndpoint_ListNodes_Blocking(t *testing.T) { t.Parallel() s1 := testServer(t, nil) diff --git a/website/source/api/nodes.html.md b/website/source/api/nodes.html.md index 7352f0d37..7527d1097 100644 --- a/website/source/api/nodes.html.md +++ b/website/source/api/nodes.html.md @@ -24,7 +24,7 @@ The table below shows this endpoint's support for | Blocking Queries | ACL Required | | ---------------- | ------------ | -| `YES` | `none` | +| `YES` | `node:read` | ### Parameters