diff --git a/nomad/alloc_endpoint.go b/nomad/alloc_endpoint.go index cbd0d5dd9..871d7733b 100644 --- a/nomad/alloc_endpoint.go +++ b/nomad/alloc_endpoint.go @@ -79,6 +79,13 @@ func (a *Alloc) GetAlloc(args *structs.AllocSpecificRequest, } defer metrics.MeasureSince([]string{"nomad", "alloc", "get_alloc"}, time.Now()) + // Check namespace read-job permissions + if aclObj, err := a.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/alloc_endpoint_test.go b/nomad/alloc_endpoint_test.go index 52334f62e..09841d0aa 100644 --- a/nomad/alloc_endpoint_test.go +++ b/nomad/alloc_endpoint_test.go @@ -226,7 +226,7 @@ func TestAllocEndpoint_GetAlloc(t *testing.T) { t.Fatalf("err: %v", err) } - // Lookup the jobs + // Lookup the alloc get := &structs.AllocSpecificRequest{ AllocID: alloc.ID, QueryOptions: structs.QueryOptions{Region: "global"}, @@ -244,6 +244,57 @@ func TestAllocEndpoint_GetAlloc(t *testing.T) { } } +func TestAllocEndpoint_GetAlloc_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 alloc + alloc := mock.Alloc() + allocs := []*structs.Allocation{alloc} + summary := mock.JobSummary(alloc.JobID) + state := s1.fsm.State() + + assert.Nil(state.UpsertJobSummary(999, summary), "UpsertJobSummary") + assert.Nil(state.UpsertAllocs(1000, allocs), "UpsertAllocs") + + // 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})) + + // Lookup the alloc without a token and expect failure + get := &structs.AllocSpecificRequest{ + AllocID: alloc.ID, + QueryOptions: structs.QueryOptions{Region: "global"}, + } + var resp structs.SingleAllocResponse + assert.NotNil(msgpackrpc.CallWithCodec(codec, "Alloc.GetAlloc", get, &resp), "RPC") + + // Try with a valid token + get.SecretID = validToken.SecretID + assert.Nil(msgpackrpc.CallWithCodec(codec, "Alloc.GetAlloc", get, &resp), "RPC") + assert.EqualValues(resp.Index, 1000, "resp.Index") + assert.Equal(alloc, resp.Alloc, "Returned alloc not equal") + + // Try with a invalid token + get.SecretID = invalidToken.SecretID + err := msgpackrpc.CallWithCodec(codec, "Alloc.GetAlloc", get, &resp) + assert.NotNil(err, "RPC") + assert.Equal(err.Error(), structs.ErrPermissionDenied.Error()) + + // Try with a root token + get.SecretID = root.SecretID + var resp2 structs.SingleAllocResponse + assert.Nil(msgpackrpc.CallWithCodec(codec, "Alloc.GetAlloc", get, &resp2), "RPC") + assert.EqualValues(resp2.Index, 1000, "resp.Index") + assert.Equal(alloc, resp2.Alloc, "Returned alloc not equal") +} + func TestAllocEndpoint_GetAlloc_Blocking(t *testing.T) { t.Parallel() s1 := testServer(t, nil) diff --git a/website/source/api/allocations.html.md b/website/source/api/allocations.html.md index dfee5b223..ab6a97511 100644 --- a/website/source/api/allocations.html.md +++ b/website/source/api/allocations.html.md @@ -158,9 +158,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