diff --git a/nomad/deployment_endpoint.go b/nomad/deployment_endpoint.go index 3b57712bc..94e254255 100644 --- a/nomad/deployment_endpoint.go +++ b/nomad/deployment_endpoint.go @@ -251,6 +251,13 @@ func (d *Deployment) List(args *structs.DeploymentListRequest, reply *structs.De } defer metrics.MeasureSince([]string{"nomad", "deployment", "list"}, time.Now()) + // Check namespace read-job permissions + if aclObj, err := d.srv.resolveToken(args.SecretID); err != nil { + return err + } else if aclObj != nil && !aclObj.AllowNsOp(args.RequestNamespace(), acl.NamespaceCapabilityReadJob) { + return structs.ErrPermissionDenied + } + // Setup the blocking query opts := blockingOptions{ queryOpts: &args.QueryOptions, diff --git a/nomad/deployment_endpoint_test.go b/nomad/deployment_endpoint_test.go index 0ad5508f2..b32e9a250 100644 --- a/nomad/deployment_endpoint_test.go +++ b/nomad/deployment_endpoint_test.go @@ -903,6 +903,74 @@ func TestDeploymentEndpoint_List(t *testing.T) { assert.Equal(resp2.Deployments[0].ID, d.ID, "Deployment ID") } +func TestDeploymentEndpoint_List_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 register request + j := mock.Job() + d := mock.Deployment() + d.JobID = j.ID + state := s1.fsm.State() + + assert.Nil(state.UpsertJob(999, j), "UpsertJob") + assert.Nil(state.UpsertDeployment(1000, d), "UpsertDeployment") + + // Create the namespace policy and tokens + validToken := CreatePolicyAndToken(t, state, 1001, "test-valid", + NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob})) + invalidToken := CreatePolicyAndToken(t, state, 1003, "test-invalid", + NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListJobs})) + + get := &structs.DeploymentListRequest{ + QueryOptions: structs.QueryOptions{ + Region: "global", + Namespace: structs.DefaultNamespace, + }, + } + + // Try with no token and expect permission denied + { + var resp structs.DeploymentUpdateResponse + err := msgpackrpc.CallWithCodec(codec, "Deployment.List", get, &resp) + assert.NotNil(err) + assert.Equal(err.Error(), structs.ErrPermissionDenied.Error()) + } + + // Try with an invalid token + { + get.SecretID = invalidToken.SecretID + var resp structs.DeploymentUpdateResponse + err := msgpackrpc.CallWithCodec(codec, "Deployment.List", get, &resp) + assert.NotNil(err) + assert.Equal(err.Error(), structs.ErrPermissionDenied.Error()) + } + + // Lookup the deployments with a root token + { + get.SecretID = root.SecretID + var resp structs.DeploymentListResponse + assert.Nil(msgpackrpc.CallWithCodec(codec, "Deployment.List", get, &resp), "RPC") + assert.EqualValues(resp.Index, 1000, "Wrong Index") + assert.Len(resp.Deployments, 1, "Deployments") + assert.Equal(resp.Deployments[0].ID, d.ID, "Deployment ID") + } + + // Lookup the deployments with a valid token + { + get.SecretID = validToken.SecretID + var resp structs.DeploymentListResponse + assert.Nil(msgpackrpc.CallWithCodec(codec, "Deployment.List", get, &resp), "RPC") + assert.EqualValues(resp.Index, 1000, "Wrong Index") + assert.Len(resp.Deployments, 1, "Deployments") + assert.Equal(resp.Deployments[0].ID, d.ID, "Deployment ID") + } +} + func TestDeploymentEndpoint_List_Blocking(t *testing.T) { t.Parallel() s1 := testServer(t, nil) diff --git a/website/source/api/deployments.html.md b/website/source/api/deployments.html.md index 389328358..d5d5c6eff 100644 --- a/website/source/api/deployments.html.md +++ b/website/source/api/deployments.html.md @@ -22,9 +22,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` | `namespace:read-job` | ### Parameters