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

@@ -51,6 +51,7 @@ from i18nfield.strings import LazyI18nString
from pretix.base.forms import SafeSessionWizardView
from pretix.base.i18n import language
from pretix.base.models import Event, EventMetaValue, Organizer, Quota, Team
from pretix.base.models.organizer import TeamQuerySet
from pretix.base.services.quotas import QuotaAvailability
from pretix.control.forms.event import (
EventWizardBasicsForm, EventWizardCopyForm, EventWizardFoundationForm,
@@ -195,7 +196,9 @@ class EventWizard(SafeSessionWizardView):
qs = Organizer.objects.all()
if not self.request.user.has_active_staff_session(self.request.session.session_key):
qs = qs.filter(
id__in=self.request.user.teams.filter(can_create_events=True).values_list('organizer', flat=True)
id__in=self.request.user.teams.filter(
TeamQuerySet.organizer_permission_q("organizer.events:create"),
).values_list('organizer', flat=True)
)
organizer = qs.get(slug=self.request.GET.get('organizer'))
initial['organizer'] = organizer
@@ -213,12 +216,7 @@ class EventWizard(SafeSessionWizardView):
except Event.DoesNotExist:
allow = False
else:
allow = (
request.user.has_event_permission(clone_from.organizer, clone_from,
'can_change_event_settings', request)
and request.user.has_event_permission(clone_from.organizer, clone_from,
'can_change_items', request)
)
allow = request.user.has_event_permission(clone_from.organizer, clone_from, None, request)
if not allow:
messages.error(self.request, _('You do not have permission to clone this event.'))
else:
@@ -227,7 +225,7 @@ class EventWizard(SafeSessionWizardView):
def get_context_data(self, form, **kwargs):
ctx = super().get_context_data(form, **kwargs)
ctx['has_organizer'] = self.request.user.teams.filter(can_create_events=True).exists()
ctx['has_organizer'] = self.request.user.teams.filter(TeamQuerySet.organizer_permission_q("organizer.events:create")).exists()
if self.steps.current == 'basics':
ctx['organizer'] = self.get_cleaned_data_for_step('foundation').get('organizer')
return ctx
@@ -244,6 +242,7 @@ class EventWizard(SafeSessionWizardView):
kwargs = {
'user': self.request.user,
'session': self.request.session,
'clone_from': self.clone_from,
}
if step != 'foundation':
fdata = self.get_cleaned_data_for_step('foundation')
@@ -255,6 +254,13 @@ class EventWizard(SafeSessionWizardView):
}
# The show must go on, we catch this error in render()
kwargs.update(fdata)
if step == 'copy':
bdata = self.get_cleaned_data_for_step('basics')
if bdata:
bdata = {
'team': bdata.get('team'),
}
kwargs.update(bdata)
return kwargs
def get_template_names(self):
@@ -279,31 +285,50 @@ class EventWizard(SafeSessionWizardView):
user=self.request.user,
)
if not EventWizardBasicsForm.has_control_rights(self.request.user, event.organizer, self.request.session):
if copy_data and copy_data['copy_from_event']:
copy_from_event = copy_data['copy_from_event']
elif self.clone_from:
copy_from_event = self.clone_from
else:
copy_from_event = None
if not EventWizardBasicsForm.has_control_rights(
self.request.user, event.organizer, self.request.session
):
if basics_data["team"] is not None:
t = basics_data["team"]
t.limit_events.add(event)
elif event.organizer.settings.event_team_provisioning:
# Create a new team for new events with full access, but for copied events with the same access
# as the source
limit_event_permissions = {}
if copy_from_event and copy_from_event.organizer == event.organizer:
source_teams = self.request.user._get_teams_for_event(copy_from_event.organizer, copy_from_event)
all_event_permissions = any(t.all_event_permissions for t in source_teams)
if not all_event_permissions:
for t in source_teams:
limit_event_permissions.update(t.limit_event_permissions)
else:
# The cross-organizer case is protected through allow_copy_data
all_event_permissions = True
t = Team.objects.create(
organizer=event.organizer,
name=_('Team {event}').format(
event=str(event.name)[:100] + "" if len(str(event.name)) > 100 else str(event.name)
),
can_change_event_settings=True, can_change_items=True,
can_view_orders=True, can_change_orders=True, can_view_vouchers=True,
can_change_vouchers=True
all_organizer_permissions=False,
all_event_permissions=all_event_permissions,
limit_event_permissions=limit_event_permissions,
)
t.members.add(self.request.user)
t.limit_events.add(event)
t.log_action('pretix.team.created', user=self.request.user, data={
'_created_by_event_wizard': True,
'name': t.name,
'can_change_event_settings': True,
'can_change_items': True,
'can_view_orders': True,
'can_change_orders': True,
'can_view_vouchers': True,
'can_change_vouchers': True,
'all_organizer_permissions': False,
'all_event_permissions': all_event_permissions,
'limit_event_permissions': limit_event_permissions,
'limit_events': [event.pk],
})
@@ -314,11 +339,8 @@ class EventWizard(SafeSessionWizardView):
})
event.log_action('pretix.event.settings', user=self.request.user, data=logdata)
if copy_data and copy_data['copy_from_event']:
from_event = copy_data['copy_from_event']
event.copy_data_from(from_event)
elif self.clone_from:
event.copy_data_from(self.clone_from)
if copy_from_event:
event.copy_data_from(copy_from_event)
else:
event.set_active_plugins(settings.PRETIX_PLUGINS_DEFAULT.split(","),
allow_restricted=settings.PRETIX_PLUGINS_DEFAULT.split(","))
@@ -331,10 +353,8 @@ class EventWizard(SafeSessionWizardView):
event.set_defaults()
if basics_data['tax_rate'] is not None:
if self.clone_from:
default_tax_rule = self.clone_from.cached_default_tax_rule
elif copy_data and copy_data['copy_from_event']:
default_tax_rule = from_event.cached_default_tax_rule
if copy_from_event:
default_tax_rule = copy_from_event.cached_default_tax_rule
else:
default_tax_rule = None
if not default_tax_rule or default_tax_rule.rate != basics_data['tax_rate']:
@@ -348,7 +368,7 @@ class EventWizard(SafeSessionWizardView):
event.settings.set('locale', basics_data['locale'])
event.settings.set('locales', foundation_data['locales'])
if (copy_data and copy_data['copy_from_event']) or self.clone_from or event.has_subevents:
if copy_from_event or event.has_subevents:
return redirect(reverse('control:event.settings', kwargs={
'organizer': event.organizer.slug,
'event': event.slug,