feat(core): add cross-club scope validator
Add clubmanager.base.validate_club_scope(instance, owning_club_id, ...): a shared model-clean() helper that rejects FKs leaking across clubs. Club-scoped FKs must share the owning club; Member FKs must have a ClubMembership in it. Unset FKs are skipped. Nothing enforced tenant consistency on the FKs between club-scoped rows, so an order could reference another club's product, an event another club's season, and so on. The following commits wire this into each app's clean(). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
0
club/services/__init__.py
Normal file
0
club/services/__init__.py
Normal file
0
club/services/access.py
Normal file
0
club/services/access.py
Normal file
@@ -1,8 +1,10 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from club.tenancy import require_current_club
|
from club.tenancy import require_current_club
|
||||||
|
|
||||||
@@ -10,6 +12,30 @@ if TYPE_CHECKING:
|
|||||||
from club.models import Club
|
from club.models import Club
|
||||||
|
|
||||||
|
|
||||||
|
def validate_club_scope(instance, owning_club_id, *, same_club_fields=(), member_fields=()):
|
||||||
|
"""Reject FKs that leak across clubs.
|
||||||
|
|
||||||
|
``same_club_fields`` are FKs to club-scoped models that must share
|
||||||
|
``owning_club_id``; ``member_fields`` are Member FKs whose target must have
|
||||||
|
a ClubMembership in that club. Unset (None) FKs are skipped. Call from a
|
||||||
|
model's ``clean()``.
|
||||||
|
"""
|
||||||
|
errors = {}
|
||||||
|
for field in same_club_fields:
|
||||||
|
if getattr(instance, f"{field}_id") is not None and getattr(instance, field).club_id != owning_club_id:
|
||||||
|
errors[field] = _("Must belong to the same club.")
|
||||||
|
|
||||||
|
if member_fields:
|
||||||
|
from club.models import ClubMembership
|
||||||
|
|
||||||
|
for field in member_fields:
|
||||||
|
if getattr(instance, f"{field}_id") is not None and not ClubMembership.objects.filter(club_id=owning_club_id, member=getattr(instance, field)).exists():
|
||||||
|
errors[field] = _("Must be a member of this club.")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
raise ValidationError(errors)
|
||||||
|
|
||||||
|
|
||||||
def unique_slugify(instance, value, *, slug_field="slug", scope=None):
|
def unique_slugify(instance, value, *, slug_field="slug", scope=None):
|
||||||
"""Return a slug derived from ``value``, unique within ``scope``.
|
"""Return a slug derived from ``value``, unique within ``scope``.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user