diff --git a/CHANGELOG.md b/CHANGELOG.md index a07114b0d..5057e1ac9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ __BACKWARDS INCOMPATIBILITIES:__ BUG FIXES: * core: Fixed a bug where blocking queries would not include the query's maximum wait time when calculating whether it was safe to retry. [[GH-8921](https://github.com/hashicorp/nomad/issues/8921)] + * core: Fixed a bug where ACL handling prevented cross-namespace allocation listing [[GH-9278](https://github.com/hashicorp/nomad/issues/9278)] * config (Enterprise): Fixed default enterprise config merging. [[GH-9083](https://github.com/hashicorp/nomad/pull/9083)] * client: Fixed an issue with the Java fingerprinter on macOS causing pop-up notifications when no JVM installed. [[GH-9225](https://github.com/hashicorp/nomad/pull/9225)] * consul: Fixed a bug to correctly validate task when using script-checks in group-level services [[GH-8952](https://github.com/hashicorp/nomad/issues/8952)] diff --git a/nomad/alloc_endpoint.go b/nomad/alloc_endpoint.go index 0d6d7210c..3a32b5f19 100644 --- a/nomad/alloc_endpoint.go +++ b/nomad/alloc_endpoint.go @@ -29,6 +29,10 @@ func (a *Alloc) List(args *structs.AllocListRequest, reply *structs.AllocListRes } defer metrics.MeasureSince([]string{"nomad", "alloc", "list"}, time.Now()) + if args.RequestNamespace() == structs.AllNamespacesSentinel { + return a.listAllNamespaces(args, reply) + } + // Check namespace read-job permissions aclObj, err := a.srv.ResolveToken(args.AuthToken) if err != nil { @@ -37,10 +41,6 @@ func (a *Alloc) List(args *structs.AllocListRequest, reply *structs.AllocListRes return structs.ErrPermissionDenied } - allow := func(ns string) bool { - return aclObj.AllowNsOp(ns, acl.NamespaceCapabilityListJobs) - } - // Setup the blocking query opts := blockingOptions{ queryOpts: &args.QueryOptions, @@ -51,16 +51,7 @@ func (a *Alloc) List(args *structs.AllocListRequest, reply *structs.AllocListRes var iter memdb.ResultIterator prefix := args.QueryOptions.Prefix - if args.RequestNamespace() == structs.AllNamespacesSentinel { - allowedNSes, err := allowedNSes(aclObj, state, allow) - if err != nil { - return err - } - iter, err = state.AllocsByIDPrefixInNSes(ws, allowedNSes, prefix) - if err != nil { - return err - } - } else if prefix != "" { + if prefix != "" { iter, err = state.AllocsByIDPrefix(ws, args.RequestNamespace(), prefix) } else { iter, err = state.AllocsByNamespace(ws, args.RequestNamespace()) @@ -94,6 +85,68 @@ func (a *Alloc) List(args *structs.AllocListRequest, reply *structs.AllocListRes return a.srv.blockingRPC(&opts) } +// listAllNamespaces lists all allocations across all namespaces +func (a *Alloc) listAllNamespaces(args *structs.AllocListRequest, reply *structs.AllocListResponse) error { + // Check for read-job permissions + aclObj, err := a.srv.ResolveToken(args.AuthToken) + if err != nil { + return err + } + prefix := args.QueryOptions.Prefix + allow := func(ns string) bool { + return aclObj.AllowNsOp(ns, acl.NamespaceCapabilityReadJob) + } + + // Setup the blocking query + opts := blockingOptions{ + queryOpts: &args.QueryOptions, + queryMeta: &reply.QueryMeta, + run: func(ws memdb.WatchSet, state *state.StateStore) error { + // get list of accessible namespaces + allowedNSes, err := allowedNSes(aclObj, state, allow) + if err == structs.ErrPermissionDenied { + // return empty allocations if token isn't authorized for any + // namespace, matching other endpoints + reply.Allocations = []*structs.AllocListStub{} + } else if err != nil { + return err + } else { + var iter memdb.ResultIterator + var err error + if prefix != "" { + iter, err = state.AllocsByIDPrefixAllNSs(ws, prefix) + } else { + iter, err = state.Allocs(ws) + } + if err != nil { + return err + } + + var allocs []*structs.AllocListStub + for raw := iter.Next(); raw != nil; raw = iter.Next() { + alloc := raw.(*structs.Allocation) + if allowedNSes != nil && !allowedNSes[alloc.Namespace] { + continue + } + allocs = append(allocs, alloc.Stub(args.Fields)) + } + reply.Allocations = allocs + } + + // Use the last index that affected the jobs table + index, err := state.Index("allocs") + if err != nil { + return err + } + reply.Index = index + + // Set the query response + a.srv.setQueryMeta(&reply.QueryMeta) + return nil + }} + return a.srv.blockingRPC(&opts) +} + // GetAlloc is used to lookup a particular allocation func (a *Alloc) GetAlloc(args *structs.AllocSpecificRequest, reply *structs.SingleAllocResponse) error { diff --git a/nomad/state/state_store.go b/nomad/state/state_store.go index d5e5f12f9..e159a0db4 100644 --- a/nomad/state/state_store.go +++ b/nomad/state/state_store.go @@ -3329,35 +3329,17 @@ func allocNamespaceFilter(namespace string) func(interface{}) bool { } // AllocsByIDPrefix is used to lookup allocs by prefix -func (s *StateStore) AllocsByIDPrefixInNSes(ws memdb.WatchSet, namespaces map[string]bool, prefix string) (memdb.ResultIterator, error) { +func (s *StateStore) AllocsByIDPrefixAllNSs(ws memdb.WatchSet, prefix string) (memdb.ResultIterator, error) { txn := s.db.ReadTxn() - var iter memdb.ResultIterator - var err error - if prefix != "" { - iter, err = txn.Get("allocs", "id_prefix", prefix) - } else { - iter, err = txn.Get("allocs", "id") - - } + iter, err := txn.Get("allocs", "id_prefix", prefix) if err != nil { return nil, fmt.Errorf("alloc lookup failed: %v", err) } ws.Add(iter.WatchCh()) - // Wrap the iterator in a filter - nsesFilter := func(raw interface{}) bool { - alloc, ok := raw.(*structs.Allocation) - if !ok { - return true - } - - return namespaces[alloc.Namespace] - } - - wrap := memdb.NewFilterIterator(iter, nsesFilter) - return wrap, nil + return iter, nil } // AllocsByNode returns all the allocations by node