job statuses: fix filtering for namespace parameter (#23456)

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
This commit is contained in:
Tim Gross
2024-06-27 16:19:36 -04:00
committed by GitHub
parent 863d42bc4b
commit 7d3ce7e92d
3 changed files with 24 additions and 11 deletions

3
.changelog/23456.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:bug
ui: Fixed support for namespace parameter on job statuses API
```

View File

@@ -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,
}

View File

@@ -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"))
}
})
}