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
co-authored by luelista Richard Schreiber
parent eddde2b6c0
commit df0b580dd6
203 changed files with 5374 additions and 2331 deletions
+5 -4
View File
@@ -197,10 +197,11 @@ Permissions & security profiles
Device authentication is currently hardcoded to grant the following permissions:
* View event meta data and products etc.
* View orders
* Change orders
* Manage gift cards
* Read event meta data and products etc.
* Read and write orders
* Read and write gift cards
* Read and write reusable media
* Read vouchers
Devices cannot change events or products and cannot access vouchers.
+1 -1
View File
@@ -30,7 +30,7 @@ software_brand string Device software
software_version string Device software version (read-only)
created datetime Creation time
initialized datetime Time of initialization (or ``null``)
initialization_token string Token for initialization
initialization_token string Token for initialization (field invisible without write permission)
revoked boolean Whether this device no longer has access
security_profile string The name of a supported security profile restricting API access
===================================== ========================== =======================================================
+2 -14
View File
@@ -65,8 +65,6 @@ Endpoints
Returns a list of all events within a given organizer the authenticated user/token has access to.
Permission required: "Can change event settings"
**Example request**:
.. sourcecode:: http
@@ -161,8 +159,6 @@ Endpoints
Returns information on one event, identified by its slug.
Permission required: "Can change event settings"
**Example request**:
.. sourcecode:: http
@@ -234,8 +230,6 @@ Endpoints
Please note that events cannot be created as 'live' using this endpoint. Quotas and payment must be added to the
event before sales can go live.
Permission required: "Can create events"
**Example request**:
.. sourcecode:: http
@@ -338,8 +332,6 @@ Endpoints
Please note that you can only copy from events under the same organizer this way. Use the ``clone_from`` parameter
when creating a new event for this instead.
Permission required: "Can create events"
**Example request**:
.. sourcecode:: http
@@ -433,8 +425,6 @@ Endpoints
Updates an event
Permission required: "Can change event settings"
**Example request**:
.. sourcecode:: http
@@ -510,8 +500,6 @@ Endpoints
Delete an event. Note that events with orders cannot be deleted to ensure data integrity.
Permission required: "Can change event settings"
**Example request**:
.. sourcecode:: http
@@ -561,8 +549,6 @@ organizer level.
Get current values of event settings.
Permission required: "Can change event settings" (Exception: with device auth, *some* settings can always be *read*.)
**Example request**:
.. sourcecode:: http
@@ -615,6 +601,8 @@ organizer level.
Updates event settings. Note that ``PUT`` is not allowed here, only ``PATCH``.
Permission "Can change event settings" is always required. Some keys require additional permissions.
.. warning::
Settings can be stored at different levels in pretix. If a value is not set on event level, a default setting
-4
View File
@@ -110,8 +110,6 @@ Endpoints
Updates an organizer. Currently only the ``plugins`` field may be updated.
Permission required: "Can change organizer settings"
**Example request**:
.. sourcecode:: http
@@ -172,8 +170,6 @@ information about the properties.
Get current values of organizer settings.
Permission required: "Can change organizer settings"
**Example request**:
.. sourcecode:: http
+1 -1
View File
@@ -154,7 +154,7 @@ Endpoints
.. http:post:: /api/v1/organizers/(organizer)/reusablemedia/lookup/
Look up a new reusable medium by its identifier. In some cases, this might lead to the automatic creation of a new
medium behind the scenes.
medium behind the scenes, therefore this endpoint requires write permissions.
This endpoint, and this endpoint only, might return media from a different organizer if there is a cross-acceptance
agreement. In this case, only linked gift cards will be returned, no order position or customer records,
-6
View File
@@ -154,8 +154,6 @@ Endpoints
Creates a new subevent.
Permission required: "Can create events"
**Example request**:
.. sourcecode:: http
@@ -300,8 +298,6 @@ Endpoints
provide all fields of the resource, other fields will be reset to default. With ``PATCH``, you only need to provide
the fields that you want to change.
Permission required: "Can change event settings"
**Example request**:
.. sourcecode:: http
@@ -373,8 +369,6 @@ Endpoints
Delete a sub-event. Note that events with orders cannot be deleted to ensure data integrity.
Permission required: "Can change event settings"
**Example request**:
.. sourcecode:: http
+72 -15
View File
@@ -24,21 +24,58 @@ all_events boolean Whether this te
limit_events list List of event slugs this team has access to
require_2fa boolean Whether members of this team are required to use
two-factor authentication
can_create_events boolean
can_change_teams boolean
can_change_organizer_settings boolean
can_manage_customers boolean
can_manage_reusable_media boolean
can_manage_gift_cards boolean
can_change_event_settings boolean
can_change_items boolean
can_view_orders boolean
can_change_orders boolean
can_view_vouchers boolean
can_change_vouchers boolean
can_checkin_orders boolean
all_event_permissions bool Whether members of this team are granted all event-level
permissions, including future additions
limit_event_permissions list of strings The event-level permissions team members are granted
all_organizer_permissions bool Whether members of this team are granted all organizer-level
permissions, including future additions
all_organizer_permissions list of strings The organizer-level permissions team members are granted
can_create_events boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``.
can_change_teams boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``.
can_change_organizer_settings boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``.
can_manage_customers boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``.
can_manage_reusable_media boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``.
can_manage_gift_cards boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``.
can_change_event_settings boolean **DEPRECATED**. Legacy interface, use ``limit_event_permissions``.
can_change_items boolean **DEPRECATED**. Legacy interface, use ``limit_event_permissions``.
can_view_orders boolean **DEPRECATED**. Legacy interface, use ``limit_event_permissions``.
can_change_orders boolean **DEPRECATED**. Legacy interface, use ``limit_event_permissions``.
can_view_vouchers boolean **DEPRECATED**. Legacy interface, use ``limit_event_permissions``.
can_change_vouchers boolean **DEPRECATED**. Legacy interface, use ``limit_event_permissions``.
can_checkin_orders boolean **DEPRECATED**. Legacy interface, use ``limit_event_permissions``.
===================================== ========================== =======================================================
Possible values for ``limit_organizer_permissions`` defined in the core pretix system (plugins might add more)::
organizer.events:create
organizer.settings.general:write
organizer.teams:write
organizer.seatingplans:write
organizer.giftcards:read
organizer.giftcards:write
organizer.customers:read
organizer.customers:write
organizer.reusablemedia:read
organizer.reusablemedia:write
organizer.devices:read
organizer.devices:write
organizer.outgoingmails:read
Possible values for ``limit_event_permissions`` defined in the core pretix system (plugins might add more)::
event.settings.general:write
event.settings.payment:write
event.settings.tax:write
event.settings.invoicing:write
event.subevents:write
event.items:write
event.orders:read
event.orders:write
event.orders:checkin
event.vouchers:read
event.vouchers:write
event:cancel
Team member resource
--------------------
@@ -121,6 +158,10 @@ Team endpoints
"all_events": true,
"limit_events": [],
"require_2fa": true,
"all_event_permissions": true,
"limit_event_permissions": [],
"all_organizer_permissions": true,
"limit_organizer_permissions": [],
"can_create_events": true,
...
}
@@ -159,6 +200,10 @@ Team endpoints
"all_events": true,
"limit_events": [],
"require_2fa": true,
"all_event_permissions": true,
"limit_event_permissions": [],
"all_organizer_permissions": true,
"limit_organizer_permissions": [],
"can_create_events": true,
...
}
@@ -187,7 +232,10 @@ Team endpoints
"all_events": true,
"limit_events": [],
"require_2fa": true,
"can_create_events": true,
"all_event_permissions": true,
"limit_event_permissions": [],
"all_organizer_permissions": true,
"limit_organizer_permissions": [],
...
}
@@ -205,6 +253,10 @@ Team endpoints
"all_events": true,
"limit_events": [],
"require_2fa": true,
"all_event_permissions": true,
"limit_event_permissions": [],
"all_organizer_permissions": true,
"limit_organizer_permissions": [],
"can_create_events": true,
...
}
@@ -232,7 +284,8 @@ Team endpoints
Content-Length: 94
{
"can_create_events": true
"all_organizer_permissions": false,
"limit_organizer_permissions": ["organizer.events:create"]
}
**Example response**:
@@ -249,6 +302,10 @@ Team endpoints
"all_events": true,
"limit_events": [],
"require_2fa": true,
"all_event_permissions": true,
"limit_event_permissions": [],
"all_organizer_permissions": false,
"limit_organizer_permissions": ["organizer.events:create"],
"can_create_events": true,
...
}
+7 -7
View File
@@ -55,12 +55,12 @@ your views:
)
class AdminView(EventPermissionRequiredMixin, View):
permission = 'can_view_orders'
permission = 'event.orders:read'
...
@event_permission_required('can_view_orders')
@event_permission_required('event.orders:read')
def admin_view(request, organizer, event):
...
@@ -78,7 +78,7 @@ event-related views, there is also a signal that allows you to add the view to t
@receiver(nav_event, dispatch_uid='friends_tickets_nav')
def navbar_info(sender, request, **kwargs):
url = resolve(request.path_info)
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_vouchers'):
if not request.user.has_event_permission(request.organizer, request.event, 'event.vouchers:read'):
return []
return [{
'label': _('My plugin view'),
@@ -118,7 +118,7 @@ for good integration. If you just want to display a form, you could do it like t
class MySettingsView(EventSettingsViewMixin, EventSettingsFormView):
model = Event
permission = 'can_change_settings'
permission = 'event.settings.general:write'
form_class = MySettingsForm
template_name = 'my_plugin/settings.html'
@@ -204,13 +204,13 @@ In case of ``orga_router`` and ``event_router``, permission checking is done for
in the control panel. However, you need to make sure on your own only to return the correct subset of data! ``request
.event`` and ``request.organizer`` are available as usual.
To require a special permission like ``can_view_orders``, you do not need to inherit from a special ViewSet base
To require a special permission like ``event.orders:read``, you do not need to inherit from a special ViewSet base
class, you can just set the ``permission`` attribute on your viewset:
.. code-block:: python
class MyViewSet(ModelViewSet):
permission = 'can_view_orders'
permission = 'event.orders:read'
...
If you want to check the permission only for some methods of your viewset, you have to do it yourself. Note here that
@@ -220,7 +220,7 @@ following:
.. code-block:: python
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken) else request.user)
if perm_holder.has_event_permission(request.event.organizer, request.event, 'can_view_orders'):
if perm_holder.has_event_permission(request.event.organizer, request.event, 'event.orders:read'):
...
+16
View File
@@ -80,8 +80,24 @@ The exporter class
.. autoattribute:: category
.. autoattribute:: feature
.. autoattribute:: export_form_fields
.. autoattribute:: repeatable_read
.. automethod:: render
This is an abstract method, you **must** override this!
.. automethod:: available_for_user
.. automethod:: get_required_event_permission
On organizer level, by default exporters are expected to handle on a *set of events* and the system will automatically
add a form field that allows the selection of events, limited to events the user has correct permissions for. If this
does not fit your organizer, because it is not related to events, you should **also** inherit from the following class:
.. class:: pretix.base.exporter.OrganizerLevelExportMixin
.. automethod:: get_required_organizer_permission
+2 -1
View File
@@ -14,7 +14,8 @@ Core
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
item_copy_data, register_sales_channel_types, register_global_settings, quota_availability, global_email_filter,
register_ticket_secret_generators, gift_card_transaction_display,
register_text_placeholders, register_mail_placeholders, device_info_updated
register_text_placeholders, register_mail_placeholders, device_info_updated,
register_event_permission_groups, register_organizer_permission_groups
Order events
""""""""""""
+1 -1
View File
@@ -196,7 +196,7 @@ A simple implementation could look like this:
.. code-block:: python
class MyNotificationType(NotificationType):
required_permission = "can_view_orders"
required_permission = "event.orders:read"
action_type = "pretix.event.order.paid"
verbose_name = _("Order has been paid")
+71 -18
View File
@@ -2,7 +2,7 @@ Permissions
===========
pretix uses a fine-grained permission system to control who is allowed to control what parts of the system.
The central concept here is the concept of *Teams*. You can read more on `configuring teams and permissions <user-teams>`_
The central concept here is the concept of *Teams*. You can read more on `configuring teams and permissions`_
and the :class:`pretix.base.models.Team` model in the respective parts of the documentation. The basic digest is:
An organizer account can have any number of teams, and any number of users can be part of a team. A team can be
assigned a set of permissions and connected to some or all of the events of the organizer.
@@ -25,8 +25,8 @@ permission level to access a view:
class MyOrgaView(OrganizerPermissionRequiredMixin, View):
permission = 'can_change_organizer_settings'
# Only users with the permission ``can_change_organizer_settings`` on
permission = 'organizer.settings.general:write'
# Only users with the permission ``organizer.settings.general:write`` on
# this organizer can access this
@@ -35,9 +35,9 @@ permission level to access a view:
# Only users with *any* permission on this organizer can access this
@organizer_permission_required('can_change_organizer_settings')
@organizer_permission_required('organizer.settings.general:write')
def my_orga_view(request, organizer, **kwargs):
# Only users with the permission ``can_change_organizer_settings`` on
# Only users with the permission ``organizer.settings.general:write`` on
# this organizer can access this
@@ -56,8 +56,8 @@ Of course, the same is available on event level:
class MyEventView(EventPermissionRequiredMixin, View):
permission = 'can_change_event_settings'
# Only users with the permission ``can_change_event_settings`` on
permission = 'event.settings.general:write'
# Only users with the permission ``event.settings.general:write`` on
# this event can access this
@@ -65,13 +65,16 @@ Of course, the same is available on event level:
permission = None
# Only users with *any* permission on this event can access this
class MyThirdEventView(EventPermissionRequiredMixin, View):
permission = AnyPermissionOf('event.settings.payment:write', 'event.settings.general:write')
# Only users with at least one of the specified permissions on this event
# can access this
@event_permission_required('can_change_event_settings')
@event_permission_required('event.settings.general:write')
def my_event_view(request, organizer, **kwargs):
# Only users with the permission ``can_change_event_settings`` on
# Only users with the permission ``event.settings.general:write`` on
# this event can access this
@event_permission_required()
def my_other_event_view(request, organizer, **kwargs):
# Only users with *any* permission on this event can access this
@@ -121,7 +124,7 @@ When creating your own ``viewset`` using Django REST framework, you just need to
and pretix will check it automatically for you::
class MyModelViewSet(viewsets.ReadOnlyModelViewSet):
permission = 'can_view_orders'
permission = 'event.orders:read'
Checking permission in code
---------------------------
@@ -136,12 +139,12 @@ Return all users that are in any team that is connected to this event::
Return all users that are in a team with a specific permission for this event::
>>> event.get_users_with_permission('can_change_event_settings')
>>> event.get_users_with_permission('event.orders:read')
<QuerySet: …>
Determine if a user has a certain permission for a specific event::
>>> user.has_event_permission(organizer, event, 'can_change_event_settings', request=request)
>>> user.has_event_permission(organizer, event, 'event.orders:read', request=request)
True
Determine if a user has any permission for a specific event::
@@ -153,27 +156,27 @@ In the two previous commands, the ``request`` argument is optional, but required
The same method exists for organizer-level permissions::
>>> user.has_organizer_permission(organizer, 'can_change_event_settings', request=request)
>>> user.has_organizer_permission(organizer, 'event.orders:read', request=request)
True
Sometimes, it might be more useful to get the set of permissions at once::
>>> user.get_event_permission_set(organizer, event)
{'can_change_event_settings', 'can_view_orders', 'can_change_orders'}
{'event.settings.general:write', 'event.orders:read', 'event.orders:write'}
>>> user.get_organizer_permission_set(organizer, event)
{'can_change_organizer_settings', 'can_create_events'}
{'organizer.settings.general:write', 'organizer.events:create'}
Within a view on the ``/control`` subpath, the results of these two methods are already available in the
``request.eventpermset`` and ``request.orgapermset`` properties. This makes it convenient to query them in templates::
{% if "can_change_orders" in request.eventpermset %}
{% if "event.orders:write" in request.eventpermset %}
{% endif %}
You can also do the reverse to get any events a user has access to::
>>> user.get_events_with_permission('can_change_event_settings', request=request)
>>> user.get_events_with_permission('event.settings.general:write', request=request)
<QuerySet: …>
>>> user.get_events_with_any_permission(request=request)
@@ -195,3 +198,53 @@ staff mode is active. You can check if a user is in staff mode using their sessi
Staff mode has a hard time limit and during staff mode, a middleware will log all requests made by that user. Later,
the user is able to also save a message to comment on what they did in their administrative session. This feature is
intended to help compliance with data protection rules as imposed e.g. by GDPR.
Adding permissions
------------------
Plugins can add permissions through the ``register_event_permission_groups`` and ``register_organizer_permission_groups``.
We recommend to use this only for very significant permissions, as the system will become less usable with too many
permission levels, also because the team page will show all permission options, even those of disabled plugins.
To register your permissions, you need to register a **permission group** (often representing an area of functionality
or a key model). Below that group, there are **actions**, which represent the actual permissions. Permissions will be
generated as ``<group_name>:<action>``. Then, you need to define **options** which are the valid combinations of the
actions that should be possible to select for a team. This two-step mechanism exists to provide a better user experience
and avoid useless combinations like "write but not read".
Example::
@receiver(register_event_permission_groups)
def register_plugin_event_permissions(sender, **kwargs):
return [
PermissionGroup(
name="pretix_myplugin.resource",
label=_("Resources"),
actions=["read", "write"],
options=[
PermissionOption(actions=tuple(), label=_("No access")),
PermissionOption(actions=("read",), label=_("View")),
PermissionOption(actions=("read", "write"), label=_("View and change")),
],
help_text=_("Some help text")
),
]
@receiver(register_organizer_permission_groups)
def register_plugin_organizer_permissions(sender, **kwargs):
return [
PermissionGroup(
name="pretix_myplugin.resource",
label=_("Resources"),
actions=["read", "write"],
options=[
PermissionOption(actions=tuple(), label=_("No access")),
PermissionOption(actions=("read",), label=_("View")),
PermissionOption(actions=("read", "write"), label=_("View and change")),
],
help_text=_("Some help text")
),
]
.. _configuring teams and permissions: https://docs.pretix.eu/guides/teams/