mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-09-09 19:31:47 +02:00
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
126 lines
3.8 KiB
Python
126 lines
3.8 KiB
Python
from django.db import models
|
|
from django.conf import settings
|
|
from django.db.models import Q
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from apps.common.middleware.thread_local import get_current_user
|
|
|
|
|
|
class SharedObjectManager(models.Manager):
|
|
def get_queryset(self):
|
|
"""Return only objects the user can access"""
|
|
user = get_current_user()
|
|
base_qs = super().get_queryset()
|
|
|
|
if user and user.is_authenticated:
|
|
return base_qs.filter(
|
|
Q(visibility="public")
|
|
| Q(owner=user)
|
|
| Q(shared_with=user)
|
|
| Q(visibility="private", owner=None)
|
|
).distinct()
|
|
|
|
return base_qs.filter(visibility="public")
|
|
|
|
|
|
class SharedObject(models.Model):
|
|
# Access control enum
|
|
class Visibility(models.TextChoices):
|
|
private = "private", _("Private")
|
|
is_paid = "public", _("Public")
|
|
|
|
# Core sharing fields
|
|
owner = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name="%(class)s_owned",
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("Owner"),
|
|
)
|
|
visibility = models.CharField(
|
|
max_length=10,
|
|
choices=Visibility.choices,
|
|
default=Visibility.private,
|
|
verbose_name=_("Visibility"),
|
|
)
|
|
shared_with = models.ManyToManyField(
|
|
settings.AUTH_USER_MODEL,
|
|
related_name="%(class)s_shared",
|
|
blank=True,
|
|
verbose_name=_("Shared with users"),
|
|
)
|
|
|
|
# Use as abstract base class
|
|
class Meta:
|
|
abstract = True
|
|
indexes = [
|
|
models.Index(fields=["visibility"]),
|
|
]
|
|
|
|
# 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:
|
|
self.owner = get_current_user()
|
|
super().save(*args, **kwargs)
|
|
|
|
|
|
class OwnedObjectManager(models.Manager):
|
|
def get_queryset(self):
|
|
"""Return only objects the user can access"""
|
|
user = get_current_user()
|
|
base_qs = super().get_queryset()
|
|
|
|
if user and user.is_authenticated:
|
|
return base_qs.filter(Q(owner=user) | Q(owner=None)).distinct()
|
|
|
|
return base_qs
|
|
|
|
|
|
class OwnedObject(models.Model):
|
|
owner = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name="%(class)s_owned",
|
|
null=True,
|
|
blank=True,
|
|
)
|
|
|
|
# Use as abstract base class
|
|
class Meta:
|
|
abstract = True
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.pk and not self.owner:
|
|
self.owner = get_current_user()
|
|
super().save(*args, **kwargs)
|