From ce84c8dff2fad90a93ee25a2b577bcc6ecd4e2bf Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 16 Dec 2025 17:16:29 +0100 Subject: [PATCH] Data model draft --- .../migrations/0297_pluggable_permissions.py | 129 ++++++++++++ src/pretix/base/models/auth.py | 18 +- src/pretix/base/models/organizer.py | 194 +++++++++--------- src/pretix/base/permissions.py | 111 ++++++++++ src/pretix/base/signals.py | 12 ++ src/pretix/control/forms/organizer.py | 10 +- src/pretix/control/views/dashboards.py | 2 +- src/pretix/helpers/permission_migration.py | 89 ++++++++ 8 files changed, 451 insertions(+), 114 deletions(-) create mode 100644 src/pretix/base/migrations/0297_pluggable_permissions.py create mode 100644 src/pretix/base/permissions.py create mode 100644 src/pretix/helpers/permission_migration.py diff --git a/src/pretix/base/migrations/0297_pluggable_permissions.py b/src/pretix/base/migrations/0297_pluggable_permissions.py new file mode 100644 index 0000000000..0b4dbc67d7 --- /dev/null +++ b/src/pretix/base/migrations/0297_pluggable_permissions.py @@ -0,0 +1,129 @@ +from django.db import migrations, models + +from pretix.helpers.permission_migration import ( + OLD_TO_NEW_EVENT_MIGRATION, OLD_TO_NEW_ORGANIZER_MIGRATION, +) + + +def migrate_teams_forward(apps, schema_editor): + Team = apps.get_model("pretixbase", "Team") + + for team in Team.objects.iterator(): + if all(getattr(team, k) for k in OLD_TO_NEW_EVENT_MIGRATION.keys() if k != "can_checkin_orders"): + team.all_event_permissions = True + team.limit_event_permissions = {} + else: + team.all_event_permissions = False + for k, v in OLD_TO_NEW_EVENT_MIGRATION.items(): + if getattr(team, k): + team.limit_event_permissions.update({kk: True for kk in v}) + + if all(getattr(team, k) for k in OLD_TO_NEW_ORGANIZER_MIGRATION.keys()): + team.all_organizer_permissions = True + team.limit_organizer_permissions = {} + else: + team.all_organizer_permissions = False + for k, v in OLD_TO_NEW_ORGANIZER_MIGRATION.items(): + if getattr(team, k): + team.limit_organizer_permissions.update({kk: True for kk in v}) + + team.save(update_fields=[ + "all_event_permissions", "limit_event_permissions", "all_organizer_permissions", "limit_organizer_permissions" + ]) + + +def migrate_teams_backward(apps, schema_editor): + Team = apps.get_model("pretixbase", "Team") + + for team in Team.objects.iterator(): + for k, v in OLD_TO_NEW_EVENT_MIGRATION.items(): + setattr(team, k, team.all_event_permissions or all(team.limit_event_permissions.get(kk) for kk in v)) + for k, v in OLD_TO_NEW_ORGANIZER_MIGRATION.items(): + setattr(team, k, team.all_organizer_permissions or all(team.limit_organizer_permissions.get(kk) for kk in v)) + team.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("pretixbase", "0296_invoice_invoice_from_state"), + ] + + operations = [ + migrations.AddField( + model_name="team", + name="all_event_permissions", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="team", + name="all_organizer_permissions", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="team", + name="limit_event_permissions", + field=models.JSONField(default=dict), + ), + migrations.AddField( + model_name="team", + name="limit_organizer_permissions", + field=models.JSONField(default=dict), + ), + migrations.RunPython( + migrate_teams_forward, + migrate_teams_backward, + ), + migrations.RemoveField( + model_name="team", + name="can_change_event_settings", + ), + migrations.RemoveField( + model_name="team", + name="can_change_items", + ), + migrations.RemoveField( + model_name="team", + name="can_change_orders", + ), + migrations.RemoveField( + model_name="team", + name="can_change_organizer_settings", + ), + migrations.RemoveField( + model_name="team", + name="can_change_teams", + ), + migrations.RemoveField( + model_name="team", + name="can_change_vouchers", + ), + migrations.RemoveField( + model_name="team", + name="can_checkin_orders", + ), + migrations.RemoveField( + model_name="team", + name="can_create_events", + ), + migrations.RemoveField( + model_name="team", + name="can_manage_customers", + ), + migrations.RemoveField( + model_name="team", + name="can_manage_gift_cards", + ), + migrations.RemoveField( + model_name="team", + name="can_manage_reusable_media", + ), + migrations.RemoveField( + model_name="team", + name="can_view_orders", + ), + migrations.RemoveField( + model_name="team", + name="can_view_vouchers", + ), + ] diff --git a/src/pretix/base/models/auth.py b/src/pretix/base/models/auth.py index 0d2ba97a27..7c0b917538 100644 --- a/src/pretix/base/models/auth.py +++ b/src/pretix/base/models/auth.py @@ -510,8 +510,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin): if teams: self._teamcache['e{}'.format(event.pk)] = teams if isinstance(perm_name, (tuple, list)): - return any([any(team.has_permission(p) for team in teams) for p in perm_name]) - if not perm_name or any([team.has_permission(perm_name) for team in teams]): + return any([any(team.has_event_permission(p) for team in teams) for p in perm_name]) + if not perm_name or any([team.has_event_permission(perm_name) for team in teams]): return True return False @@ -530,8 +530,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin): teams = self._get_teams_for_organizer(organizer) if teams: if isinstance(perm_name, (tuple, list)): - return any([any(team.has_permission(p) for team in teams) for p in perm_name]) - if not perm_name or any([team.has_permission(perm_name) for team in teams]): + return any([any(team.has_organizer_permission(p) for team in teams) for p in perm_name]) + if not perm_name or any([team.has_organizer_permission(perm_name) for team in teams]): return True return False @@ -562,14 +562,15 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin): :return: Iterable of Events """ from .event import Event + from .organizer import TeamQuerySet if request and self.has_active_staff_session(request.session.session_key): return Event.objects.all() if isinstance(permission, (tuple, list)): - q = reduce(operator.or_, [Q(**{p: True}) for p in permission]) + q = reduce(operator.or_, [TeamQuerySet.event_permission_q(p) for p in permission]) else: - q = Q(**{permission: True}) + q = TeamQuerySet.event_permission_q(permission) return Event.objects.filter( Q(organizer_id__in=self.teams.filter(q, all_events=True).values_list('organizer', flat=True)) @@ -602,14 +603,13 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin): :return: Iterable of Organizers """ from .event import Organizer + from .organizer import TeamQuerySet if request and self.has_active_staff_session(request.session.session_key): return Organizer.objects.all() - kwargs = {permission: True} - return Organizer.objects.filter( - id__in=self.teams.filter(**kwargs).values_list('organizer', flat=True) + id__in=self.teams.filter(TeamQuerySet.organizer_permission_q(permission)).values_list('organizer', flat=True) ) def has_active_staff_session(self, session_key=None): diff --git a/src/pretix/base/models/organizer.py b/src/pretix/base/models/organizer.py index 71cc083f45..04da1b5334 100644 --- a/src/pretix/base/models/organizer.py +++ b/src/pretix/base/models/organizer.py @@ -31,9 +31,10 @@ # Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under the License. - +import operator import string from datetime import date, datetime, time +from functools import reduce import pytz_deprecation_shim from django.conf import settings @@ -53,6 +54,10 @@ from i18nfield.strings import LazyI18nString from pretix.base.models.base import LoggedModel from pretix.base.validators import OrganizerSlugBanlistValidator +from ...helpers.permission_migration import ( + OLD_TO_NEW_EVENT_COMPAT, OLD_TO_NEW_ORGANIZER_COMPAT, + LegacyPermissionProperty, +) from ..settings import settings_hierarkey from .auth import User @@ -309,6 +314,32 @@ def generate_api_token(): return get_random_string(length=64, allowed_chars=string.ascii_lowercase + string.digits) +class TeamQuerySet(models.QuerySet): + @classmethod + def event_permission_q(cls, perm_name): + 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]]) + return ( + Q(all_event_permissions=True) | + Q(**{f'limit_event_permissions__{perm_name}': True}) + ) + + @classmethod + def organizer_permission_q(cls, perm_name): + 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]]) + return ( + Q(all_organizer_permissions=True) | + Q(**{f'limit_organizer_permissions__{perm_name}': True}) + ) + + def with_event_permission(self, perm_name): + return self.filter(self.event_permission_q(perm_name)) + + def with_organizer_permission(self, perm_name): + return self.filter(self.organizer_permission_q(perm_name)) + + class Team(LoggedModel): """ A team is a collection of people given certain access rights to one or more events of an organizer. @@ -321,36 +352,10 @@ class Team(LoggedModel): :param all_events: Whether this team has access to all events of this organizer :type all_events: bool :param limit_events: A set of events this team has access to. Irrelevant if ``all_events`` is ``True``. - :param can_create_events: Whether or not the members can create new events with this organizer account. - :type can_create_events: bool - :param can_change_teams: If ``True``, the members can change the teams of this organizer account. - :type can_change_teams: bool - :param can_manage_customers: If ``True``, the members can view and change organizer-level customer accounts. - :type can_manage_customers: bool - :param can_manage_reusable_media: If ``True``, the members can view and change organizer-level reusable media. - :type can_manage_reusable_media: bool - :param can_change_organizer_settings: If ``True``, the members can change the settings of this organizer account. - :type can_change_organizer_settings: bool - :param can_change_event_settings: If ``True``, the members can change the settings of the associated events. - :type can_change_event_settings: bool - :param can_change_items: If ``True``, the members can change and add items and related objects for the associated events. - :type can_change_items: bool - :param can_view_orders: If ``True``, the members can inspect details of all orders of the associated events. - :type can_view_orders: bool - :param can_change_orders: If ``True``, the members can change details of orders of the associated events. - :type can_change_orders: bool - :param can_checkin_orders: If ``True``, the members can perform check-in related actions. - :type can_checkin_orders: bool - :param can_view_vouchers: If ``True``, the members can inspect details of all vouchers of the associated events. - :type can_view_vouchers: bool - :param can_change_vouchers: If ``True``, the members can change and create vouchers for the associated events. - :type can_change_vouchers: bool """ organizer = models.ForeignKey(Organizer, related_name="teams", on_delete=models.CASCADE) name = models.CharField(max_length=190, verbose_name=_("Team name")) members = models.ManyToManyField(User, related_name="teams", verbose_name=_("Team members")) - all_events = models.BooleanField(default=False, verbose_name=_("All events (including newly created ones)")) - limit_events = models.ManyToManyField('Event', verbose_name=_("Limit to events"), blank=True) require_2fa = models.BooleanField( default=False, verbose_name=_("Require all members of this team to use two-factor authentication"), help_text=_("If you turn this on, all members of the team will be required to either set up two-factor " @@ -358,62 +363,33 @@ class Team(LoggedModel): "all users.") ) - can_create_events = models.BooleanField( - default=False, - verbose_name=_("Can create events"), - ) - can_change_teams = models.BooleanField( - default=False, - verbose_name=_("Can change teams and permissions"), - ) - can_change_organizer_settings = models.BooleanField( - default=False, - verbose_name=_("Can change organizer settings"), - help_text=_('Someone with this setting can get access to most data of all of your events, i.e. via privacy ' - 'reports, so be careful who you add to this team!') - ) - can_manage_customers = models.BooleanField( - default=False, - verbose_name=_("Can manage customer accounts") - ) - can_manage_reusable_media = models.BooleanField( - default=False, - verbose_name=_("Can manage reusable media") - ) - can_manage_gift_cards = models.BooleanField( - default=False, - verbose_name=_("Can manage gift cards") - ) - can_change_event_settings = models.BooleanField( - default=False, - verbose_name=_("Can change event settings") - ) - can_change_items = models.BooleanField( - default=False, - verbose_name=_("Can change product settings") - ) - can_view_orders = models.BooleanField( - default=False, - verbose_name=_("Can view orders") - ) - can_change_orders = models.BooleanField( - default=False, - verbose_name=_("Can change orders") - ) - can_checkin_orders = models.BooleanField( - default=False, - verbose_name=_("Can perform check-ins"), - help_text=_('This includes searching for attendees, which can be used to obtain personal information about ' - 'attendees. Users with "can change orders" can also perform check-ins.') - ) - can_view_vouchers = models.BooleanField( - default=False, - verbose_name=_("Can view vouchers") - ) - can_change_vouchers = models.BooleanField( - default=False, - verbose_name=_("Can change vouchers") - ) + # Scope + all_events = models.BooleanField(default=False, verbose_name=_("All events (including newly created ones)")) + limit_events = models.ManyToManyField('Event', verbose_name=_("Limit to events"), blank=True) + + # Permissions + # We store them as {key: True} instead of [key] because otherwise not all lookups we need are supported on SQLite + all_event_permissions = models.BooleanField(default=False, verbose_name=_("All event permissions")) + limit_event_permissions = models.JSONField(default=dict, verbose_name=_("Event permissions")) + all_organizer_permissions = models.BooleanField(default=False, verbose_name=_("All organizer permissions")) + limit_organizer_permissions = models.JSONField(default=dict, verbose_name=_("Organizer permissions")) + + # Legacy lookups for plugin compatibility + can_change_event_settings = LegacyPermissionProperty() + can_change_items = LegacyPermissionProperty() + can_view_orders = LegacyPermissionProperty() + can_change_orders = LegacyPermissionProperty() + can_checkin_orders = LegacyPermissionProperty() + can_view_vouchers = LegacyPermissionProperty() + can_change_vuchers = LegacyPermissionProperty() + can_create_events = LegacyPermissionProperty() + can_change_organizer_settings = LegacyPermissionProperty() + can_change_teams = LegacyPermissionProperty() + can_manage_gift_cards = LegacyPermissionProperty() + can_manage_customers = LegacyPermissionProperty() + can_manage_reusable_media = LegacyPermissionProperty() + + objects = TeamQuerySet.as_manager() def __str__(self) -> str: return _("%(name)s on %(object)s") % { @@ -421,21 +397,45 @@ class Team(LoggedModel): 'object': str(self.organizer), } - def permission_set(self) -> set: - attribs = dir(self) - return { - a for a in attribs if a.startswith('can_') and self.has_permission(a) - } + def permission_set(self, include_legacy=True) -> set: + from ..permissions import ( + get_all_event_permissions, get_all_organizer_permissions, + ) + + result = set() + for permission in get_all_event_permissions(): + if self.all_event_permissions or self.limit_event_permissions.get(permission.name): + result.add(permission.name) + + for permission in get_all_organizer_permissions(): + if self.all_organizer_permissions or self.limit_organizer_permissions.get(permission.name): + result.add(permission.name) + + if include_legacy: + # Add legacy permissions as well for plugin compatibility + for k, v in OLD_TO_NEW_EVENT_COMPAT.items(): + if self.all_event_permissions or all(self.limit_event_permissions.get(kk) for kk in v): + result.add(k) + + for k, v in OLD_TO_NEW_ORGANIZER_COMPAT.items(): + if self.all_organizer_permissions or all(self.limit_organizer_permissions.get(kk) for kk in v): + result.add(k) + + return result @property - def can_change_settings(self): # Legacy compatiblilty + def can_change_settings(self): # Legacy compatibility return self.can_change_event_settings - def has_permission(self, perm_name): - try: + def has_event_permission(self, perm_name): + if perm_name.startswith('can_') and hasattr(self, perm_name): # legacy return getattr(self, perm_name) - except AttributeError: - raise ValueError('Invalid required permission: %s' % perm_name) + return self.all_event_permissions or self.limit_event_permissions.get(perm_name, False) + + def has_organizer_permission(self, perm_name): + if perm_name.startswith('can_') and hasattr(self, perm_name): # legacy + return getattr(self, perm_name) + return self.all_organizer_permissions or self.limit_organizer_permissions.get(perm_name, False) def permission_for_event(self, event): if self.all_events: @@ -529,8 +529,8 @@ class TeamAPIToken(models.Model): event in self.team.limit_events.all() ) if isinstance(perm_name, (tuple, list)): - return has_event_access and any(self.team.has_permission(p) for p in perm_name) - return has_event_access and (not perm_name or self.team.has_permission(perm_name)) + return has_event_access and any(self.team.has_event_permission(p) for p in perm_name) + return has_event_access and (not perm_name or self.team.has_event_permission(perm_name)) def has_organizer_permission(self, organizer, perm_name=None, request=None): """ @@ -543,8 +543,8 @@ class TeamAPIToken(models.Model): :return: bool """ if isinstance(perm_name, (tuple, list)): - return organizer == self.team.organizer and any(self.team.has_permission(p) for p in perm_name) - return organizer == self.team.organizer and (not perm_name or self.team.has_permission(perm_name)) + return organizer == self.team.organizer and any(self.team.has_organizer_permission(p) for p in perm_name) + return organizer == self.team.organizer and (not perm_name or self.team.has_organizer_permission(perm_name)) def get_events_with_any_permission(self): """ diff --git a/src/pretix/base/permissions.py b/src/pretix/base/permissions.py new file mode 100644 index 0000000000..46d6fbfb8f --- /dev/null +++ b/src/pretix/base/permissions.py @@ -0,0 +1,111 @@ +# +# 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 logging +from collections import OrderedDict, namedtuple + +from django.dispatch import receiver +from django.utils.translation import gettext_lazy as _, pgettext_lazy + +from pretix.base.signals import ( + register_event_permissions, register_organizer_permissions, +) + +logger = logging.getLogger(__name__) +_ALL_EVENT_PERMISSIONS = None +_ALL_ORGANIZER_PERMISSIONS = None + + +Permission = namedtuple('Permission', ('name', 'label', 'plugin_name', 'help_text')) + + +def get_all_event_permissions(): + global _ALL_EVENT_PERMISSIONS + + if _ALL_EVENT_PERMISSIONS: + return _ALL_EVENT_PERMISSIONS + + types = OrderedDict() + for recv, ret in register_event_permissions.send(None): + if isinstance(ret, (list, tuple)): + for r in ret: + types[r.name] = r + else: + types[ret.name] = ret + _ALL_EVENT_PERMISSIONS = types + return types + + +def get_all_organizer_permissions(): + global _ALL_ORGANIZER_PERMISSIONS + + if _ALL_ORGANIZER_PERMISSIONS: + return _ALL_ORGANIZER_PERMISSIONS + + types = OrderedDict() + for recv, ret in register_organizer_permissions.send(None): + if isinstance(ret, (list, tuple)): + for r in ret: + types[r.name] = r + else: + types[ret.name] = ret + _ALL_ORGANIZER_PERMISSIONS = types + return types + + +@receiver(register_event_permissions, dispatch_uid="base_register_default_event_permissions") +def register_default_event_permissions(sender, **kwargs): + return [ + Permission("event.settings.general:write", _("Change general settings"), None, None), + Permission("event.settings.payment:write", _("Change payment settings"), None, None), + Permission("event.settings.plugins:write", _("Change plugin settings"), None, None), + Permission("event.settings.email.sender:write", _("Change email sending settings"), None, None), + Permission("event.settings.tax:write", _("Change tax rules"), None, None), + Permission("event.settings.invoicing:write", _("Change invoicing settings"), None, None), + Permission("event.subevents:write", pgettext_lazy("subevent", "Change event series dates"), None, None), + Permission("event.items:write", _("Change products and quotas"), None, None), # and questions but that might change? + Permission("event.orders:read", _("View orders"), None, None), + Permission("event.orders:write", _("Change orders"), None, _("This includes the ability to cancel and refund individual orders.")), + Permission("event.orders:checkin", _("Check-in orders"), None, None), + Permission("event:cancel", pgettext_lazy("subevent", "Cancel the entire event or date"), None, None), + Permission("event.vouchers:read", _("View vouchers"), None, None), + Permission("event.vouchers:write", _("Change vouchers"), None, None), + ] + + +@receiver(register_organizer_permissions, dispatch_uid="base_register_default_organizer_permissions") +def register_default_organizer_permissions(sender, **kwargs): + return [ + Permission("organizer.events:create", _("Create events"), None, None), + Permission("organizer.settings.general:write", _("Change settings"), None, None), + Permission("organizer.teams:write", _("Change teams"), None, + _("This includes the ability to give someone (including oneself) additional permissions.")), + Permission("organizer.giftcards:read", _("View gift cards"), None, None), + Permission("organizer.giftcards:write", _("Change gift cards"), None, None), + Permission("organizer.customers:read", _("View customer accounts"), None, None), + Permission("organizer.customers:write", _("Change customer accounts"), None, None), + Permission("organizer.reusablemedia:read", _("View reusable media"), None, None), + Permission("organizer.reusablemedia:write", _("Change reusable media"), None, None), + Permission("organizer.devices:read", _("View devices"), None, None), + Permission("organizer.devices:write", _("Change devices"), None, + _("This inclues the ability to give access to events and date oneself does not have access to.")), + ] diff --git a/src/pretix/base/signals.py b/src/pretix/base/signals.py index c917f318bd..aa42950139 100644 --- a/src/pretix/base/signals.py +++ b/src/pretix/base/signals.py @@ -561,6 +561,18 @@ however for this signal, the ``sender`` **may also be None** to allow creating t notification settings! """ +register_event_permissions = GlobalSignal() +""" +This signal is sent out to get all known permissions. Receivers should return an +instance of pretix.base.permissions.Permission or a list of such instances. +""" + +register_organizer_permissions = GlobalSignal() +""" +This signal is sent out to get all known permissions. Receivers should return an +instance of pretix.base.permissions.Permission or a list of such instances. +""" + notification = EventPluginSignal() """ Arguments: ``logentry_id``, ``notification_type`` diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py index 06743457bb..075aaac2aa 100644 --- a/src/pretix/control/forms/organizer.py +++ b/src/pretix/control/forms/organizer.py @@ -308,13 +308,9 @@ class TeamForm(forms.ModelForm): class Meta: model = Team - fields = ['name', 'require_2fa', 'all_events', 'limit_events', 'can_create_events', - 'can_change_teams', 'can_change_organizer_settings', - 'can_manage_gift_cards', 'can_manage_customers', - 'can_manage_reusable_media', - 'can_change_event_settings', 'can_change_items', - 'can_view_orders', 'can_change_orders', 'can_checkin_orders', - 'can_view_vouchers', 'can_change_vouchers'] + fields = ['name', 'require_2fa', 'all_events', 'limit_events', + 'all_event_permissions', 'limit_event_permissions', + 'all_organizer_permissions', 'limit_organizer_permissions'] widgets = { 'limit_events': forms.CheckboxSelectMultiple(attrs={ 'data-inverse-dependency': '#id_all_events', diff --git a/src/pretix/control/views/dashboards.py b/src/pretix/control/views/dashboards.py index 941e7f5ee5..25486f7c22 100644 --- a/src/pretix/control/views/dashboards.py +++ b/src/pretix/control/views/dashboards.py @@ -635,7 +635,7 @@ def user_index(request): ctx = { 'widgets': rearrange(widgets), - 'can_create_event': request.user.teams.filter(can_create_events=True).exists(), + 'can_create_event': request.user.teams.with_organizer_permission("organizer:events.create").exists(), 'upcoming': widgets_for_event_qs( request, annotated_event_query(request, lazy=True).filter( diff --git a/src/pretix/helpers/permission_migration.py b/src/pretix/helpers/permission_migration.py new file mode 100644 index 0000000000..b810e63a2a --- /dev/null +++ b/src/pretix/helpers/permission_migration.py @@ -0,0 +1,89 @@ +# +# 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 warnings + +OLD_TO_NEW_EVENT_MIGRATION = { + "can_change_event_settings": [ + "event.settings.general:write", + "event.settings.payment:write", + "event.settings.plugins:write", + "event.settings.email.sender:write", + "event.settings.tax:write" + "event.settings.invoicing:write", + "event.subevents:write", + ], + "can_change_items": ["event.items:write"], + "can_view_orders": ["event.orders:read"], + "can_change_orders": ["event.orders:write", "event:cancel"], + "can_checkin_orders": ["event.orders:checkin"], + "can_view_vouchers": ["event.vouchers:read"], + "can_change_vouchers": ["event.vouchers:write"], +} +OLD_TO_NEW_ORGANIZER_MIGRATION = { + "can_create_events": ["organizer.events:create"], + "can_change_organizer_settings": ["organizer.settings.general:write", "organizer.devices:read", + "organizer.devices:write"], + "can_change_teams": ["organizer.teams:write"], + "can_manage_gift_cards": ["organizer.giftcards:read", "organizer.giftcards:write"], + "can_manage_customers": ["organizer.customers:read", "organizer.customers:write"], + "can_manage_reusable_media": ["organizer.reusablemedia:read", "organizer.reusablemedia:write"], +} +OLD_TO_NEW_EVENT_COMPAT = { + "can_change_event_settings": ["event.settings.general:write",], + "can_change_items": ["event.items:write"], + "can_view_orders": ["event.orders:read"], + "can_change_orders": ["event.orders:write"], + "can_checkin_orders": ["event.orders:checkin"], + "can_view_vouchers": ["event.vouchers:read"], + "can_change_vouchers": ["event.vouchers:write"], +} +OLD_TO_NEW_ORGANIZER_COMPAT = { + "can_create_events": ["organizer.events:create"], + "can_change_organizer_settings": ["organizer.settings.general:write"], + "can_change_teams": ["organizer.teams:write"], + "can_manage_gift_cards": ["organizer.giftcards:read", "organizer.giftcards:write"], + "can_manage_customers": ["organizer.customers:read", "organizer.customers:write"], + "can_manage_reusable_media": ["organizer.reusablemedia:read", "organizer.reusablemedia:write"], +} + + +class LegacyPermissionProperty: + name = None + + def __set_name__(self, owner, name): + self.name = name + + def __get__(self, instance, owner=None): + if instance is None: + return self + + warnings.warn("Legacy permission attribute used", DeprecationWarning, stacklevel=2) + + if self.name in OLD_TO_NEW_EVENT_COMPAT: + return instance.all_event_permissions or all( + kk in instance.limit_event_permissions for kk in OLD_TO_NEW_EVENT_COMPAT[self.name] + ) + if self.name in OLD_TO_NEW_ORGANIZER_COMPAT: + return instance.all_organizer_permissions or all( + kk in instance.limit_organizer_permissions for kk in OLD_TO_NEW_ORGANIZER_COMPAT[self.name] + ) + raise AttributeError("Unknown legacy attribute")