# # This file is part of pretix (Community Edition). # # Copyright (C) 2014-2020 Raphael Michel and contributors # Copyright (C) 2020-today pretix GmbH and contributors # # This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General # Public License as published by the Free Software Foundation in version 3 of the License. # # ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are # applicable granting you additional permissions and placing additional restrictions on your usage of this software. # Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive # this file, see . # # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more # details. # # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # import functools import logging import warnings from collections import OrderedDict from typing import Callable, Dict, List, NamedTuple, Set, Tuple from django.apps import apps from django.dispatch import receiver from django.utils.functional import Promise from django.utils.translation import gettext_lazy as _, pgettext_lazy from pretix.base.signals import ( register_event_permission_groups, register_organizer_permission_groups, ) logger = logging.getLogger(__name__) def cache_until_change(input_value: Callable): def decorator(func): old_input_value = None cached_result = None @functools.wraps(func) def wrapper(): nonlocal cached_result, old_input_value if cached_result is None or old_input_value != input_value(): cached_result = func() old_input_value = input_value() return cached_result return wrapper return decorator class PermissionOption(NamedTuple): actions: Tuple[str, ...] label: str | Promise help_text: str | Promise = None class PermissionGroup(NamedTuple): name: str label: str | Promise actions: List[str] options: List[PermissionOption] help_text: str | Promise = None @cache_until_change(input_value=lambda: apps.ready) def get_all_event_permission_groups() -> Dict[str, PermissionGroup]: types = OrderedDict() for recv, ret in register_event_permission_groups.send(None): if isinstance(ret, (list, tuple)): for r in ret: types[r.name] = r else: types[ret.name] = ret return types @cache_until_change(input_value=lambda: apps.ready) def get_all_organizer_permission_groups() -> Dict[str, PermissionGroup]: types = OrderedDict() for recv, ret in register_organizer_permission_groups.send(None): if isinstance(ret, (list, tuple)): for r in ret: types[r.name] = r else: types[ret.name] = ret return types @cache_until_change(input_value=lambda: apps.ready) def get_all_event_permissions() -> Set[str]: from pretix.helpers.permission_migration import OLD_TO_NEW_EVENT_COMPAT 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}") return res @cache_until_change(input_value=lambda: apps.ready) def get_all_organizer_permissions() -> Set[str]: from pretix.helpers.permission_migration import OLD_TO_NEW_ORGANIZER_COMPAT 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}") return res def assert_valid_event_permission(permission, allow_legacy=True, allow_tuple=True): if not apps.ready: # can't really check yet return if allow_legacy and permission == "can_change_settings": permission = "can_change_event_settings" if permission is None: return if isinstance(permission, (AnyPermissionOf, 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 not apps.ready: # can't really check yet return if permission is None: return if isinstance(permission, (AnyPermissionOf, 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}'") class AnyPermissionOf(list): def __init__(self, *items): super().__init__(items) OPTS_ALL_READ = [ PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "View")), PermissionOption(actions=("write",), label=pgettext_lazy("permission_level", "View and change")), ] OPTS_ALL_READ_SETTINGS_API = [ PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "View"), help_text=_("API only")), PermissionOption(actions=("write",), label=pgettext_lazy("permission_level", "View and change")), ] OPTS_ALL_READ_SETTINGS_PARENT = [ PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "View"), help_text=_("Menu item will only show up if the user has permission for general settings.")), PermissionOption(actions=("write",), label=pgettext_lazy("permission_level", "View and change")), ] OPTS_READ_WRITE = [ PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "No access")), PermissionOption(actions=("read",), label=pgettext_lazy("permission_level", "View")), PermissionOption(actions=("read", "write"), label=pgettext_lazy("permission_level", "View and change")), ] @receiver(register_event_permission_groups, dispatch_uid="base_register_default_event_permissions") def register_default_event_permissions(sender, **kwargs): return [ PermissionGroup( name="event.settings.general", label=_("General settings"), actions=["write"], options=OPTS_ALL_READ_SETTINGS_API, help_text=_( "This includes access to all settings not listed explicitly below, including plugin settings." ), ), PermissionGroup( name="event.settings.payment", label=_("Payment settings"), actions=["write"], options=OPTS_ALL_READ_SETTINGS_PARENT, ), PermissionGroup( name="event.settings.tax", label=_("Tax settings"), actions=["write"], options=OPTS_ALL_READ_SETTINGS_PARENT, ), PermissionGroup( name="event.settings.invoicing", label=_("Invoicing settings"), actions=["write"], options=OPTS_ALL_READ_SETTINGS_PARENT, ), PermissionGroup( name="event.subevents", label=_("Event series dates"), actions=["write"], options=OPTS_ALL_READ, ), PermissionGroup( name="event.items", label=_("Products, quotas and questions"), actions=["write"], options=OPTS_ALL_READ, help_text=_("Also includes related objects like categories or discounts."), ), PermissionGroup( name="event.orders", label=_("Orders"), actions=["read", "write", "checkin"], options=[ PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "No access")), PermissionOption(actions=("checkin",), label=pgettext_lazy("permission_level", "Only check-in")), PermissionOption(actions=("read",), label=pgettext_lazy("permission_level", "View all")), PermissionOption(actions=("read", "checkin"), label=pgettext_lazy("permission_level", "View all and check-in")), PermissionOption(actions=("read", "write"), label=pgettext_lazy("permission_level", "View all and change"), help_text=_("Includes the ability to cancel and refund individual orders.")), ], help_text=_("Also includes related objects like the waiting list."), ), PermissionGroup( name="event.vouchers", label=_("Vouchers"), actions=["read", "write"], options=OPTS_READ_WRITE, ), PermissionGroup( name="event", label=_("Full event or date cancellation"), actions=["cancel"], options=[ # If we ever add more actions, we need a new UI idea here PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "Not allowed")), PermissionOption(actions=("cancel",), label=pgettext_lazy("permission_level", "Allowed")), ], help_text="", ), ] @receiver(register_organizer_permission_groups, dispatch_uid="base_register_default_organizer_permissions") def register_default_organizer_permissions(sender, **kwargs): return [ PermissionGroup( name="organizer.events", label=_("Events"), actions=["create"], options=[ PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "Access existing events")), PermissionOption(actions=("create",), label=pgettext_lazy("permission_level", "Access existing and create new events")), ], help_text=_("The level of access to events is determined in detail by the settings below."), ), PermissionGroup( name="organizer.settings.general", label=_("Settings"), actions=["write"], options=OPTS_ALL_READ_SETTINGS_API, help_text=_("This includes access to all organizer-level functionality not listed explicitly below, including plugin settings."), ), PermissionGroup( name="organizer.teams", label=_("Teams"), actions=["write"], options=[ PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "No access")), PermissionOption(actions=("write",), label=pgettext_lazy("permission_level", "View and change"), help_text=_("Includes the ability to give someone (including oneself) additional permissions.")), ], ), PermissionGroup( name="organizer.giftcards", label=_("Gift cards"), actions=["read", "write"], options=OPTS_READ_WRITE, ), PermissionGroup( name="organizer.customers", label=_("Customers"), actions=["read", "write"], options=OPTS_READ_WRITE, ), PermissionGroup( name="organizer.reusablemedia", label=_("Reusable media"), actions=["read", "write"], options=OPTS_READ_WRITE, ), PermissionGroup( name="organizer.devices", label=_("Devices"), actions=["read", "write"], options=[ PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "No access")), PermissionOption(actions=("read",), label=pgettext_lazy("permission_level", "View")), PermissionOption(actions=("read", "write"), label=pgettext_lazy("permission_level", "View and change"), help_text=_("Includes the ability to give access to events and data oneself does not have access to.")), ], ), PermissionGroup( name="organizer.seatingplans", label=_("Seating plans"), actions=["write"], options=OPTS_ALL_READ, ), PermissionGroup( name="organizer.outgoingmails", label=_("Outgoing emails"), actions=["read"], options=[ PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "No access")), PermissionOption(actions=("read",), label=pgettext_lazy("permission_level", "View")), ], ), ]