Runtime linting of permission names

This commit is contained in:
Raphael Michel
2026-03-09 14:57:31 +01:00
parent 46cf0048c3
commit 24b0433c28
7 changed files with 132 additions and 18 deletions
+5 -3
View File
@@ -36,7 +36,9 @@ from rest_framework.permissions import SAFE_METHODS, BasePermission
from pretix.api.models import OAuthAccessToken
from pretix.base.models import Device, Event, User
from pretix.base.models.auth import SuperuserPermissionSet
from pretix.base.models.auth import (
EventPermissionSet, OrganizerPermissionSet, SuperuserPermissionSet,
)
from pretix.base.models.organizer import TeamAPIToken
from pretix.helpers.security import (
Session2FASetupRequired, SessionInvalid, SessionPasswordChangeRequired,
@@ -85,7 +87,7 @@ class EventPermission(BasePermission):
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
request.eventpermset = SuperuserPermissionSet()
else:
request.eventpermset = perm_holder.get_event_permission_set(request.organizer, request.event)
request.eventpermset = EventPermissionSet(perm_holder.get_event_permission_set(request.organizer, request.event))
if isinstance(required_permission, (list, tuple)):
if not any(p in request.eventpermset for p in required_permission):
@@ -100,7 +102,7 @@ class EventPermission(BasePermission):
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
request.orgapermset = SuperuserPermissionSet()
else:
request.orgapermset = perm_holder.get_organizer_permission_set(request.organizer)
request.orgapermset = OrganizerPermissionSet(perm_holder.get_organizer_permission_set(request.organizer))
if isinstance(required_permission, (list, tuple)):
if not any(p in request.eventpermset for p in required_permission):
+22
View File
@@ -212,6 +212,28 @@ class SuperuserPermissionSet:
return True
class EventPermissionSet(set):
def __contains__(self, item):
from pretix.base.permissions import assert_valid_event_permission
if super().__contains__(item):
return True
assert_valid_event_permission(item, allow_tuple=False)
return False
class OrganizerPermissionSet(set):
def __contains__(self, item):
from pretix.base.permissions import assert_valid_organizer_permission
if super().__contains__(item):
return True
assert_valid_organizer_permission(item, allow_tuple=False)
return False
class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
"""
This is the user model used by pretix for authentication.
+2
View File
@@ -29,6 +29,7 @@ from django.utils.translation import gettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled
from pretix.base.models import LoggedModel
from pretix.base.permissions import assert_valid_event_permission
@scopes_disabled()
@@ -276,6 +277,7 @@ class Device(LoggedModel):
:param request: Ignored, for compatibility with User model
:return: Iterable of Events
"""
assert_valid_event_permission(permission)
if (
isinstance(permission, (list, tuple)) and any(p in self._event_permission_set() for p in permission)
) or (isinstance(permission, str) and permission in self._event_permission_set()):
+12
View File
@@ -317,8 +317,11 @@ def generate_api_token():
class TeamQuerySet(models.QuerySet):
@classmethod
def event_permission_q(cls, perm_name):
from ..permissions import assert_valid_event_permission
if perm_name.startswith('can_') and perm_name in OLD_TO_NEW_EVENT_COMPAT: # legacy
return reduce(operator.and_, [cls.event_permission_q(p) for p in OLD_TO_NEW_EVENT_COMPAT[perm_name]])
assert_valid_event_permission(perm_name, allow_legacy=False)
return (
Q(all_event_permissions=True) |
Q(**{f'limit_event_permissions__{perm_name}': True})
@@ -326,8 +329,11 @@ class TeamQuerySet(models.QuerySet):
@classmethod
def organizer_permission_q(cls, perm_name):
from ..permissions import assert_valid_organizer_permission
if perm_name.startswith('can_') and perm_name in OLD_TO_NEW_ORGANIZER_COMPAT: # legacy
return reduce(operator.and_, [cls.organizer_permission_q(p) for p in OLD_TO_NEW_ORGANIZER_COMPAT[perm_name]])
assert_valid_organizer_permission(perm_name, allow_legacy=False)
return (
Q(all_organizer_permissions=True) |
Q(**{f'limit_organizer_permissions__{perm_name}': True})
@@ -436,13 +442,19 @@ class Team(LoggedModel):
return self.can_change_event_settings
def has_event_permission(self, perm_name):
from ..permissions import assert_valid_event_permission
if perm_name.startswith('can_') and hasattr(self, perm_name): # legacy
return getattr(self, perm_name)
assert_valid_event_permission(perm_name, allow_legacy=False)
return self.all_event_permissions or self.limit_event_permissions.get(perm_name, False)
def has_organizer_permission(self, perm_name):
from ..permissions import assert_valid_organizer_permission
if perm_name.startswith('can_') and hasattr(self, perm_name): # legacy
return getattr(self, perm_name)
assert_valid_organizer_permission(perm_name, allow_legacy=False)
return self.all_organizer_permissions or self.limit_organizer_permissions.get(perm_name, False)
def permission_for_event(self, event):
+76 -9
View File
@@ -21,8 +21,9 @@
#
import logging
import warnings
from collections import OrderedDict
from typing import Dict, List, NamedTuple, Tuple
from typing import Dict, List, NamedTuple, Set, Tuple
from django.dispatch import receiver
from django.utils.functional import Promise
@@ -33,6 +34,8 @@ from pretix.base.signals import (
)
logger = logging.getLogger(__name__)
_ALL_EVENT_PERMISSION_GROUPS = None
_ALL_ORGANIZER_PERMISSION_GROUPS = None
_ALL_EVENT_PERMISSIONS = None
_ALL_ORGANIZER_PERMISSIONS = None
@@ -52,10 +55,10 @@ class PermissionGroup(NamedTuple):
def get_all_event_permission_groups() -> Dict[str, PermissionGroup]:
global _ALL_EVENT_PERMISSIONS
global _ALL_EVENT_PERMISSION_GROUPS
if _ALL_EVENT_PERMISSIONS:
return _ALL_EVENT_PERMISSIONS
if _ALL_EVENT_PERMISSION_GROUPS:
return _ALL_EVENT_PERMISSION_GROUPS
types = OrderedDict()
for recv, ret in register_event_permission_groups.send(None):
@@ -64,15 +67,15 @@ def get_all_event_permission_groups() -> Dict[str, PermissionGroup]:
types[r.name] = r
else:
types[ret.name] = ret
_ALL_EVENT_PERMISSIONS = types
_ALL_EVENT_PERMISSION_GROUPS = types
return types
def get_all_organizer_permission_groups() -> Dict[str, PermissionGroup]:
global _ALL_ORGANIZER_PERMISSIONS
global _ALL_ORGANIZER_PERMISSION_GROUPS
if _ALL_ORGANIZER_PERMISSIONS:
return _ALL_ORGANIZER_PERMISSIONS
if _ALL_ORGANIZER_PERMISSION_GROUPS:
return _ALL_ORGANIZER_PERMISSION_GROUPS
types = OrderedDict()
for recv, ret in register_organizer_permission_groups.send(None):
@@ -81,10 +84,74 @@ def get_all_organizer_permission_groups() -> Dict[str, PermissionGroup]:
types[r.name] = r
else:
types[ret.name] = ret
_ALL_ORGANIZER_PERMISSIONS = types
_ALL_ORGANIZER_PERMISSION_GROUPS = types
return types
def get_all_event_permissions() -> Set[str]:
from pretix.helpers.permission_migration import OLD_TO_NEW_EVENT_COMPAT
global _ALL_EVENT_PERMISSIONS
if _ALL_EVENT_PERMISSIONS:
return _ALL_EVENT_PERMISSIONS
res = set(OLD_TO_NEW_EVENT_COMPAT.keys())
for pg in get_all_event_permission_groups().values():
for a in pg.actions:
res.add(f"{pg.name}:{a}")
_ALL_EVENT_PERMISSIONS = res
return res
def get_all_organizer_permissions() -> Set[str]:
from pretix.helpers.permission_migration import OLD_TO_NEW_ORGANIZER_COMPAT
global _ALL_ORGANIZER_PERMISSIONS
if _ALL_ORGANIZER_PERMISSIONS:
return _ALL_ORGANIZER_PERMISSIONS
res = set(OLD_TO_NEW_ORGANIZER_COMPAT.keys())
for pg in get_all_organizer_permission_groups().values():
for a in pg.actions:
res.add(f"{pg.name}:{a}")
_ALL_ORGANIZER_PERMISSIONS = res
return res
def assert_valid_event_permission(permission, allow_legacy=True, allow_tuple=True):
if permission is None:
return
if isinstance(permission, (list, tuple)) and allow_tuple:
for p in permission:
assert_valid_event_permission(p)
return
if not allow_legacy and ':' not in permission:
raise ValueError(f"Not allowed to use legacy permission '{permission}'")
all_permissions = get_all_event_permissions()
if permission not in all_permissions:
# Warning *and* exception because warning is silently caught when used in if statements in Django templates
warnings.warn(f"Use of undefined permission '{permission}'")
raise Exception(f"Undefined permission '{permission}'")
def assert_valid_organizer_permission(permission, allow_legacy=True, allow_tuple=True):
if permission is None:
return
if isinstance(permission, (list, tuple)) and allow_tuple:
for p in permission:
assert_valid_organizer_permission(p)
return
if not allow_legacy and ':' not in permission:
raise ValueError(f"Not allowed to use legacy permission '{permission}'")
all_permissions = get_all_organizer_permissions()
if permission not in all_permissions:
# Warning *and* exception because warning is silently caught when used in if statements in Django templates
warnings.warn(f"Use of undefined permission '{permission}'")
raise Exception(f"Undefined permission '{permission}'")
OPTS_ALL_READ = [
PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "View")),
PermissionOption(actions=("write",), label=pgettext_lazy("permission_level", "View and change")),
+5 -3
View File
@@ -45,7 +45,9 @@ from django.utils.translation import gettext as _
from django_scopes import scope
from pretix.base.models import Event, Organizer
from pretix.base.models.auth import SuperuserPermissionSet, User
from pretix.base.models.auth import (
EventPermissionSet, OrganizerPermissionSet, SuperuserPermissionSet, User,
)
from pretix.helpers.http import redirect_to_url
from pretix.helpers.security import (
Session2FASetupRequired, SessionInvalid, SessionPasswordChangeRequired,
@@ -170,7 +172,7 @@ class PermissionMiddleware:
if request.user.has_active_staff_session(request.session.session_key):
request.eventpermset = SuperuserPermissionSet()
else:
request.eventpermset = request.user.get_event_permission_set(request.organizer, request.event)
request.eventpermset = EventPermissionSet(request.user.get_event_permission_set(request.organizer, request.event))
elif 'organizer' in url.kwargs:
if url.kwargs['organizer'] == '-':
# This is a hack that just takes the user to ANY organizer. It's useful to link to features in support
@@ -192,7 +194,7 @@ class PermissionMiddleware:
if request.user.has_active_staff_session(request.session.session_key):
request.orgapermset = SuperuserPermissionSet()
else:
request.orgapermset = request.user.get_organizer_permission_set(request.organizer)
request.orgapermset = OrganizerPermissionSet(request.user.get_organizer_permission_set(request.organizer))
with scope(organizer=getattr(request, 'organizer', None)):
r = self.get_response(request)
+10 -3
View File
@@ -38,6 +38,9 @@ from django.core.exceptions import PermissionDenied
from django.urls import reverse
from django.utils.translation import gettext as _
from pretix.base.permissions import (
assert_valid_event_permission, assert_valid_organizer_permission,
)
from pretix.helpers.http import redirect_to_url
@@ -57,6 +60,8 @@ def event_permission_required(permission):
# Legacy support
permission = 'event.settings.general:write'
assert_valid_event_permission(permission)
def decorator(function):
def wrapper(request, *args, **kw):
if not request.user.is_authenticated: # NOQA
@@ -79,7 +84,7 @@ class EventPermissionRequiredMixin:
This mixin is equivalent to the event_permission_required view decorator but
is in a form suitable for class-based views.
"""
permission = ''
permission = None # None means "any permission"
@classmethod
def as_view(cls, **initkwargs):
@@ -92,10 +97,12 @@ def organizer_permission_required(permission):
This view decorator rejects all requests with a 403 response which are not from
users having the given permission for the event the request is associated with.
"""
if permission == 'event.settings.general:write':
if permission in ('event.settings.general:write', 'can_change_settings', 'can_change_event_settings'):
# Legacy support
permission = 'organizer.settings.general:write'
assert_valid_organizer_permission(permission)
def decorator(function):
def wrapper(request, *args, **kw):
if not request.user.is_authenticated: # NOQA
@@ -116,7 +123,7 @@ class OrganizerPermissionRequiredMixin:
This mixin is equivalent to the organizer_permission_required view decorator but
is in a form suitable for class-based views.
"""
permission = ''
permission = None # None means "any permission"
@classmethod
def as_view(cls, **initkwargs):