Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ci/apiv2/test_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})
1 change: 1 addition & 0 deletions ci/apiv2/test_agentassignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions ci/apiv2/test_agentstat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})
1 change: 1 addition & 0 deletions ci/apiv2/test_apitoken.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
28 changes: 27 additions & 1 deletion ci/apiv2/test_completed_count.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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")
32 changes: 31 additions & 1 deletion ci/apiv2/test_count.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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)
26 changes: 25 additions & 1 deletion ci/apiv2/test_cracks_per_day.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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")
1 change: 1 addition & 0 deletions ci/apiv2/test_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions ci/apiv2/test_hashlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})
9 changes: 6 additions & 3 deletions ci/apiv2/test_realworld_dump.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions ci/apiv2/test_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down
1 change: 1 addition & 0 deletions ci/apiv2/test_taskwrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})
19 changes: 19 additions & 0 deletions ci/apiv2/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions ci/phpunit/fixtures/openapi/config.spec.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions ci/phpunit/fixtures/openapi/crackerbinarytype.spec.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions ci/phpunit/fixtures/openapi/hashtype.spec.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading