Merge pull request #3222 from hashicorp/f-acl-node
Node ACL enforcement
This commit is contained in:
commit
008b660513
|
@ -24,6 +24,11 @@ func NamespacePolicy(namespace string, policy string, capabilities []string) str
|
|||
return policyHCL
|
||||
}
|
||||
|
||||
// NodePolicy is a helper for generating the hcl for a given node policy.
|
||||
func NodePolicy(policy string) string {
|
||||
return fmt.Sprintf("node {\n\tpolicy = %q\n}\n", policy)
|
||||
}
|
||||
|
||||
// CreatePolicy creates a policy with the given name and rule.
|
||||
func CreatePolicy(t *testing.T, state *state.StateStore, index uint64, name, rule string) {
|
||||
t.Helper()
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/armon/go-metrics"
|
||||
"github.com/hashicorp/go-memdb"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/nomad/acl"
|
||||
"github.com/hashicorp/nomad/nomad/state"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/raft"
|
||||
|
@ -386,6 +387,13 @@ func (n *Node) UpdateDrain(args *structs.NodeUpdateDrainRequest,
|
|||
}
|
||||
defer metrics.MeasureSince([]string{"nomad", "client", "update_drain"}, time.Now())
|
||||
|
||||
// Check node write permissions
|
||||
if aclObj, err := n.srv.resolveToken(args.SecretID); err != nil {
|
||||
return err
|
||||
} else if aclObj != nil && !aclObj.AllowNodeWrite() {
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// Verify the arguments
|
||||
if args.NodeID == "" {
|
||||
return fmt.Errorf("missing node ID for drain update")
|
||||
|
@ -441,6 +449,13 @@ func (n *Node) Evaluate(args *structs.NodeEvaluateRequest, reply *structs.NodeUp
|
|||
}
|
||||
defer metrics.MeasureSince([]string{"nomad", "client", "evaluate"}, time.Now())
|
||||
|
||||
// Check node write permissions
|
||||
if aclObj, err := n.srv.resolveToken(args.SecretID); err != nil {
|
||||
return err
|
||||
} else if aclObj != nil && !aclObj.AllowNodeWrite() {
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// Verify the arguments
|
||||
if args.NodeID == "" {
|
||||
return fmt.Errorf("missing node ID for evaluation")
|
||||
|
@ -489,6 +504,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,
|
||||
|
@ -536,6 +558,36 @@ func (n *Node) GetAllocs(args *structs.NodeSpecificRequest,
|
|||
}
|
||||
defer metrics.MeasureSince([]string{"nomad", "client", "get_allocs"}, time.Now())
|
||||
|
||||
// Check node read and namespace job read permissions
|
||||
aclObj, err := n.srv.resolveToken(args.SecretID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if aclObj != nil && !aclObj.AllowNodeRead() {
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// cache namespace perms
|
||||
readableNamespaces := map[string]bool{}
|
||||
|
||||
// readNS is a caching namespace read-job helper
|
||||
readNS := func(ns string) bool {
|
||||
if aclObj == nil {
|
||||
// ACLs are disabled; everything is readable
|
||||
return true
|
||||
}
|
||||
|
||||
if readable, ok := readableNamespaces[ns]; ok {
|
||||
// cache hit
|
||||
return readable
|
||||
}
|
||||
|
||||
// cache miss
|
||||
readable := aclObj.AllowNsOp(ns, acl.NamespaceCapabilityReadJob)
|
||||
readableNamespaces[ns] = readable
|
||||
return readable
|
||||
}
|
||||
|
||||
// Verify the arguments
|
||||
if args.NodeID == "" {
|
||||
return fmt.Errorf("missing node ID")
|
||||
|
@ -553,9 +605,16 @@ func (n *Node) GetAllocs(args *structs.NodeSpecificRequest,
|
|||
}
|
||||
|
||||
// Setup the output
|
||||
if len(allocs) != 0 {
|
||||
reply.Allocs = allocs
|
||||
if n := len(allocs); n != 0 {
|
||||
reply.Allocs = make([]*structs.Allocation, 0, n)
|
||||
for _, alloc := range allocs {
|
||||
if readNS(alloc.Namespace) {
|
||||
reply.Allocs = append(reply.Allocs, alloc)
|
||||
}
|
||||
|
||||
// Get the max of all allocs since
|
||||
// subsequent requests need to start
|
||||
// from the latest index
|
||||
reply.Index = maxUint64(reply.Index, alloc.ModifyIndex)
|
||||
}
|
||||
} else {
|
||||
|
@ -756,6 +815,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,
|
||||
|
|
|
@ -9,10 +9,12 @@ import (
|
|||
|
||||
memdb "github.com/hashicorp/go-memdb"
|
||||
"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"
|
||||
vapi "github.com/hashicorp/vault/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestClientEndpoint_Register(t *testing.T) {
|
||||
|
@ -656,6 +658,61 @@ func TestClientEndpoint_UpdateDrain(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClientEndpoint_UpdateDrain_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 policy and tokens
|
||||
validToken := CreatePolicyAndToken(t, state, 1001, "test-valid", NodePolicy(acl.PolicyWrite))
|
||||
invalidToken := CreatePolicyAndToken(t, state, 1003, "test-invalid", NodePolicy(acl.PolicyRead))
|
||||
|
||||
// Update the status without a token and expect failure
|
||||
dereg := &structs.NodeUpdateDrainRequest{
|
||||
NodeID: node.ID,
|
||||
Drain: true,
|
||||
WriteRequest: structs.WriteRequest{Region: "global"},
|
||||
}
|
||||
{
|
||||
var resp structs.NodeDrainUpdateResponse
|
||||
err := msgpackrpc.CallWithCodec(codec, "Node.UpdateDrain", dereg, &resp)
|
||||
assert.NotNil(err, "RPC")
|
||||
assert.Equal(err.Error(), structs.ErrPermissionDenied.Error())
|
||||
}
|
||||
|
||||
// Try with a valid token
|
||||
dereg.SecretID = validToken.SecretID
|
||||
{
|
||||
var resp structs.NodeDrainUpdateResponse
|
||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Node.UpdateDrain", dereg, &resp), "RPC")
|
||||
}
|
||||
|
||||
// Try with a invalid token
|
||||
dereg.SecretID = invalidToken.SecretID
|
||||
{
|
||||
var resp structs.NodeDrainUpdateResponse
|
||||
err := msgpackrpc.CallWithCodec(codec, "Node.UpdateDrain", dereg, &resp)
|
||||
assert.NotNil(err, "RPC")
|
||||
assert.Equal(err.Error(), structs.ErrPermissionDenied.Error())
|
||||
}
|
||||
|
||||
// Try with a root token
|
||||
dereg.SecretID = root.SecretID
|
||||
{
|
||||
var resp structs.NodeDrainUpdateResponse
|
||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Node.UpdateDrain", dereg, &resp), "RPC")
|
||||
}
|
||||
}
|
||||
|
||||
// This test ensures that Nomad marks client state of allocations which are in
|
||||
// pending/running state to lost when a node is marked as down.
|
||||
func TestClientEndpoint_Drain_Down(t *testing.T) {
|
||||
|
@ -853,6 +910,61 @@ 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 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
|
||||
err := msgpackrpc.CallWithCodec(codec, "Node.GetNode", req, &resp)
|
||||
assert.NotNil(err, "RPC")
|
||||
assert.Equal(err.Error(), structs.ErrPermissionDenied.Error())
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
@ -1017,6 +1129,95 @@ func TestClientEndpoint_GetAllocs(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClientEndpoint_GetAllocs_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
|
||||
allocDefaultNS := mock.Alloc()
|
||||
allocAltNS := mock.Alloc()
|
||||
allocAltNS.Namespace = "altnamespace"
|
||||
allocOtherNS := mock.Alloc()
|
||||
allocOtherNS.Namespace = "should-only-be-displayed-for-root-ns"
|
||||
|
||||
node := mock.Node()
|
||||
allocDefaultNS.NodeID = node.ID
|
||||
allocAltNS.NodeID = node.ID
|
||||
allocOtherNS.NodeID = node.ID
|
||||
state := s1.fsm.State()
|
||||
assert.Nil(state.UpsertNode(1, node), "UpsertNode")
|
||||
assert.Nil(state.UpsertJobSummary(2, mock.JobSummary(allocDefaultNS.JobID)), "UpsertJobSummary")
|
||||
assert.Nil(state.UpsertJobSummary(3, mock.JobSummary(allocAltNS.JobID)), "UpsertJobSummary")
|
||||
assert.Nil(state.UpsertJobSummary(4, mock.JobSummary(allocOtherNS.JobID)), "UpsertJobSummary")
|
||||
allocs := []*structs.Allocation{allocDefaultNS, allocAltNS, allocOtherNS}
|
||||
assert.Nil(state.UpsertAllocs(5, allocs), "UpsertAllocs")
|
||||
|
||||
// Create the namespace policy and tokens
|
||||
validDefaultToken := CreatePolicyAndToken(t, state, 1001, "test-default-valid", NodePolicy(acl.PolicyRead)+
|
||||
NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}))
|
||||
validNoNSToken := CreatePolicyAndToken(t, state, 1003, "test-alt-valid", NodePolicy(acl.PolicyRead))
|
||||
invalidToken := CreatePolicyAndToken(t, state, 1004, "test-invalid",
|
||||
NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}))
|
||||
|
||||
// Lookup the node without a token and expect failure
|
||||
req := &structs.NodeSpecificRequest{
|
||||
NodeID: node.ID,
|
||||
QueryOptions: structs.QueryOptions{Region: "global"},
|
||||
}
|
||||
{
|
||||
var resp structs.NodeAllocsResponse
|
||||
err := msgpackrpc.CallWithCodec(codec, "Node.GetAllocs", req, &resp)
|
||||
assert.NotNil(err, "RPC")
|
||||
assert.Equal(err.Error(), structs.ErrPermissionDenied.Error())
|
||||
}
|
||||
|
||||
// Try with a valid token for the default namespace
|
||||
req.SecretID = validDefaultToken.SecretID
|
||||
{
|
||||
var resp structs.NodeAllocsResponse
|
||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Node.GetAllocs", req, &resp), "RPC")
|
||||
assert.Len(resp.Allocs, 1)
|
||||
assert.Equal(allocDefaultNS.ID, resp.Allocs[0].ID)
|
||||
}
|
||||
|
||||
// Try with a valid token for a namespace with no allocs on this node
|
||||
req.SecretID = validNoNSToken.SecretID
|
||||
{
|
||||
var resp structs.NodeAllocsResponse
|
||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Node.GetAllocs", req, &resp), "RPC")
|
||||
assert.Len(resp.Allocs, 0)
|
||||
}
|
||||
|
||||
// Try with a invalid token
|
||||
req.SecretID = invalidToken.SecretID
|
||||
{
|
||||
var resp structs.NodeAllocsResponse
|
||||
err := msgpackrpc.CallWithCodec(codec, "Node.GetAllocs", 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.NodeAllocsResponse
|
||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Node.GetAllocs", req, &resp), "RPC")
|
||||
assert.Len(resp.Allocs, 3)
|
||||
for _, alloc := range resp.Allocs {
|
||||
switch alloc.ID {
|
||||
case allocDefaultNS.ID, allocAltNS.ID, allocOtherNS.ID:
|
||||
// expected
|
||||
default:
|
||||
t.Errorf("unexpected alloc %q for namespace %q", alloc.ID, alloc.Namespace)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientEndpoint_GetClientAllocs(t *testing.T) {
|
||||
t.Parallel()
|
||||
s1 := testServer(t, nil)
|
||||
|
@ -1640,6 +1841,64 @@ func TestClientEndpoint_Evaluate(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClientEndpoint_Evaluate_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 with an alloc
|
||||
alloc := mock.Alloc()
|
||||
node := mock.Node()
|
||||
node.ID = alloc.NodeID
|
||||
state := s1.fsm.State()
|
||||
|
||||
assert.Nil(state.UpsertNode(1, node), "UpsertNode")
|
||||
assert.Nil(state.UpsertJobSummary(2, mock.JobSummary(alloc.JobID)), "UpsertJobSummary")
|
||||
assert.Nil(state.UpsertAllocs(3, []*structs.Allocation{alloc}), "UpsertAllocs")
|
||||
|
||||
// Create the policy and tokens
|
||||
validToken := CreatePolicyAndToken(t, state, 1001, "test-valid", NodePolicy(acl.PolicyWrite))
|
||||
invalidToken := CreatePolicyAndToken(t, state, 1003, "test-invalid", NodePolicy(acl.PolicyRead))
|
||||
|
||||
// Re-evaluate without a token and expect failure
|
||||
req := &structs.NodeEvaluateRequest{
|
||||
NodeID: alloc.NodeID,
|
||||
WriteRequest: structs.WriteRequest{Region: "global"},
|
||||
}
|
||||
{
|
||||
var resp structs.NodeUpdateResponse
|
||||
err := msgpackrpc.CallWithCodec(codec, "Node.Evaluate", req, &resp)
|
||||
assert.NotNil(err, "RPC")
|
||||
assert.Equal(err.Error(), structs.ErrPermissionDenied.Error())
|
||||
}
|
||||
|
||||
// Try with a valid token
|
||||
req.SecretID = validToken.SecretID
|
||||
{
|
||||
var resp structs.NodeUpdateResponse
|
||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Node.Evaluate", req, &resp), "RPC")
|
||||
}
|
||||
|
||||
// Try with a invalid token
|
||||
req.SecretID = invalidToken.SecretID
|
||||
{
|
||||
var resp structs.NodeUpdateResponse
|
||||
err := msgpackrpc.CallWithCodec(codec, "Node.Evaluate", 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.NodeUpdateResponse
|
||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Node.Evaluate", req, &resp), "RPC")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientEndpoint_ListNodes(t *testing.T) {
|
||||
t.Parallel()
|
||||
s1 := testServer(t, nil)
|
||||
|
@ -1701,6 +1960,60 @@ 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
|
||||
err := msgpackrpc.CallWithCodec(codec, "Node.List", req, &resp)
|
||||
assert.NotNil(err, "RPC")
|
||||
assert.Equal(err.Error(), structs.ErrPermissionDenied.Error())
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
|
|
@ -24,7 +24,7 @@ The table below shows this endpoint's support for
|
|||
|
||||
| Blocking Queries | ACL Required |
|
||||
| ---------------- | ------------ |
|
||||
| `YES` | `none` |
|
||||
| `YES` | `node:read` |
|
||||
|
||||
### Parameters
|
||||
|
||||
|
@ -74,8 +74,8 @@ The table below shows this endpoint's support for
|
|||
[required ACLs](/api/index.html#acls).
|
||||
|
||||
| Blocking Queries | ACL Required |
|
||||
| ---------------- | ------------ |
|
||||
| `YES` | `none` |
|
||||
| ---------------- | ----------------- |
|
||||
| `YES` | `node:read` |
|
||||
|
||||
### Parameters
|
||||
|
||||
|
@ -176,8 +176,8 @@ The table below shows this endpoint's support for
|
|||
[required ACLs](/api/index.html#acls).
|
||||
|
||||
| Blocking Queries | ACL Required |
|
||||
| ---------------- | ------------ |
|
||||
| `YES` | `none` |
|
||||
| ---------------- | ------------------------------ |
|
||||
| `YES` | `node:read,namespace:read-job` |
|
||||
|
||||
### Parameters
|
||||
|
||||
|
@ -524,8 +524,8 @@ The table below shows this endpoint's support for
|
|||
[required ACLs](/api/index.html#acls).
|
||||
|
||||
| Blocking Queries | ACL Required |
|
||||
| ---------------- | ------------ |
|
||||
| `NO` | `none` |
|
||||
| ---------------- | ------------------ |
|
||||
| `NO` | `node:write` |
|
||||
|
||||
### Parameters
|
||||
|
||||
|
@ -581,8 +581,8 @@ The table below shows this endpoint's support for
|
|||
[required ACLs](/api/index.html#acls).
|
||||
|
||||
| Blocking Queries | ACL Required |
|
||||
| ---------------- | ------------ |
|
||||
| `NO` | `none` |
|
||||
| ---------------- | ------------------ |
|
||||
| `NO` | `node:write` |
|
||||
|
||||
### Parameters
|
||||
|
||||
|
|
Loading…
Reference in a new issue