mirror of
https://github.com/pretix/pretix.git
synced 2026-08-20 12:26:27 +00:00
Runtime linting of permission names
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user