mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-09-09 19:31:47 +02:00
Regression tests for GHSA-83g9-vjqf-2j5q, one per endpoint the advisory named plus the delete and view paths found alongside them: a non-owner gets 403 on every mutation of a public or shared rule, and the object is asserted unchanged afterwards. Also covers the parts that are easy to regress in the other direction: shared users keep read access, a shared user deleting only revokes their own access, unowned rules stay claimable, and children of invisible rules answer 404 rather than 403. SharedObjectPredicateParityTests asserts is_visible_to agrees with SharedObjectManager across every owner/visibility/shared combination. The manager builds a Q and the predicate tests an instance, so they cannot share an implementation and can otherwise drift apart.
270 lines
9.3 KiB
Python
270 lines
9.3 KiB
Python
from django.contrib.auth import get_user_model
|
|
from django.contrib.auth.models import AnonymousUser
|
|
from django.core.exceptions import PermissionDenied
|
|
from django.http import Http404
|
|
from django.test import RequestFactory, TestCase, override_settings
|
|
|
|
from apps.common.functions.permissions import (
|
|
EDIT,
|
|
READ,
|
|
get_shared_object_or_error,
|
|
)
|
|
from apps.common.middleware.thread_local import delete_current_user, write_current_user
|
|
from apps.rules.models import TransactionRule, TransactionRuleAction
|
|
|
|
|
|
@override_settings(
|
|
STORAGES={
|
|
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
|
"staticfiles": {
|
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"
|
|
},
|
|
},
|
|
WHITENOISE_AUTOREFRESH=True,
|
|
)
|
|
class SharedObjectPredicateTests(TestCase):
|
|
"""Unit tests for is_visible_to / is_editable_by on SharedObject."""
|
|
|
|
def setUp(self):
|
|
User = get_user_model()
|
|
self.owner = User.objects.create_user(
|
|
email="owner@test.com", password="testpass123"
|
|
)
|
|
self.shared_user = User.objects.create_user(
|
|
email="shared@test.com", password="testpass123"
|
|
)
|
|
self.stranger = User.objects.create_user(
|
|
email="stranger@test.com", password="testpass123"
|
|
)
|
|
|
|
def _rule(self, **kwargs):
|
|
kwargs.setdefault("name", "Rule")
|
|
kwargs.setdefault("trigger", "True")
|
|
return TransactionRule.all_objects.create(**kwargs)
|
|
|
|
def test_owner_can_read_and_edit_own_private_rule(self):
|
|
rule = self._rule(owner=self.owner, visibility="private")
|
|
|
|
self.assertTrue(rule.is_visible_to(self.owner))
|
|
self.assertTrue(rule.is_editable_by(self.owner))
|
|
|
|
def test_private_rule_is_invisible_to_stranger(self):
|
|
rule = self._rule(owner=self.owner, visibility="private")
|
|
|
|
self.assertFalse(rule.is_visible_to(self.stranger))
|
|
self.assertFalse(rule.is_editable_by(self.stranger))
|
|
|
|
def test_shared_rule_is_readable_but_not_editable(self):
|
|
rule = self._rule(owner=self.owner, visibility="private")
|
|
rule.shared_with.add(self.shared_user)
|
|
|
|
self.assertTrue(rule.is_visible_to(self.shared_user))
|
|
self.assertFalse(rule.is_editable_by(self.shared_user))
|
|
|
|
def test_public_rule_is_readable_but_not_editable(self):
|
|
rule = self._rule(owner=self.owner, visibility="public")
|
|
|
|
self.assertTrue(rule.is_visible_to(self.stranger))
|
|
self.assertFalse(rule.is_editable_by(self.stranger))
|
|
|
|
def test_unowned_rule_stays_readable_and_editable_by_everyone(self):
|
|
rule = self._rule(owner=None, visibility="private")
|
|
|
|
self.assertTrue(rule.is_visible_to(self.stranger))
|
|
self.assertTrue(rule.is_editable_by(self.stranger))
|
|
|
|
def test_anonymous_user_gets_no_access_to_owned_rules(self):
|
|
rule = self._rule(owner=self.owner, visibility="private")
|
|
|
|
self.assertFalse(rule.is_visible_to(AnonymousUser()))
|
|
self.assertFalse(rule.is_editable_by(AnonymousUser()))
|
|
|
|
|
|
@override_settings(
|
|
STORAGES={
|
|
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
|
"staticfiles": {
|
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"
|
|
},
|
|
},
|
|
WHITENOISE_AUTOREFRESH=True,
|
|
)
|
|
class SharedObjectPredicateParityTests(TestCase):
|
|
"""is_visible_to must agree with what SharedObjectManager returns.
|
|
|
|
The manager filters at the queryset level and the predicate checks a single
|
|
instance, so the two cannot share an implementation. This asserts they do
|
|
not drift apart.
|
|
"""
|
|
|
|
def setUp(self):
|
|
User = get_user_model()
|
|
self.owner = User.objects.create_user(
|
|
email="owner@test.com", password="testpass123"
|
|
)
|
|
self.other = User.objects.create_user(
|
|
email="other@test.com", password="testpass123"
|
|
)
|
|
self.addCleanup(self._clear_current_user)
|
|
|
|
def _clear_current_user(self):
|
|
try:
|
|
delete_current_user()
|
|
except AttributeError:
|
|
pass
|
|
|
|
def test_manager_and_predicate_agree_over_every_combination(self):
|
|
combinations = []
|
|
for owner in (self.owner, self.other, None):
|
|
for visibility in ("private", "public"):
|
|
for shared in (True, False):
|
|
rule = TransactionRule.all_objects.create(
|
|
name=f"{owner}-{visibility}-{shared}",
|
|
trigger="True",
|
|
owner=owner,
|
|
visibility=visibility,
|
|
)
|
|
if shared:
|
|
rule.shared_with.add(self.owner)
|
|
combinations.append(rule)
|
|
|
|
write_current_user(self.owner)
|
|
visible_ids = set(TransactionRule.objects.values_list("id", flat=True))
|
|
|
|
for rule in combinations:
|
|
with self.subTest(rule=rule.name):
|
|
self.assertEqual(
|
|
rule.id in visible_ids,
|
|
rule.is_visible_to(self.owner),
|
|
f"manager and is_visible_to disagree for {rule.name}",
|
|
)
|
|
|
|
|
|
@override_settings(
|
|
STORAGES={
|
|
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
|
"staticfiles": {
|
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"
|
|
},
|
|
},
|
|
WHITENOISE_AUTOREFRESH=True,
|
|
)
|
|
class GetSharedObjectOrErrorTests(TestCase):
|
|
def setUp(self):
|
|
User = get_user_model()
|
|
self.owner = User.objects.create_user(
|
|
email="owner@test.com", password="testpass123"
|
|
)
|
|
self.stranger = User.objects.create_user(
|
|
email="stranger@test.com", password="testpass123"
|
|
)
|
|
self.factory = RequestFactory()
|
|
|
|
self.public_rule = TransactionRule.all_objects.create(
|
|
name="Public", trigger="True", owner=self.owner, visibility="public"
|
|
)
|
|
self.private_rule = TransactionRule.all_objects.create(
|
|
name="Private", trigger="True", owner=self.owner, visibility="private"
|
|
)
|
|
self.public_action = TransactionRuleAction.objects.create(
|
|
rule=self.public_rule, field="notes", value="x"
|
|
)
|
|
self.private_action = TransactionRuleAction.objects.create(
|
|
rule=self.private_rule, field="notes", value="x"
|
|
)
|
|
|
|
self.addCleanup(self._clear_current_user)
|
|
|
|
def _clear_current_user(self):
|
|
try:
|
|
delete_current_user()
|
|
except AttributeError:
|
|
pass
|
|
|
|
def _request(self, user):
|
|
request = self.factory.get("/")
|
|
request.user = user
|
|
write_current_user(user)
|
|
return request
|
|
|
|
def test_read_allows_visible_object(self):
|
|
request = self._request(self.stranger)
|
|
|
|
rule = get_shared_object_or_error(
|
|
TransactionRule, request, id=self.public_rule.id, level=READ
|
|
)
|
|
|
|
self.assertEqual(rule, self.public_rule)
|
|
|
|
def test_edit_denies_visible_but_unowned_object_with_403(self):
|
|
request = self._request(self.stranger)
|
|
|
|
with self.assertRaises(PermissionDenied):
|
|
get_shared_object_or_error(
|
|
TransactionRule, request, id=self.public_rule.id, level=EDIT
|
|
)
|
|
|
|
def test_edit_allows_owner(self):
|
|
request = self._request(self.owner)
|
|
|
|
rule = get_shared_object_or_error(
|
|
TransactionRule, request, id=self.public_rule.id, level=EDIT
|
|
)
|
|
|
|
self.assertEqual(rule, self.public_rule)
|
|
|
|
def test_invisible_object_raises_404_not_403(self):
|
|
"""403 must not confirm the existence of an object the user cannot see."""
|
|
request = self._request(self.stranger)
|
|
|
|
with self.assertRaises(Http404):
|
|
get_shared_object_or_error(
|
|
TransactionRule, request, id=self.private_rule.id, level=EDIT
|
|
)
|
|
|
|
def test_via_traverses_to_the_governing_object(self):
|
|
request = self._request(self.stranger)
|
|
|
|
with self.assertRaises(PermissionDenied):
|
|
get_shared_object_or_error(
|
|
TransactionRuleAction,
|
|
request,
|
|
id=self.public_action.id,
|
|
level=EDIT,
|
|
via="rule",
|
|
)
|
|
|
|
def test_via_hides_children_of_invisible_parents_behind_404(self):
|
|
"""TransactionRuleAction has an unscoped manager, so the id is reachable."""
|
|
request = self._request(self.stranger)
|
|
|
|
with self.assertRaises(Http404):
|
|
get_shared_object_or_error(
|
|
TransactionRuleAction,
|
|
request,
|
|
id=self.private_action.id,
|
|
level=EDIT,
|
|
via="rule",
|
|
)
|
|
|
|
def test_unresolvable_via_path_fails_closed(self):
|
|
"""A typo'd path must raise, never silently grant access."""
|
|
request = self._request(self.stranger)
|
|
|
|
with self.assertRaises(AttributeError):
|
|
get_shared_object_or_error(
|
|
TransactionRuleAction,
|
|
request,
|
|
id=self.public_action.id,
|
|
level=EDIT,
|
|
via="rulee",
|
|
)
|
|
|
|
def test_unknown_level_is_rejected(self):
|
|
request = self._request(self.owner)
|
|
|
|
with self.assertRaises(ValueError):
|
|
get_shared_object_or_error(
|
|
TransactionRule, request, id=self.public_rule.id, level="write"
|
|
)
|