Made counts respect the access group of users - #2463
Open
jessevz wants to merge 8 commits into
Open
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR scopes APIv2 count-related endpoints to the requesting user’s access groups (aligning /count, meta.total_count, and getCompletedCount with list endpoint ACL behavior) and fixes /count filter mapping so invalid/unresolvable filters fail loudly instead of being silently ignored.
Changes:
- Apply
getFilterACL()(filters + joins) to/api/v2/ui/<model>/countand tometa.total_count. - Fix
/countfilter resolution to use aliased fields (incl.id), support__in/__nin, and reject unknown/unmappable filters with 403. - Scope
getCompletedCountto access groups viaHashlistjoin +ContainFilter, and extend CI coverage for ACL-scoped counts.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/inc/startup/setup.php | Disables migration execution on startup (unrelated to count ACL; high impact). |
| src/inc/apiv2/openapi/ModelApiPathBuilder.php | Updates OpenAPI text to clarify total_count is ACL-scoped. |
| src/inc/apiv2/openapi/JsonApiFragments.php | Updates OpenAPI schema descriptions to reflect ACL-scoped counts. |
| src/inc/apiv2/helper/GetCompletedCountHelperAPI.php | Adds access-group scoping to completed task/supertask counts via Hashlist ACL join/filter. |
| src/inc/apiv2/common/AbstractModelAPI.php | Fixes filter mapping and applies ACL scoping to /count and total_count. |
| src/dba/AbstractModelFactory.php | Allows extra joins in joinAggregationFilter() via Factory::JOIN. |
| ci/apiv2/utils.py | Adds _test_acl_count() helper asserting ACL-scoped count and total_count. |
| ci/apiv2/test_taskwrapper.py | Extends ACL test to cover count. |
| ci/apiv2/test_task.py | Extends ACL test to cover count. |
| ci/apiv2/test_realworld_dump.py | Updates expected dump counts to match ACL-scoped behavior. |
| ci/apiv2/test_hashlist.py | Extends ACL test to cover count. |
| ci/apiv2/test_file.py | Extends ACL test to cover count. |
| ci/apiv2/test_count.py | Adds tests for id, aliased-field filtering, and rejection of unknown/column-name filters. |
| ci/apiv2/test_completed_count.py | Adds regression test ensuring getCompletedCount is ACL-scoped. |
| ci/apiv2/test_apitoken.py | Extends ACL test to cover count. |
| ci/apiv2/test_agentstat.py | Extends ACL test to cover count. |
| ci/apiv2/test_agentassignment.py | Extends ACL test to cover count. |
| ci/apiv2/test_agent.py | Extends ACL test to cover count. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
jessevz
marked this pull request as draft
August 27, 2026 06:26
added 2 commits
August 27, 2026 08:32
jessevz
marked this pull request as ready for review
August 27, 2026 15:02
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Scope APIv2 count endpoints to the access groups of the user
closes #2461
Problem
Counting in APIv2 ignored access groups entirely.
GET /api/v2/ui/<model>/countandGET /api/v2/helper/getCompletedCountreported numbers covering the whole installation,while the matching list endpoints correctly returned only what the user may see.
The visible symptom was a dashboard reporting 7 completed tasks next to 1 total task:
the task list was ACL-filtered, the completed counter was not.
While fixing this a second defect surfaced in the same code path:
/countsilently droppedfilters it could not resolve, answering with an unfiltered total instead of an error.
Changes
1. Apply the ACL to
/count(AbstractModelAPI::count())getManyResources()mergesgetFilterACL()into its query,count()never called it.The ACL filters and joins are now applied the same way, reusing
checkJoinExists()so an ACLjoin is not duplicated when the same relation was already joined through
expand.All 15 model APIs that override
getFilterACL()were affected:Agent,AgentAssignment,AgentError,AgentStat,ApiToken,Chunk,File,Hash,Hashlist,HealthCheckAgent,Speed,Task,TaskWrapperandTaskWrapperDisplay.meta.total_countwas a second instance of the same leak (countFilter([]), no filters at all)and is now counted within the ACL as well — "without any filter applied" means without the
request's filters, not without the user's access groups.
2. Fix filter resolution on
/count(AbstractModelAPI::filterObjectMap())The method matched filter keys against the raw DBA features, which are keyed by database column,
while
makeFilter()— which actually builds the query — works with aliases:filter[id]never resolved. The_idspecial case looked uparray_column($features, 'alias', 'dbname')on features that carry nodbnamekey, which emitted anUndefined array keywarning(corrupting the JSON body) and produced
null._idis only ever a response key, never a filterinput, so the case is replaced by
id, matching the list endpoint.Hashlist.hashlistName→name,User.username→name,User.rightGroupId→globalPermissionGroupId,RightGroup.groupName→nameandTask/TaskWrapperDisplaypreprocessorId→usePreprocessor.raise
HttpForbidden, like listing does.__in/__ninso it stays in sync withmakeFilter(); both previouslyreturned 403 on
/countwhile working on the list endpoint.3. Apply the ACL to
getCompletedCountThe helper validated permissions (
permTaskWrapperRead,permTaskRead) but never access groups.Both counts are now restricted with the same join to
HashlistandContainFilteronHashlist::ACCESS_GROUP_IDthatTaskWrapperDisplayAPIandTaskWrapperAPIuse, so the dashboardfigure and the task list derive from one rule. The supertask query inherits the restriction through
the already ACL-filtered
$taskWrapperIds.TaskWrapperDisplayexposes its ownaccessGroupId, but that is the task wrapper's group while thelist endpoints key off the hashlist's group. Joining
Hashlistkeeps the helper consistent with them.4. Allow extra joins in
AbstractModelFactory::joinAggregationFilter()The method ignored
Factory::JOINin its options, unlikefilter()andcountFilter(), leaving no wayto add the ACL join above. It now applies them. Purely additive: the only callers are the helper above
and
AbstractModelFactoryTest, none of which passedJOINbefore. The result is grouped by the primarykeys of the factory, so such joins must not multiply rows — noted in the docblock.
Behaviour changes
They now agree with the corresponding list endpoint.
/countreturns 403 for a filter it cannot resolve, where it previously returned an unfilteredcount. Clients relying on a filter that silently did nothing will see an error instead of a wrong
number. Dotted relation filters (
task.taskName) fall into this group: the list endpoint resolvesraise
HttpForbidden, like listing does.__in/__ninso it stays in sync withmakeFilter(); both previouslyreturned 403 on
/countwhile working on the list endpoint.3. Apply the ACL to
getCompletedCountThe helper validated permissions (
permTaskWrapperRead,permTaskRead) but never access groups.Both counts are now restricted with the same join to
HashlistandContainFilteronHashlist::ACCESS_GROUP_IDthatTaskWrapperDisplayAPIandTaskWrapperAPIuse, so the dashboardfigure and the task list derive from one rule. The supertask query inherits the restriction through
the already ACL-filtered
$taskWrapperIds.TaskWrapperDisplayexposes its ownaccessGroupId, but that is the task wrapper's group while thelist endpoints key off the hashlist's group. Joining
Hashlistkeeps the helper consistent with them.4. Allow extra joins in
AbstractModelFactory::joinAggregationFilter()The method ignored
Factory::JOINin its options, unlikefilter()andcountFilter(), leaving no wayto add the ACL join above. It now applies them. Purely additive: the only callers are the helper above
and
AbstractModelFactoryTest, none of which passedJOINbefore. The result is grouped by the primarykeys of the factory, so such joins must not multiply rows — noted in the docblock.
Behaviour changes
They now agree with the corresponding list endpoint.
/countreturns 403 for a filter it cannot resolve, where it previously returned an unfilteredcount. Clients relying on a filter that silently did nothing will see an error instead of a wrong
number. Dotted relation filters (
task.taskName) fall into this group: the list endpoint resolvesthem,
/counthas never supported them, and they now fail loudly rather than being ignored.filter[format__nin]=3and friends now work on/count.Updated test expectations
test_realworld_dump.py::test_known_dump_countshad three values baked in from the unfilteredbehaviour. In the dump,
admin(userId 1) is only a member of the default group while hashlist 272lives in access group 5, so that hashlist plus its 5477 hashes and 10 tasks are no longer counted:
Hash.count(hashId__lte=30129)Hashlist.count(hashlistId__lte=272)Task.count(taskId__lte=1336)The new values match what
admincan already list today. The other counts and every list andpagination assertion in that file are unchanged.
Tests
_test_acl_count()added toci/apiv2/utils.pynext to_test_acl_list(), asserting a user with noaccess groups counts 0 in both
countandtotal_countwhile an admin counts more than 0. Wired intothe eight existing
test_aclcases: agent, agentassignment, agentstat, apitoken, file, hashlist, taskand taskwrapper.
test_count.pycovers theidkey, aliased fields, rejection of the underlying column name andrejection of an unknown filter.
test_completed_count.pygainstest_acl_counts_are_scoped_to_access_groups. Verified that it catchesthe regression: with the ACL reverted it fails with
8 != 0, reproducing the reported dashboard state.