Pluggable permissions (#5728)

* Data model draft

* Refactor query and assignment usages of old permissions

* Backend UI

* API serializer

* Big string replace

* Docs, tests and fixes for teams api

* Update docs for device auth

* Eliminate old names

* Make tests pass

* Use new permissions, remove inconsistencies

* Add test for translations

* Show plugin permissions

* Add permission for seating plans

* Fix plugin activation

* Fix failing test

* Refactor to permission groups

* Update doc/api/resources/devices.rst

Co-authored-by: luelista <weller@rami.io>

* Update doc/api/resources/events.rst

Co-authored-by: luelista <weller@rami.io>

* Update src/pretix/api/serializers/organizer.py

Co-authored-by: luelista <weller@rami.io>

* Fix typo

* Fix python version compat

* Replacement after rebase

* Add proper permission handling for exports

* Docs for exporters

* Runtime linting of permission names

* Fix typos

* Show export page even without orders permission

* More legacy compat

* Do not strongly validate before plugins are loaded

* Rebase migration

* Add permission for outgoing mails

* Review notes

* Update doc/api/resources/teams.rst

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>

* Clean up logic around exporters

* Review and failures

* Fix migration leading to forbidden combination

* Handle permissions on event copying

* Remove print-statements

* Make test clearer

* Review feedback

* Add AnyPermissionOf

* migration safety

---------

Co-authored-by: luelista <weller@rami.io>
Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
This commit is contained in:
Raphael Michel
2026-03-17 14:43:56 +01:00
committed by GitHub
parent eddde2b6c0
commit df0b580dd6
203 changed files with 5374 additions and 2331 deletions

View File

@@ -62,6 +62,7 @@ from pretix.base.forms import (
)
from pretix.base.models import Event, Organizer, TaxRule, Team
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
from pretix.base.models.organizer import TeamQuerySet
from pretix.base.models.tax import TAX_CODE_LISTS
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.base.services.placeholders import FormPlaceholderMixin
@@ -100,11 +101,12 @@ class EventWizardFoundationForm(forms.Form):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
self.session = kwargs.pop('session')
self.clone_from = kwargs.pop('clone_from')
super().__init__(*args, **kwargs)
qs = Organizer.objects.all()
if not self.user.has_active_staff_session(self.session.session_key):
qs = qs.filter(
id__in=self.user.teams.filter(can_create_events=True).values_list('organizer', flat=True)
id__in=self.user.teams.filter(TeamQuerySet.organizer_permission_q("organizer.events:create")).values_list('organizer', flat=True)
)
self.fields['organizer'] = forms.ModelChoiceField(
label=_("Organizer"),
@@ -125,6 +127,16 @@ class EventWizardFoundationForm(forms.Form):
self.fields['organizer'].initial = organizer
self.fields['locales'].initial = organizer.settings.locales
def clean(self):
d = super().clean()
if d.get('organizer') and self.clone_from and not self.user.has_active_staff_session(self.session.session_key):
if not self.clone_from.allow_copy_data(d['organizer'], self.user):
raise ValidationError({
"organizer": _("You do not have a sufficient level of access on the event you selected "
"to copy it to the desired organizer.")
})
return d
class EventWizardBasicsForm(I18nModelForm):
error_messages = {
@@ -198,6 +210,7 @@ class EventWizardBasicsForm(I18nModelForm):
self.has_subevents = kwargs.pop('has_subevents')
self.user = kwargs.pop('user')
self.session = kwargs.pop('session')
self.clone_from = kwargs.pop('clone_from')
super().__init__(*args, **kwargs)
if 'timezone' not in self.initial:
self.initial['timezone'] = get_current_timezone_name()
@@ -238,6 +251,16 @@ class EventWizardBasicsForm(I18nModelForm):
'check "{field}" above.').format(field=self.fields["no_taxes"].label)
})
if self.clone_from and not self.user.has_active_staff_session(self.session.session_key):
if data.get("team"):
source_event_perms = self.user.get_event_permission_set(self.organizer, self.clone_from)
team_perms = data["team"].event_permission_set(include_legacy=False)
if any(t not in source_event_perms for t in team_perms):
raise ValidationError({
"team": _("You cannot choose a team that would give you more access than you have on "
"the event you are copying.")
})
# change timezone
zone = ZoneInfo(data.get('timezone'))
data['date_from'] = self.reset_timezone(zone, data.get('date_from'))
@@ -261,9 +284,12 @@ class EventWizardBasicsForm(I18nModelForm):
@staticmethod
def has_control_rights(user, organizer, session):
# It's mostly pointless to let a user create an event where they can't event change the name or create products,
# so we detect if the user has sufficient access for that on a new event.
return user.teams.filter(
organizer=organizer, all_events=True, can_change_event_settings=True, can_change_items=True,
can_change_orders=True, can_change_vouchers=True
TeamQuerySet.event_permission_q("event.settings.general:write"),
organizer=organizer,
all_events=True,
).exists() or user.has_active_staff_session(session.session_key)
@@ -293,18 +319,24 @@ class EventWizardCopyForm(forms.Form):
if user.has_active_staff_session(session.session_key):
return Event.objects.all()
return Event.objects.filter(
# It is generally pointless to let users copy events when they would not even be able to change the
# date of the event they have just created. Therefore, even if it looks wrong, we're checking a write
# permission for read access.
Q(organizer_id__in=user.teams.filter(
all_events=True, can_change_event_settings=True, can_change_items=True
TeamQuerySet.event_permission_q("event.settings.general:write"),
all_events=True,
).values_list('organizer', flat=True)) | Q(id__in=user.teams.filter(
can_change_event_settings=True, can_change_items=True
TeamQuerySet.event_permission_q("event.settings.general:write"),
).values_list('limit_events__id', flat=True))
)
def __init__(self, *args, **kwargs):
kwargs.pop('organizer')
self.organizer = kwargs.pop('organizer')
kwargs.pop('locales')
self.session = kwargs.pop('session')
self.team = kwargs.pop('team')
kwargs.pop('has_subevents')
kwargs.pop('clone_from')
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
@@ -323,6 +355,24 @@ class EventWizardCopyForm(forms.Form):
)
self.fields['copy_from_event'].widget.choices = self.fields['copy_from_event'].choices
def clean(self):
d = super().clean()
if d.get('copy_from_event') and not self.user.has_active_staff_session(self.session.session_key):
if not d['copy_from_event'].allow_copy_data(self.organizer, self.user):
raise ValidationError({
"copy_from_event": _("You do not have a sufficient level of access on the event you selected "
"to copy it to the desired organizer.")
})
if self.team:
source_event_perms = self.user.get_event_permission_set(self.organizer, d['copy_from_event'])
team_perms = self.team.event_permission_set(include_legacy=False)
if any(t not in source_event_perms for t in team_perms):
raise ValidationError({
"copy_from_event": _("You cannot choose an event on which you have less access than the "
"team you selected in the previous step.")
})
return d
class EventMetaValueForm(forms.ModelForm):