From 2c53e735f9c6e5d88f47b092b36fc6f0111bec1c Mon Sep 17 00:00:00 2001 From: Tim Gross Date: Thu, 27 Jun 2024 13:34:46 -0400 Subject: [PATCH] job statuses: fix filtering for namespace parameter The job statuses endpoint does not filter jobs by the namespace query parameter unless the user passes a management token. The RPC handler creates a filter based on all the allowed namespaces but improperly conditions reducing this down to only the requested set on there being a management token. Note this does not give the user access to jobs they shouldn't have, only ignores the parameter. Remove the RPC handler's extra condition that prevents using the requested namespace. This is safe because we specifically check the ACL for that namespace earlier in the handler. Fixes: https://github.com/hashicorp/nomad/issues/23370 --- .changelog/23456.txt | 3 +++ nomad/job_endpoint_statuses.go | 10 ++++------ nomad/job_endpoint_statuses_test.go | 22 +++++++++++++++++----- 3 files changed, 24 insertions(+), 11 deletions(-) create mode 100644 .changelog/23456.txt diff --git a/.changelog/23456.txt b/.changelog/23456.txt new file mode 100644 index 00000000000..75f9b107e0c --- /dev/null +++ b/.changelog/23456.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: Fixed support for namespace parameter on job statuses API +``` diff --git a/nomad/job_endpoint_statuses.go b/nomad/job_endpoint_statuses.go index 7b54c2b831f..d55ae7c366b 100644 --- a/nomad/job_endpoint_statuses.go +++ b/nomad/job_endpoint_statuses.go @@ -70,12 +70,10 @@ func (j *Job) Statuses( } else if err != nil { return err } - // since the state index we're using doesn't include namespace, - // explicitly add the user-provided ns to our filter if needed. - // (allowableNamespaces will be nil if the caller sent a mgmt token) - if allowableNamespaces == nil && - namespace != "" && - namespace != structs.AllNamespacesSentinel { + // since the state index we're using doesn't include namespace, explicitly + // set the user-provided ns to our filter if needed. we've already verified + // that the user has access to the specific namespace above + if namespace != "" && namespace != structs.AllNamespacesSentinel { allowableNamespaces = map[string]bool{ namespace: true, } diff --git a/nomad/job_endpoint_statuses_test.go b/nomad/job_endpoint_statuses_test.go index c5b3367d832..af43f34d241 100644 --- a/nomad/job_endpoint_statuses_test.go +++ b/nomad/job_endpoint_statuses_test.go @@ -21,22 +21,32 @@ func TestJob_Statuses_ACL(t *testing.T) { t.Cleanup(cleanup) testutil.WaitForLeader(t, s.RPC) + job1 := mock.MinJob() + job2 := mock.MinJob() + job2.Namespace = "infra" + must.NoError(t, s.State().UpsertNamespaces(100, []*structs.Namespace{{Name: "infra"}})) + must.NoError(t, s.State().UpsertJob(structs.MsgTypeTestSetup, 101, nil, job1)) + must.NoError(t, s.State().UpsertJob(structs.MsgTypeTestSetup, 102, nil, job2)) + insufficientToken := mock.CreatePolicyAndToken(t, s.State(), 1, "job-lister", mock.NamespacePolicy("default", "", []string{"list-jobs"})) happyToken := mock.CreatePolicyAndToken(t, s.State(), 2, "job-reader", - mock.NamespacePolicy("default", "", []string{"read-job"})) + mock.NamespacePolicy("*", "", []string{"read-job"})) for _, tc := range []struct { - name, token, err string + name, token, err, ns string + expectJobs int }{ - {"no token", "", "Permission denied"}, - {"insufficient perms", insufficientToken.SecretID, "Permission denied"}, - {"happy token", happyToken.SecretID, ""}, + {"no token", "", "Permission denied", "", 0}, + {"insufficient perms", insufficientToken.SecretID, "Permission denied", "", 0}, + {"happy token specific ns", happyToken.SecretID, "", "infra", 1}, + {"happy token wildcard ns", happyToken.SecretID, "", "*", 2}, } { t.Run(tc.name, func(t *testing.T) { req := &structs.JobStatusesRequest{} req.QueryOptions.Region = "global" req.QueryOptions.AuthToken = tc.token + req.QueryOptions.Namespace = tc.ns var resp structs.JobStatusesResponse err := s.RPC("Job.Statuses", &req, &resp) @@ -45,6 +55,8 @@ func TestJob_Statuses_ACL(t *testing.T) { must.ErrorContains(t, err, tc.err) } else { must.NoError(t, err) + must.Len(t, tc.expectJobs, resp.Jobs, + must.Sprint("expected jobs to be filtered by namespace")) } }) }