From aeba9f9a355347041edbd0167fcd2bb164b3262f Mon Sep 17 00:00:00 2001 From: jessevz Date: Wed, 26 Aug 2026 08:36:08 +0200 Subject: [PATCH 1/8] Made counts respect the access group of users --- ci/apiv2/test_agent.py | 1 + ci/apiv2/test_agentassignment.py | 1 + ci/apiv2/test_agentstat.py | 1 + ci/apiv2/test_apitoken.py | 1 + ci/apiv2/test_completed_count.py | 28 ++++- ci/apiv2/test_count.py | 32 +++++- ci/apiv2/test_file.py | 1 + ci/apiv2/test_hashlist.py | 1 + ci/apiv2/test_realworld_dump.py | 9 +- ci/apiv2/test_task.py | 1 + ci/apiv2/test_taskwrapper.py | 1 + ci/apiv2/utils.py | 19 ++++ src/dba/AbstractModelFactory.php | 8 ++ src/inc/apiv2/common/AbstractModelAPI.php | 44 ++++++-- .../helper/GetCompletedCountHelperAPI.php | 20 +++- src/inc/apiv2/openapi/JsonApiFragments.php | 4 +- src/inc/apiv2/openapi/ModelApiPathBuilder.php | 2 +- src/inc/startup/setup.php | 102 +++++++++--------- 18 files changed, 208 insertions(+), 68 deletions(-) diff --git a/ci/apiv2/test_agent.py b/ci/apiv2/test_agent.py index 83e619091..261572966 100644 --- a/ci/apiv2/test_agent.py +++ b/ci/apiv2/test_agent.py @@ -110,3 +110,4 @@ def test_hide_ip_info(self): def test_acl(self): model_obj = self.create_test_object() self._test_acl_list(model_obj, {'permAgentRead': True}) + self._test_acl_count(model_obj, {'permAgentRead': True}) diff --git a/ci/apiv2/test_agentassignment.py b/ci/apiv2/test_agentassignment.py index ec9628818..5711e6d6d 100644 --- a/ci/apiv2/test_agentassignment.py +++ b/ci/apiv2/test_agentassignment.py @@ -53,6 +53,7 @@ def test_agent_assign_task(self): def test_acl(self): model_obj = self.create_test_object() self._test_acl_list(model_obj, {'permAgentAssignmentRead': True}) + self._test_acl_count(model_obj, {'permAgentAssignmentRead': True}) def test_cracking_time_aggregation(self): dummy_agent, agent, _, task = self.create_agent_with_task().values() diff --git a/ci/apiv2/test_agentstat.py b/ci/apiv2/test_agentstat.py index 848f66bbd..52dbe7386 100644 --- a/ci/apiv2/test_agentstat.py +++ b/ci/apiv2/test_agentstat.py @@ -41,3 +41,4 @@ def test_acl(self): stats = list(AgentStat.objects.filter(agentId=agent.id)) self.assertGreater(len(stats), 0, "Expected agent stats to exist for ACL test") self._test_acl_list(stats[0], {'permAgentStatRead': True}) + self._test_acl_count(stats[0], {'permAgentStatRead': True}) diff --git a/ci/apiv2/test_apitoken.py b/ci/apiv2/test_apitoken.py index 245aefdde..4a0b08014 100644 --- a/ci/apiv2/test_apitoken.py +++ b/ci/apiv2/test_apitoken.py @@ -32,6 +32,7 @@ def test_expandables(self): def test_acl(self): model_obj = self.create_test_object() self._test_acl_list(model_obj, {'permJwtApiKeyRead': True}) + self._test_acl_count(model_obj, {'permJwtApiKeyRead': True}) def test_token_scope_admin_grants_requested(self): """Admin holds every legacy permission, so any requested scope must be granted in the JWT.""" diff --git a/ci/apiv2/test_completed_count.py b/ci/apiv2/test_completed_count.py index bb2ae9e4e..ecf127ce9 100644 --- a/ci/apiv2/test_completed_count.py +++ b/ci/apiv2/test_completed_count.py @@ -1,4 +1,6 @@ -from utils import BaseTest, do_create_dummy_agent, do_create_agentassignent +import requests + +from utils import BaseTest, create_restricted_user, do_create_dummy_agent, do_create_agentassignent from hashtopolis import Helper from hashtopolis import Task @@ -164,3 +166,27 @@ def test_counts_are_consistent_across_calls(self): result1 = Helper().get_completed_count() result2 = Helper().get_completed_count() self.assertEqual(result1, result2) + + def test_acl_counts_are_scoped_to_access_groups(self): + """A user without access groups must not be told about completed tasks of other groups. + + Reporting them makes the dashboard show more completed tasks than the user has tasks. + """ + _create_completed_task(self) + completed_tasks, _ = self._get_counts() + self.assertGreater(completed_tasks, 0, "Expected a completed task to exist for the ACL test") + + auth = create_restricted_user(self, {'permTaskWrapperRead': True, 'permTaskRead': True}) + + # get_completed_count() always authenticates as the config user, so request it directly + helper = Helper() + helper.authenticate(auth=auth) + response = requests.get(helper._api_endpoint + helper._model_uri + 'getCompletedCount', + headers=helper._headers) + self.assertEqual(response.status_code, 200, response.text) + + data = response.json()['data'] + self.assertEqual(data['completedTasks'], 0, + "Restricted user should not count completed tasks outside their access groups") + self.assertEqual(data['completedSupertasks'], 0, + "Restricted user should not count completed supertasks outside their access groups") diff --git a/ci/apiv2/test_count.py b/ci/apiv2/test_count.py index 82c0312d5..38fd7394c 100644 --- a/ci/apiv2/test_count.py +++ b/ci/apiv2/test_count.py @@ -1,5 +1,7 @@ +from hashtopolis import Hashlist from hashtopolis import HashType -from utils import BaseTest +from hashtopolis import HashtopolisError +from utils import BaseTest, do_create_hashlist class CountTest(BaseTest): @@ -21,3 +23,31 @@ def test_count(self): model_count = len(model_objs) api_count = HashType.objects.count(hashTypeId__gte=90000, hashTypeId__lte=91000)['count'] self.assertEqual(model_count, api_count) + + def test_count_by_id(self): + """The generic 'id' key must address the primary key, exactly like it does when listing.""" + model_obj = self.create_test_objects()[0] + self.assertEqual(HashType.objects.count(id=model_obj.id)['count'], 1) + + def test_count_by_aliased_field(self): + """Filtering has to use the alias of a field, not the name of its database column.""" + wanted = do_create_hashlist(extra_payload={'name': 'Hashlist-count-alias-wanted'}) + self.delete_after_test(wanted) + other = do_create_hashlist(extra_payload={'name': 'Hashlist-count-alias-other'}) + self.delete_after_test(other) + + counted = Hashlist.objects.count(name=wanted.name)['count'] + listed = len(list(Hashlist.objects.filter(name=wanted.name))) + self.assertEqual(counted, listed, "count must agree with the list endpoint") + # both hashlists exist, so an applied filter can never count all of them + self.assertLess(counted, Hashlist.objects.count()['count']) + + # 'hashlistName' is the column behind the 'name' alias and must not be accepted + with self.assertRaises(HashtopolisError): + Hashlist.objects.count(hashlistName=wanted.name) + + def test_count_rejects_unknown_filter(self): + """An unusable filter must fail loudly, silently ignoring it reports an unfiltered count.""" + self.create_test_objects() + with self.assertRaises(HashtopolisError): + HashType.objects.count(thisFieldDoesNotExist=1) diff --git a/ci/apiv2/test_file.py b/ci/apiv2/test_file.py index fdd5d31f7..4f123c61b 100644 --- a/ci/apiv2/test_file.py +++ b/ci/apiv2/test_file.py @@ -63,6 +63,7 @@ def test_bulk_delete(self): def test_acl(self): model_obj = self.create_test_object() self._test_acl_list(model_obj, {'permFileRead': True}) + self._test_acl_count(model_obj, {'permFileRead': True}) @pytest.mark.synthetic_only def test_helper_rescan_global_files(self): diff --git a/ci/apiv2/test_hashlist.py b/ci/apiv2/test_hashlist.py index 1a86cad2e..83ebad0f0 100644 --- a/ci/apiv2/test_hashlist.py +++ b/ci/apiv2/test_hashlist.py @@ -162,3 +162,4 @@ def test_bulk_delete(self): def test_acl(self): model_obj = self.create_test_object() self._test_acl_list(model_obj, {'permHashlistRead': True}) + self._test_acl_count(model_obj, {'permHashlistRead': True}) diff --git a/ci/apiv2/test_realworld_dump.py b/ci/apiv2/test_realworld_dump.py index e8aa093de..bd87dffc0 100644 --- a/ci/apiv2/test_realworld_dump.py +++ b/ci/apiv2/test_realworld_dump.py @@ -21,10 +21,13 @@ def _api_get(self, path, params): return response.json() def test_known_dump_counts(self): - self.assertEqual(Hash.objects.count(hashId__lte=30129)['count'], 28957) - self.assertEqual(Hashlist.objects.count(hashlistId__lte=272)['count'], 267) + # Counts are scoped to the access groups of the requesting user, just like listing is. + # 'admin' is only a member of the default group, while hashlist 272 of the dump lives in + # access group 5, so that hashlist and its 5477 hashes and 10 tasks are not counted here. + self.assertEqual(Hash.objects.count(hashId__lte=30129)['count'], 23480) + self.assertEqual(Hashlist.objects.count(hashlistId__lte=272)['count'], 266) self.assertEqual(Agent.objects.count(agentId__lte=18)['count'], 7) - self.assertEqual(Task.objects.count(taskId__lte=1336)['count'], 304) + self.assertEqual(Task.objects.count(taskId__lte=1336)['count'], 294) self.assertEqual(Hash.objects.count(hashlistId=4)['count'], 10346) self.assertEqual(Task.objects.count(taskWrapperId=1004)['count'], 1) diff --git a/ci/apiv2/test_task.py b/ci/apiv2/test_task.py index 7cd943a02..eecad7331 100644 --- a/ci/apiv2/test_task.py +++ b/ci/apiv2/test_task.py @@ -263,6 +263,7 @@ def test_toggle_archive_task_supertask_type(self): def test_acl(self): model_obj = self.create_test_object() self._test_acl_list(model_obj, {'permTaskRead': True}) + self._test_acl_count(model_obj, {'permTaskRead': True}) def test_toggle_archive_task_invalid_type_error(self): """Test that toggleArchiveTask throws an error for invalid task types""" diff --git a/ci/apiv2/test_taskwrapper.py b/ci/apiv2/test_taskwrapper.py index 8cb15ae92..5615c391c 100644 --- a/ci/apiv2/test_taskwrapper.py +++ b/ci/apiv2/test_taskwrapper.py @@ -75,3 +75,4 @@ def test_helper_create_supertask_generic_cracker(self): def test_acl(self): model_obj = self.create_test_object() self._test_acl_list(model_obj, {'permTaskWrapperRead': True}) + self._test_acl_count(model_obj, {'permTaskWrapperRead': True}) diff --git a/ci/apiv2/utils.py b/ci/apiv2/utils.py index 5791edb14..243ebcd2a 100644 --- a/ci/apiv2/utils.py +++ b/ci/apiv2/utils.py @@ -479,6 +479,25 @@ def _test_acl_list(self, model_obj, permissions): objs = list(self.model_class.objects.filter(id=model_obj.id)) self.assertGreater(len(objs), 0, "Admin user should see this object in list results") + def _test_acl_count(self, model_obj, permissions): + """Test that a restricted user (with no access groups) does not see the object in count results. + + The restricted user has no access group membership at all, so every ACL-restricted + model must report zero for them, both in `count` and in the unfiltered `total_count`. + """ + auth = create_restricted_user(self, permissions) + conn = self.model_class.objects.get_conn() + + restricted = conn.count(filter={}, extra_params={'include_total': 'true'}, auth=auth) + self.assertEqual(restricted['count'], 0, + "Restricted user should not count objects outside their access groups") + self.assertEqual(restricted['total_count'], 0, + "Restricted user's total_count should not include objects outside their access groups") + + # NOTE: must run after the restricted call, this resets the connector back to the admin token + admin = conn.count(filter={}) + self.assertGreater(admin['count'], 0, "Admin user should count this object") + def _test_patch(self, model_obj, attr, new_attr_value=None): """ Generic test worker to PATCH object""" # Create new value diff --git a/src/dba/AbstractModelFactory.php b/src/dba/AbstractModelFactory.php index 879a68d1a..9bcd79b0d 100755 --- a/src/dba/AbstractModelFactory.php +++ b/src/dba/AbstractModelFactory.php @@ -478,6 +478,10 @@ public function minMaxFilter(array $options, string $sumColumn, string $op): mix } /** + * $joinFilter is the join the aggregations are calculated over. Additional joins can be supplied + * as Factory::JOIN in $options, for example to filter on a table which is not aggregated over. + * As the result is grouped by the primary keys of this factory, such joins must not multiply rows. + * * @param array $options * @param JoinFilter $joinFilter * @param Aggregation[] $aggregations @@ -497,6 +501,10 @@ public function joinAggregationFilter(array $options, JoinFilter $joinFilter, ar $match2 = self::getMappedModelKey($joinFilter->getOtherFactory()->getNullObject(), $joinFilter->getMatch2()); $query .= " " . $joinFilter->getJoinType() . " JOIN " . $joinFilter->getOtherFactory()->getMappedModelTable() . " ON " . $this->getMappedModelTable() . "." . $match1 . "=" . $joinFilter->getOtherFactory()->getMappedModelTable() . "." . $match2 . " "; + if (array_key_exists(Factory::JOIN, $options)) { + $query .= $this->applyJoins($options[Factory::JOIN]); + } + // Apply all normal filter to this query if (array_key_exists(Factory::FILTER, $options)) { $query .= $this->applyFilters($vals, $options[Factory::FILTER]); diff --git a/src/inc/apiv2/common/AbstractModelAPI.php b/src/inc/apiv2/common/AbstractModelAPI.php index 92833af3c..886c5534a 100644 --- a/src/inc/apiv2/common/AbstractModelAPI.php +++ b/src/inc/apiv2/common/AbstractModelAPI.php @@ -922,7 +922,7 @@ public function get(Request $request, Response $response, array $args): Response } /** - * Maps filters to the appropiate models based on their feautures. + * Maps filters to the appropiate models based on their features. * * Helper function to get valid filters for the models. This is usefull when multiple objects * have been included and the correct filters need to be mapped to the correct objects. @@ -952,21 +952,35 @@ public function get(Request $request, Response $response, array $args): Response */ public function filterObjectMap(array $filters, array $models): array { $modelFilterMap = []; + /* + * Filter parameters address fields by their alias, not by their database column name, + * so the primary key has to be looked up by alias as well. Same expression as makeFilter(). + */ + $primaryKeyAlias = array_column($this->getAliasedFeatures(), 'alias', 'dbname')[$this->getPrimaryKey()]; foreach ($filters as $filter => $value) { - if (preg_match('/^(?P[_a-zA-Z0-9]+?)(?|__eq|__ne|__lt|__lte|__gt|__gte|__contains|__startswith|__endswith|__icontains|__istartswith|__iendswith)$/', $filter, $matches) == 0) { + /* Operator list has to stay in sync with makeFilter(), it resolves the filters mapped here */ + if (preg_match('/^(?P[_a-zA-Z0-9]+?)(?|__eq|__ne|__lt|__lte|__gt|__gte|__contains|__startswith|__endswith|__icontains|__istartswith|__iendswith|__in|__nin)$/', $filter, $matches) == 0) { throw new HttpForbidden("Filter parameter '" . $filter . "' is not valid"); } + $isMapped = false; foreach ($models as $model) { - $features = $model->getFeatures(); - // Special filtering of _id to use for uniform access to model primary key - $cast_key = $matches['key'] == '_id' ? array_column($features, 'alias', 'dbname')[$this->getPrimaryKey()] : $matches['key']; + /* Aliased features are keyed by alias, the raw DBA features are keyed by column name */ + $features = $this->getAliasedFeaturesOther($model::class); + // Special filtering of id to use for uniform access to model primary key + $cast_key = $matches['key'] == 'id' ? $primaryKeyAlias : $matches['key']; if (!array_key_exists($cast_key, $features)) { continue; //not a valid filter for current model } $modelFilterMap[$model::class][$filter] = $value; + $isMapped = true; break; //filter has been found for current model, so break to go to next filter } + + if (!$isMapped) { + /* Dropping the filter here would silently answer the request with an unfiltered result */ + throw new HttpForbidden("Filter parameter '" . $filter . "' is not valid (key not valid field)"); + } } return $modelFilterMap; } @@ -1014,6 +1028,23 @@ public function count(Request $request, Response $response, array $args): Respon $qFs = array_merge($qFs, $current_qFs); } + /* + * Restrict the count to the objects the current user is allowed to see, exactly like + * getManyResources() does. Without this the endpoint leaks the existence of objects + * outside of the user's access groups. + */ + $aFs_ACL = $this->getFilterACL(); + if (isset($aFs_ACL[Factory::FILTER])) { + $qFs = array_merge($aFs_ACL[Factory::FILTER], $qFs); + } + if (isset($aFs_ACL[Factory::JOIN])) { + foreach ($aFs_ACL[Factory::JOIN] as $joinFilter) { + if (!$this::checkJoinExists($aFs[Factory::JOIN] ?? [], $joinFilter->getOtherFactory()->getModelName())) { + $aFs[Factory::JOIN][] = $joinFilter; + } + } + } + if (count($qFs) > 0) { $aFs[Factory::FILTER] = $qFs; } @@ -1023,7 +1054,8 @@ public function count(Request $request, Response $response, array $args): Respon $include_total = $request->getQueryParams()['include_total'] ?? false; if ($include_total == "true") { - $meta["total_count"] = $factory->countFilter([]); + /* "Without any filter applied" means without the request's filters, but still within the user's ACL */ + $meta["total_count"] = $factory->countFilter(array_intersect_key($aFs_ACL, [Factory::FILTER => 1, Factory::JOIN => 1])); } $ret = self::createJsonResponse(meta: $meta); diff --git a/src/inc/apiv2/helper/GetCompletedCountHelperAPI.php b/src/inc/apiv2/helper/GetCompletedCountHelperAPI.php index c920a10b5..ccbab3a42 100644 --- a/src/inc/apiv2/helper/GetCompletedCountHelperAPI.php +++ b/src/inc/apiv2/helper/GetCompletedCountHelperAPI.php @@ -8,6 +8,7 @@ use Hashtopolis\dba\ContainFilter; use Hashtopolis\dba\JoinFilter; use Hashtopolis\dba\models\Chunk; +use Hashtopolis\dba\models\Hashlist; use Hashtopolis\dba\models\TaskWrapper; use Hashtopolis\dba\models\TaskWrapperDisplay; use Hashtopolis\inc\apiv2\common\AbstractHelperAPI; @@ -15,6 +16,8 @@ use Hashtopolis\inc\apiv2\error\HttpForbidden; use Hashtopolis\inc\defines\DTaskTypes; use Hashtopolis\inc\HTException; +use Hashtopolis\inc\Util; +use Hashtopolis\inc\utils\AccessUtils; use JsonException; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; @@ -68,15 +71,24 @@ public function handleGet(Request $request, Response $response): Response { $this->preCommon($request); $data = []; + /* + * Both counts are restricted to the access groups of the requesting user, the same way + * TaskWrapperDisplayAPI and TaskWrapperAPI restrict their listings. Without this the + * dashboard reports more completed tasks than there are tasks visible to the user. + */ + $accessGroups = Util::arrayOfIds(AccessUtils::getAccessGroupsOfUser($this->getCurrentUser())); + // count of completed normal tasks $qF1 = new QueryFilter(TaskWrapperDisplay::TASK_TYPE, DTaskTypes::NORMAL, "="); $qF2 = new QueryFilter(TaskWrapperDisplay::TASK_IS_ARCHIVED, 0, "="); + $qF3 = new ContainFilter(Hashlist::ACCESS_GROUP_ID, $accessGroups, Factory::getHashlistFactory()); $jF = new JoinFilter(Factory::getChunkFactory(), TaskWrapperDisplay::TASK_ID, Chunk::TASK_ID); + $aclJF = new JoinFilter(Factory::getHashlistFactory(), TaskWrapperDisplay::HASHLIST_ID, Hashlist::HASHLIST_ID); $agg1 = new Aggregation(Chunk::CHECKPOINT, Aggregation::SUM, Factory::getChunkFactory()); $agg2 = new Aggregation(Chunk::SKIP, Aggregation::SUM, Factory::getChunkFactory()); - $results = Factory::getTaskWrapperDisplayFactory()->joinAggregationFilter([Factory::FILTER => [$qF1, $qF2]], $jF, [$agg1, $agg2]); + $results = Factory::getTaskWrapperDisplayFactory()->joinAggregationFilter([Factory::FILTER => [$qF1, $qF2, $qF3], Factory::JOIN => [$aclJF]], $jF, [$agg1, $agg2]); $completed = 0; for ($i = 0; $i < sizeof($results[Factory::getTaskWrapperDisplayFactory()->getModelName()]); $i++) { @@ -89,10 +101,12 @@ public function handleGet(Request $request, Response $response): Response { } $data["completedTasks"] = $completed; - // count of completed supertasks + // count of completed supertasks, the task query below inherits the ACL through $taskWrapperIds $qF1 = new QueryFilter(TaskWrapper::TASK_TYPE, DTaskTypes::SUPERTASK, "="); $qF2 = new QueryFilter(TaskWrapper::IS_ARCHIVED, 0, "="); - $taskWrapperIds = Factory::getTaskWrapperFactory()->columnFilter([Factory::FILTER => [$qF1, $qF2]], TaskWrapper::TASK_WRAPPER_ID); + $qF3 = new ContainFilter(Hashlist::ACCESS_GROUP_ID, $accessGroups, Factory::getHashlistFactory()); + $aclJF = new JoinFilter(Factory::getHashlistFactory(), TaskWrapper::HASHLIST_ID, Hashlist::HASHLIST_ID); + $taskWrapperIds = Factory::getTaskWrapperFactory()->columnFilter([Factory::FILTER => [$qF1, $qF2, $qF3], Factory::JOIN => [$aclJF]], TaskWrapper::TASK_WRAPPER_ID); $qF = new ContainFilter(Task::TASK_WRAPPER_ID, $taskWrapperIds); $jF = new JoinFilter(Factory::getChunkFactory(), Task::TASK_ID, Chunk::TASK_ID, joinType: JoinFilter::LEFT); diff --git a/src/inc/apiv2/openapi/JsonApiFragments.php b/src/inc/apiv2/openapi/JsonApiFragments.php index 9cbe7fdfa..8cf0791cd 100644 --- a/src/inc/apiv2/openapi/JsonApiFragments.php +++ b/src/inc/apiv2/openapi/JsonApiFragments.php @@ -207,11 +207,11 @@ public function buildCountResponse(): array { "properties" => [ "count" => [ "type" => "integer", - "description" => "Number of objects matching the given filters" + "description" => "Number of objects accessible to the current user matching the given filters" ], "total_count" => [ "type" => "integer", - "description" => "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description" => "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" ] ] ], diff --git a/src/inc/apiv2/openapi/ModelApiPathBuilder.php b/src/inc/apiv2/openapi/ModelApiPathBuilder.php index e605d7938..621c699ce 100644 --- a/src/inc/apiv2/openapi/ModelApiPathBuilder.php +++ b/src/inc/apiv2/openapi/ModelApiPathBuilder.php @@ -467,7 +467,7 @@ public function addRoute(RouteTarget $target, AbstractModelAPI $api, ContainerIn "type" => "boolean" ], "example" => true, - "description" => "Also report the number of objects without any filter applied, as `meta.total_count`" + "description" => "Also report the number of accessible objects without any filter applied, as `meta.total_count`" ] ]; return; diff --git a/src/inc/startup/setup.php b/src/inc/startup/setup.php index 74ff07e83..b30a08648 100755 --- a/src/inc/startup/setup.php +++ b/src/inc/startup/setup.php @@ -62,68 +62,68 @@ * - if needed (because there are more generations available), run the previous step again */ -if (!$initialSetup) { - // retrieve the oldest migration - $oF = new OrderFilter(_sqlx_migrations::VERSION, "ASC"); - $firstEntry = Factory::get_sqlx_migrationsFactory()->filter([Factory::ORDER => $oF], true); +// if (!$initialSetup) { +// // retrieve the oldest migration +// $oF = new OrderFilter(_sqlx_migrations::VERSION, "ASC"); +// $firstEntry = Factory::get_sqlx_migrationsFactory()->filter([Factory::ORDER => $oF], true); - if ($firstEntry == null) { - echo "Unable to identify migrations position!\n"; - exit(-1); - } +// if ($firstEntry == null) { +// echo "Unable to identify migrations position!\n"; +// exit(-1); +// } // identify the generation we are on - $allGenerations = MigrationUtils::getAllGenerations(StartupConfig::getInstance()->getDatabaseType()); - $generation = -1; - foreach ($allGenerations as $gen => $migrations) { - if (sizeof($migrations) == 0) { - continue; - } - if (explode("_", $migrations[0])[0] == $firstEntry->getId()) { - $generation = $gen; - break; - } - } +// $allGenerations = MigrationUtils::getAllGenerations(StartupConfig::getInstance()->getDatabaseType()); +// $generation = -1; +// foreach ($allGenerations as $gen => $migrations) { +// if (sizeof($migrations) == 0) { +// continue; +// } +// if (explode("_", $migrations[0])[0] == $firstEntry->getId()) { +// $generation = $gen; +// break; +// } +// } - if ($generation == -1) { - echo "Could not determine current migrations generation, aborting...\n"; - exit(-1); - } +// if ($generation == -1) { +// echo "Could not determine current migrations generation, aborting...\n"; +// exit(-1); +// } - try { - while ($generation > 0) { - echo "Upgrading to a new sqlx migrations generation (current $generation)...\n"; +// try { +// while ($generation > 0) { +// echo "Upgrading to a new sqlx migrations generation (current $generation)...\n"; - // we are on an older generation branch, we need to migrate - // make sure we are up-to-date on this generation - echo "Running migration on current generation to be up-to-date...\n"; - MigrationUtils::runDatabaseMigration($generation); +// // we are on an older generation branch, we need to migrate +// // make sure we are up-to-date on this generation +// echo "Running migration on current generation to be up-to-date...\n"; +// MigrationUtils::runDatabaseMigration($generation); - // jump to next migration - $generation--; - $entry = MigrationUtils::getMigrationStartEntry($generation); - if ($entry === null) { - throw new Exception("Failed to retrieve initial migration information for generation $generation!"); - } +// // jump to next migration +// $generation--; +// $entry = MigrationUtils::getMigrationStartEntry($generation); +// if ($entry === null) { +// throw new Exception("Failed to retrieve initial migration information for generation $generation!"); +// } - // clear migration table - echo "Clearing migration table...\n"; - Factory::get_sqlx_migrationsFactory()->massDeletion([]); +// // clear migration table +// echo "Clearing migration table...\n"; +// Factory::get_sqlx_migrationsFactory()->massDeletion([]); - // add first entry - echo "Add initial migration entry...\n"; - Factory::get_sqlx_migrationsFactory()->save($entry); - echo "Generation switch from " . ($generation + 1) . " to $generation completed!\n"; - } - } - catch (Exception $e) { - echo "Failed to run generation upgrade: $e\n"; - exit(-1); - } -} +// // add first entry +// echo "Add initial migration entry...\n"; +// Factory::get_sqlx_migrationsFactory()->save($entry); +// echo "Generation switch from " . ($generation + 1) . " to $generation completed!\n"; +// } +// } +// catch (Exception $e) { +// echo "Failed to run generation upgrade: $e\n"; +// exit(-1); +// } +// } // run database migration on current generation to be fully up-to-date -MigrationUtils::runDatabaseMigration(); +// MigrationUtils::runDatabaseMigration(); if ($initialSetup === true) { // if peppers are not set, generate them and save them From e092dfd3aa46a70ed83d773c8924ecf6a388c2a1 Mon Sep 17 00:00:00 2001 From: jessevz Date: Wed, 26 Aug 2026 12:39:09 +0200 Subject: [PATCH 2/8] Revert accidental comitted comments --- src/inc/startup/setup.php | 102 +++++++++++++++++++------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/src/inc/startup/setup.php b/src/inc/startup/setup.php index b30a08648..63e14e6d9 100755 --- a/src/inc/startup/setup.php +++ b/src/inc/startup/setup.php @@ -62,65 +62,65 @@ * - if needed (because there are more generations available), run the previous step again */ -// if (!$initialSetup) { -// // retrieve the oldest migration -// $oF = new OrderFilter(_sqlx_migrations::VERSION, "ASC"); -// $firstEntry = Factory::get_sqlx_migrationsFactory()->filter([Factory::ORDER => $oF], true); +if (!$initialSetup) { + // retrieve the oldest migration + $oF = new OrderFilter(_sqlx_migrations::VERSION, "ASC"); + $firstEntry = Factory::get_sqlx_migrationsFactory()->filter([Factory::ORDER => $oF], true); -// if ($firstEntry == null) { -// echo "Unable to identify migrations position!\n"; -// exit(-1); -// } + if ($firstEntry == null) { + echo "Unable to identify migrations position!\n"; + exit(-1); + } - // identify the generation we are on -// $allGenerations = MigrationUtils::getAllGenerations(StartupConfig::getInstance()->getDatabaseType()); -// $generation = -1; -// foreach ($allGenerations as $gen => $migrations) { -// if (sizeof($migrations) == 0) { -// continue; -// } -// if (explode("_", $migrations[0])[0] == $firstEntry->getId()) { -// $generation = $gen; -// break; -// } -// } + identify the generation we are on + $allGenerations = MigrationUtils::getAllGenerations(StartupConfig::getInstance()->getDatabaseType()); + $generation = -1; + foreach ($allGenerations as $gen => $migrations) { + if (sizeof($migrations) == 0) { + continue; + } + if (explode("_", $migrations[0])[0] == $firstEntry->getId()) { + $generation = $gen; + break; + } + } -// if ($generation == -1) { -// echo "Could not determine current migrations generation, aborting...\n"; -// exit(-1); -// } + if ($generation == -1) { + echo "Could not determine current migrations generation, aborting...\n"; + exit(-1); + } -// try { -// while ($generation > 0) { -// echo "Upgrading to a new sqlx migrations generation (current $generation)...\n"; + try { + while ($generation > 0) { + echo "Upgrading to a new sqlx migrations generation (current $generation)...\n"; -// // we are on an older generation branch, we need to migrate -// // make sure we are up-to-date on this generation -// echo "Running migration on current generation to be up-to-date...\n"; -// MigrationUtils::runDatabaseMigration($generation); + // we are on an older generation branch, we need to migrate + // make sure we are up-to-date on this generation + echo "Running migration on current generation to be up-to-date...\n"; + MigrationUtils::runDatabaseMigration($generation); -// // jump to next migration -// $generation--; -// $entry = MigrationUtils::getMigrationStartEntry($generation); -// if ($entry === null) { -// throw new Exception("Failed to retrieve initial migration information for generation $generation!"); -// } + // jump to next migration + $generation--; + $entry = MigrationUtils::getMigrationStartEntry($generation); + if ($entry === null) { + throw new Exception("Failed to retrieve initial migration information for generation $generation!"); + } -// // clear migration table -// echo "Clearing migration table...\n"; -// Factory::get_sqlx_migrationsFactory()->massDeletion([]); + // clear migration table + echo "Clearing migration table...\n"; + Factory::get_sqlx_migrationsFactory()->massDeletion([]); -// // add first entry -// echo "Add initial migration entry...\n"; -// Factory::get_sqlx_migrationsFactory()->save($entry); -// echo "Generation switch from " . ($generation + 1) . " to $generation completed!\n"; -// } -// } -// catch (Exception $e) { -// echo "Failed to run generation upgrade: $e\n"; -// exit(-1); -// } -// } + // add first entry + echo "Add initial migration entry...\n"; + Factory::get_sqlx_migrationsFactory()->save($entry); + echo "Generation switch from " . ($generation + 1) . " to $generation completed!\n"; + } + } + catch (Exception $e) { + echo "Failed to run generation upgrade: $e\n"; + exit(-1); + } +} // run database migration on current generation to be fully up-to-date // MigrationUtils::runDatabaseMigration(); From fed95c2822ae3f3432d814b4acebbbbb4a775f41 Mon Sep 17 00:00:00 2001 From: jessevz Date: Wed, 26 Aug 2026 12:41:09 +0200 Subject: [PATCH 3/8] Fixed accidnetally commented out, comment --- src/inc/startup/setup.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inc/startup/setup.php b/src/inc/startup/setup.php index 63e14e6d9..0c9425859 100755 --- a/src/inc/startup/setup.php +++ b/src/inc/startup/setup.php @@ -72,7 +72,7 @@ exit(-1); } - identify the generation we are on + // identify the generation we are on $allGenerations = MigrationUtils::getAllGenerations(StartupConfig::getInstance()->getDatabaseType()); $generation = -1; foreach ($allGenerations as $gen => $migrations) { From a6ccc0b06059352fcc3235254aa4eb858b832756 Mon Sep 17 00:00:00 2001 From: jessevz Date: Wed, 26 Aug 2026 18:37:18 +0200 Subject: [PATCH 4/8] revert changes to setup.php --- src/inc/startup/setup.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inc/startup/setup.php b/src/inc/startup/setup.php index 0c9425859..74ff07e83 100755 --- a/src/inc/startup/setup.php +++ b/src/inc/startup/setup.php @@ -123,7 +123,7 @@ } // run database migration on current generation to be fully up-to-date -// MigrationUtils::runDatabaseMigration(); +MigrationUtils::runDatabaseMigration(); if ($initialSetup === true) { // if peppers are not set, generate them and save them From 0b65c47e7e6b59ea3c5807102c16ea44c48f218e Mon Sep 17 00:00:00 2001 From: jessevz Date: Wed, 26 Aug 2026 18:41:11 +0200 Subject: [PATCH 5/8] Also make count of cracks respect access groups --- ci/apiv2/test_cracks_per_day.py | 26 ++++++++++++++++++- src/dba/AbstractModelFactory.php | 10 ++++++- .../apiv2/helper/GetCracksPerDayHelperAPI.php | 22 +++++++++++++--- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/ci/apiv2/test_cracks_per_day.py b/ci/apiv2/test_cracks_per_day.py index 7b64510a8..787c684fa 100644 --- a/ci/apiv2/test_cracks_per_day.py +++ b/ci/apiv2/test_cracks_per_day.py @@ -1,7 +1,9 @@ from datetime import date +import requests + from hashtopolis import Hashlist, Helper -from utils import BaseTest +from utils import BaseTest, create_restricted_user class CracksPerDayTest(BaseTest): @@ -49,3 +51,25 @@ def test_count_increases_with_more_cracks(self): count_after = result_after.get(today, 0) self.assertEqual(count_after, count_before + 2) + + def test_acl_cracks_are_scoped_to_access_groups(self): + """A user without access groups must not be told about cracks of other groups.""" + hashlist = self.create_hashlist() + helper = Helper() + helper.import_cracked_hashes(hashlist, 'paste', 'cc03e747a6afbbcbf8be7668acfebee5:test123', ':', 0) + + today = date.today().strftime('%Y-%m-%d') + self.assertGreaterEqual(helper.get_cracks_per_day().get(today, 0), 1, + "Expected a crack today for the ACL test") + + auth = create_restricted_user(self, {'permHashlistRead': True, 'permHashRead': True}) + + # get_cracks_per_day() always authenticates as the config user, so request it directly + restricted = Helper() + restricted.authenticate(auth=auth) + response = requests.get(restricted._api_endpoint + restricted._model_uri + 'getCracksPerDay', + headers=restricted._headers) + self.assertEqual(response.status_code, 200, response.text) + + self.assertEqual(response.json()['data'], {}, + "Restricted user should not see cracks outside their access groups") diff --git a/src/dba/AbstractModelFactory.php b/src/dba/AbstractModelFactory.php index 9bcd79b0d..66ab396de 100755 --- a/src/dba/AbstractModelFactory.php +++ b/src/dba/AbstractModelFactory.php @@ -668,6 +668,8 @@ public function sumFilter(array $options, string $sumColumn): int { * Create a timeseries with counts per day for a given table. * * @param array $options can contain FILTER options to select which entries should match to be counted (e.g. also if the timeseries should only be over a certain amount of day) + * and JOIN options to filter on a column of another table. As every counted row is one entry + * of this factory, such joins must not multiply rows. * @param string $timeColumn table column which should be used to be use for the 'day' grouping * @return array list of [day => count] entries * @throws Exception @@ -676,12 +678,18 @@ public function columnTimeseriesFilter(array $options, string $timeColumn): arra $dbType = StartupConfig::getInstance()->getDatabaseType(); $to_timestamp = ($dbType == "postgres") ? "TO_TIMESTAMP" : "FROM_UNIXTIME"; - $query = "SELECT DATE(" . $to_timestamp . "(" . self::getMappedModelKey($this->getNullObject(), $timeColumn) . ")) AS day, COUNT(*) AS total"; + /* Prefixed with the table, the column name can also exist on a joined table */ + $timeColumnRef = $this->getMappedModelTable() . "." . self::getMappedModelKey($this->getNullObject(), $timeColumn); + $query = "SELECT DATE(" . $to_timestamp . "(" . $timeColumnRef . ")) AS day, COUNT(*) AS total"; $query .= " FROM " . $this->getMappedModelTable(); $vals = array(); + if (array_key_exists(Factory::JOIN, $options)) { + $query .= $this->applyJoins($options[Factory::JOIN]); + } + if (array_key_exists(Factory::FILTER, $options)) { $query .= $this->applyFilters($vals, $options[Factory::FILTER]); } diff --git a/src/inc/apiv2/helper/GetCracksPerDayHelperAPI.php b/src/inc/apiv2/helper/GetCracksPerDayHelperAPI.php index f8de95558..aee84eaad 100644 --- a/src/inc/apiv2/helper/GetCracksPerDayHelperAPI.php +++ b/src/inc/apiv2/helper/GetCracksPerDayHelperAPI.php @@ -6,10 +6,15 @@ use Hashtopolis\dba\AbstractModel; use Hashtopolis\inc\apiv2\common\AbstractHelperAPI; use Hashtopolis\inc\apiv2\error\HttpError; +use Hashtopolis\dba\ContainFilter; use Hashtopolis\dba\Factory; +use Hashtopolis\dba\JoinFilter; use Hashtopolis\dba\models\Hash; +use Hashtopolis\dba\models\HashBinary; use Hashtopolis\dba\models\Hashlist; use Hashtopolis\dba\QueryFilter; +use Hashtopolis\inc\Util; +use Hashtopolis\inc\utils\AccessUtils; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use stdClass; @@ -52,12 +57,23 @@ public function getParamsSwagger(): array { */ public function handleGet(Request $request, Response $response): Response { $this->preCommon($request); - + + /* + * Cracks are only reported for hashlists within the access groups of the requesting user, + * the same restriction HashAPI applies to its listing. + */ + $accessGroups = Util::arrayOfIds(AccessUtils::getAccessGroupsOfUser($this->getCurrentUser())); + $start = time() - 3600 * 24 * 365; $qF1 = new QueryFilter(Hash::IS_CRACKED, 1, "="); $qF2 = new QueryFilter(Hash::TIME_CRACKED, $start, ">"); - $counts = Factory::getHashFactory()->columnTimeseriesFilter([Factory::FILTER => [$qF1, $qF2]], Hash::TIME_CRACKED); - $counts2 = Factory::getHashBinaryFactory()->columnTimeseriesFilter([Factory::FILTER => [$qF1, $qF2]], Hash::TIME_CRACKED); + $qF3 = new ContainFilter(Hashlist::ACCESS_GROUP_ID, $accessGroups, Factory::getHashlistFactory()); + + $hashJF = new JoinFilter(Factory::getHashlistFactory(), Hash::HASHLIST_ID, Hashlist::HASHLIST_ID); + $binaryJF = new JoinFilter(Factory::getHashlistFactory(), HashBinary::HASHLIST_ID, Hashlist::HASHLIST_ID); + + $counts = Factory::getHashFactory()->columnTimeseriesFilter([Factory::FILTER => [$qF1, $qF2, $qF3], Factory::JOIN => [$hashJF]], Hash::TIME_CRACKED); + $counts2 = Factory::getHashBinaryFactory()->columnTimeseriesFilter([Factory::FILTER => [$qF1, $qF2, $qF3], Factory::JOIN => [$binaryJF]], Hash::TIME_CRACKED); foreach ($counts2 as $key => $value) { $counts[$key] = ($counts[$key] ?? 0) + $value; } From 260a228d64e8f7da0fe6f99a0bf4c5191caf25a4 Mon Sep 17 00:00:00 2001 From: jessevz Date: Wed, 26 Aug 2026 18:50:02 +0200 Subject: [PATCH 6/8] fixed typo --- src/inc/apiv2/common/AbstractModelAPI.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inc/apiv2/common/AbstractModelAPI.php b/src/inc/apiv2/common/AbstractModelAPI.php index 886c5534a..278b7fc43 100644 --- a/src/inc/apiv2/common/AbstractModelAPI.php +++ b/src/inc/apiv2/common/AbstractModelAPI.php @@ -922,7 +922,7 @@ public function get(Request $request, Response $response, array $args): Response } /** - * Maps filters to the appropiate models based on their features. + * Maps filters to the appropriate models based on their features. * * Helper function to get valid filters for the models. This is usefull when multiple objects * have been included and the correct filters need to be mapped to the correct objects. From c3a823bcb32299b5cbe0a1d1fa5edcb0a9ecc5a7 Mon Sep 17 00:00:00 2001 From: jessevz Date: Thu, 27 Aug 2026 08:32:01 +0200 Subject: [PATCH 7/8] Regenerated openapi.json --- openapi.json | 180 +++++++++++++++++++++++++-------------------------- 1 file changed, 90 insertions(+), 90 deletions(-) diff --git a/openapi.json b/openapi.json index 455c481e9..110134960 100644 --- a/openapi.json +++ b/openapi.json @@ -446,7 +446,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count AccessGroups", @@ -1541,7 +1541,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count Agents", @@ -2712,7 +2712,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count AgentAssignments", @@ -3671,7 +3671,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count AgentBinarys", @@ -4203,7 +4203,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count AgentErrors", @@ -4910,7 +4910,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count AgentStats", @@ -5502,7 +5502,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count ApiTokens", @@ -6243,7 +6243,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count Chunks", @@ -6917,7 +6917,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count Configs", @@ -7582,7 +7582,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count ConfigSections", @@ -8108,7 +8108,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count CrackerBinarys", @@ -9257,7 +9257,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count CrackerBinaryTypes", @@ -10404,7 +10404,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count Files", @@ -11367,7 +11367,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count GlobalPermissionGroups", @@ -12290,7 +12290,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count Hashs", @@ -13097,7 +13097,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count Hashlists", @@ -14241,7 +14241,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count HashTypes", @@ -14706,7 +14706,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count HealthCheckAgents", @@ -15511,7 +15511,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count HealthChecks", @@ -16429,7 +16429,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count LogEntrys", @@ -16953,7 +16953,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count NotificationSettings", @@ -17910,7 +17910,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count Preprocessors", @@ -18614,7 +18614,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count PreTasks", @@ -19537,7 +19537,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count Speeds", @@ -20356,7 +20356,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count Supertasks", @@ -21524,7 +21524,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count Tasks", @@ -22604,7 +22604,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count TaskWrappers", @@ -23547,7 +23547,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count TaskWrapperDisplays", @@ -24531,7 +24531,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count Users", @@ -25672,7 +25672,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ], "summary": "Count Vouchers", @@ -30682,11 +30682,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -34126,11 +34126,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -35845,11 +35845,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -36483,11 +36483,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -37204,11 +37204,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -37623,11 +37623,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -38578,11 +38578,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -39837,11 +39837,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -41494,11 +41494,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -41821,11 +41821,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -43200,11 +43200,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -44611,11 +44611,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -45873,11 +45873,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -47039,11 +47039,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -48612,11 +48612,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -52112,11 +52112,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -52704,11 +52704,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -53713,11 +53713,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -55399,11 +55399,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -55830,11 +55830,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -57317,11 +57317,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -57965,11 +57965,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -59098,11 +59098,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -60191,11 +60191,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -61353,11 +61353,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -65653,11 +65653,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -68678,11 +68678,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -69735,11 +69735,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -71266,11 +71266,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -71825,11 +71825,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, From 4b45283bfba303fd0a90892b8b9d2dd452e35954 Mon Sep 17 00:00:00 2001 From: jessevz Date: Thu, 27 Aug 2026 16:53:18 +0200 Subject: [PATCH 8/8] Fixed generated spec --- ci/phpunit/fixtures/openapi/config.spec.json | 12 ++++++------ .../fixtures/openapi/crackerbinarytype.spec.json | 6 +++--- ci/phpunit/fixtures/openapi/hashtype.spec.json | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ci/phpunit/fixtures/openapi/config.spec.json b/ci/phpunit/fixtures/openapi/config.spec.json index ca79b7c8a..99f491137 100644 --- a/ci/phpunit/fixtures/openapi/config.spec.json +++ b/ci/phpunit/fixtures/openapi/config.spec.json @@ -324,7 +324,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ] } @@ -961,7 +961,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ] } @@ -2952,11 +2952,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, @@ -3575,11 +3575,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, diff --git a/ci/phpunit/fixtures/openapi/crackerbinarytype.spec.json b/ci/phpunit/fixtures/openapi/crackerbinarytype.spec.json index 4ae24e389..4188aebcf 100644 --- a/ci/phpunit/fixtures/openapi/crackerbinarytype.spec.json +++ b/ci/phpunit/fixtures/openapi/crackerbinarytype.spec.json @@ -458,7 +458,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ] } @@ -3051,11 +3051,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } }, diff --git a/ci/phpunit/fixtures/openapi/hashtype.spec.json b/ci/phpunit/fixtures/openapi/hashtype.spec.json index ba19e9616..9c3f29d22 100644 --- a/ci/phpunit/fixtures/openapi/hashtype.spec.json +++ b/ci/phpunit/fixtures/openapi/hashtype.spec.json @@ -450,7 +450,7 @@ "type": "boolean" }, "example": true, - "description": "Also report the number of objects without any filter applied, as `meta.total_count`" + "description": "Also report the number of accessible objects without any filter applied, as `meta.total_count`" } ] } @@ -1544,11 +1544,11 @@ "properties": { "count": { "type": "integer", - "description": "Number of objects matching the given filters" + "description": "Number of objects accessible to the current user matching the given filters" }, "total_count": { "type": "integer", - "description": "Number of objects without any filter applied, only present when `include_total=true` was requested" + "description": "Number of objects accessible to the current user without any filter applied, only present when `include_total=true` was requested" } } },