diff --git a/nomad/node_endpoint.go b/nomad/node_endpoint.go index a34d1cccf..565ea8c3b 100644 --- a/nomad/node_endpoint.go +++ b/nomad/node_endpoint.go @@ -503,6 +503,13 @@ func (n *Node) GetNode(args *structs.NodeSpecificRequest, } defer metrics.MeasureSince([]string{"nomad", "client", "get_node"}, 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 bf5847724..a7c474a91 100644 --- a/nomad/node_endpoint_test.go +++ b/nomad/node_endpoint_test.go @@ -666,7 +666,7 @@ func TestClientEndpoint_UpdateDrain_ACL(t *testing.T) { testutil.WaitForLeader(t, s1.RPC) assert := assert.New(t) - // Create the alloc + // Create the node node := mock.Node() state := s1.fsm.State() @@ -908,6 +908,59 @@ func TestClientEndpoint_GetNode(t *testing.T) { } } +func TestClientEndpoint_GetNode_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.NodeSpecificRequest{ + NodeID: node.ID, + QueryOptions: structs.QueryOptions{Region: "global"}, + } + { + var resp structs.SingleNodeResponse + assert.NotNil(msgpackrpc.CallWithCodec(codec, "Node.GetNode", req, &resp), "RPC") + } + + // Try with a valid token + req.SecretID = validToken.SecretID + { + var resp structs.SingleNodeResponse + assert.Nil(msgpackrpc.CallWithCodec(codec, "Node.GetNode", req, &resp), "RPC") + assert.Equal(node.ID, resp.Node.ID) + } + + // Try with a invalid token + req.SecretID = invalidToken.SecretID + { + var resp structs.SingleNodeResponse + err := msgpackrpc.CallWithCodec(codec, "Node.GetNode", 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.SingleNodeResponse + assert.Nil(msgpackrpc.CallWithCodec(codec, "Node.GetNode", req, &resp), "RPC") + assert.Equal(node.ID, resp.Node.ID) + } +} + func TestClientEndpoint_GetNode_Blocking(t *testing.T) { t.Parallel() s1 := testServer(t, nil) @@ -1703,7 +1756,7 @@ func TestClientEndpoint_Evaluate_ACL(t *testing.T) { testutil.WaitForLeader(t, s1.RPC) assert := assert.New(t) - // Create the alloc + // Create the node with an alloc alloc := mock.Alloc() node := mock.Node() node.ID = alloc.NodeID diff --git a/website/source/api/nodes.html.md b/website/source/api/nodes.html.md index b0ef03aab..c3aed5549 100644 --- a/website/source/api/nodes.html.md +++ b/website/source/api/nodes.html.md @@ -73,9 +73,9 @@ 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 | -| ---------------- | ------------ | -| `YES` | `none` | +| Blocking Queries | ACL Required | +| ---------------- | ----------------- | +| `YES` | `node:read` | ### Parameters