mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-09-11 04:11:48 +02:00
fix(rules): enforce object-level authorization on rule endpoints
SharedObjectManager scopes querysets to what a user may see, which includes other people's public and shared-with-them objects. Several mutating rule endpoints treated that visibility as permission to write. Generalise get_owned_object_or_403 into get_shared_object_or_error, which takes an explicit access level instead of inferring one: - READ requires the object to be visible; denial is 404 so the response does not confirm that an id exists. - EDIT requires ownership; denial is 403, but only after the visibility check, so 403 never leaks the existence of an invisible object. This matters for TransactionRuleAction, whose manager is unscoped. The previous owner_path resolved to a User and discarded the object, so it could not express visibility at all. via= now points at the governing SharedObject and is resolved with a plain getattr, so an unresolvable path raises instead of silently granting access. is_visible_to/is_editable_by replace is_accessible_by, which was never called and tested visibility == "shared", a value that does not exist in Visibility. Also fixes two further holes in the same module: - transaction_rule_delete fell through to delete() whenever the caller was not in shared_with, so any user could delete a public rule. Now only the owner deletes; a shared user revokes their own access. - transaction_rule_view was read-only but is now explicitly READ, so rules shared with a user stay viewable. The activate/deactivate control is hidden for rules the user does not own, instead of rendering a button that always fails. Refs GHSA-83g9-vjqf-2j5q
This commit is contained in:
@@ -58,13 +58,35 @@ class SharedObject(models.Model):
|
||||
models.Index(fields=["visibility"]),
|
||||
]
|
||||
|
||||
def is_accessible_by(self, user):
|
||||
"""Check if a user can access this object"""
|
||||
return (
|
||||
self.visibility == "public"
|
||||
or self.owner == user
|
||||
or (self.visibility == "shared" and user in self.shared_with.all())
|
||||
)
|
||||
# NOTE: these two predicates must stay in sync with the ``Q`` objects built
|
||||
# by ``SharedObjectManager.get_queryset`` above. The manager filters at the
|
||||
# queryset level and these check a single instance, so they cannot share an
|
||||
# implementation; ``SharedObjectPredicateParityTests`` asserts they agree.
|
||||
def is_visible_to(self, user):
|
||||
"""Whether ``user`` may read this object.
|
||||
|
||||
Mirrors ``SharedObjectManager``: public objects, objects with no owner,
|
||||
the owner's own objects, and objects explicitly shared with the user.
|
||||
"""
|
||||
if self.owner is None or self.visibility == "public":
|
||||
return True
|
||||
|
||||
if not user or not user.is_authenticated:
|
||||
return False
|
||||
|
||||
return self.owner_id == user.pk or self.shared_with.filter(pk=user.pk).exists()
|
||||
|
||||
def is_editable_by(self, user):
|
||||
"""Whether ``user`` may mutate this object.
|
||||
|
||||
Sharing grants read access only; mutation stays with the owner. Objects
|
||||
with no owner remain editable by everyone, preserving the behaviour of
|
||||
legacy/unowned objects.
|
||||
"""
|
||||
if self.owner is None:
|
||||
return True
|
||||
|
||||
return bool(user and user.is_authenticated and self.owner_id == user.pk)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.pk and not self.owner:
|
||||
|
||||
Reference in New Issue
Block a user