From d39a36e06a133a3b200f998ad98a8302d2eba3cb Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Fri, 9 Jan 2026 14:34:28 +0100 Subject: [PATCH] Docs, tests and fixes for teams api --- doc/api/resources/teams.rst | 87 ++++++++++-- src/pretix/api/serializers/organizer.py | 53 ++++--- src/pretix/helpers/permission_migration.py | 2 +- src/tests/api/conftest.py | 10 +- src/tests/api/test_teams.py | 155 ++++++++++++++++++++- 5 files changed, 265 insertions(+), 42 deletions(-) diff --git a/doc/api/resources/teams.rst b/doc/api/resources/teams.rst index baff8e493a..7af667b3f8 100644 --- a/doc/api/resources/teams.rst +++ b/doc/api/resources/teams.rst @@ -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.giftcards:read + organizer.giftcards:write + organizer.customers:read + organizer.customers:write + organizer.reusablemedia:read + organizer.reusablemedia:write + organizer.devices:read + organizer.devices:write + +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.plugins:write + event.settings.email.sender: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, ... } diff --git a/src/pretix/api/serializers/organizer.py b/src/pretix/api/serializers/organizer.py index 9caf30e33f..a0aa4f6377 100644 --- a/src/pretix/api/serializers/organizer.py +++ b/src/pretix/api/serializers/organizer.py @@ -316,7 +316,7 @@ class EventSlugField(serializers.SlugRelatedField): class PermissionMultipleChoiceField(serializers.MultipleChoiceField): def to_internal_value(self, data): return { - p: True for p in data + p: True for p in super().to_internal_value(data) } def to_representation(self, value): @@ -328,11 +328,29 @@ class TeamSerializer(serializers.ModelSerializer): limit_event_permissions = PermissionMultipleChoiceField(choices=[], required=False, allow_null=False, allow_empty=True) limit_organizer_permissions = PermissionMultipleChoiceField(choices=[], required=False, allow_null=False, allow_empty=True) + # Legacy fields, handled in to_representation and validate + can_change_event_settings = serializers.BooleanField(required=False, write_only=True) + can_change_items = serializers.BooleanField(required=False, write_only=True) + can_view_orders = serializers.BooleanField(required=False, write_only=True) + can_change_orders = serializers.BooleanField(required=False, write_only=True) + can_checkin_orders = serializers.BooleanField(required=False, write_only=True) + can_view_vouchers = serializers.BooleanField(required=False, write_only=True) + can_change_vouchers = serializers.BooleanField(required=False, write_only=True) + can_create_events = serializers.BooleanField(required=False, write_only=True) + can_change_organizer_settings = serializers.BooleanField(required=False, write_only=True) + can_change_teams = serializers.BooleanField(required=False, write_only=True) + can_manage_gift_cards = serializers.BooleanField(required=False, write_only=True) + can_manage_customers = serializers.BooleanField(required=False, write_only=True) + can_manage_reusable_media = serializers.BooleanField(required=False, write_only=True) + class Meta: model = Team fields = ( 'id', 'name', 'require_2fa', 'all_events', 'limit_events', 'all_event_permissions', 'limit_event_permissions', - 'all_organizer_permissions', 'limit_organizer_permissions', + 'all_organizer_permissions', 'limit_organizer_permissions', 'can_change_event_settings', + 'can_change_items', 'can_view_orders', 'can_change_orders', 'can_checkin_orders', 'can_view_vouchers', + 'can_change_vouchers', 'can_create_events', 'can_change_organizer_settings', 'can_change_teams', + 'can_manage_gift_cards', 'can_manage_customers', 'can_manage_reusable_media' ) def __init__(self, *args, **kwargs): @@ -356,35 +374,36 @@ class TeamSerializer(serializers.ModelSerializer): if old_data_set and new_data_set: raise ValidationError("You cannot set deprecated and current permission attributes at the same time.") + full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {} + full_data.update(data) + if new_data_set: - if data.get('limit_event_permissions') and data.get('all_event_permissions'): + if full_data.get('limit_event_permissions') and full_data.get('all_event_permissions'): raise ValidationError('Do not set both limit_event_permissions and all_event_permissions.') - if data.get('limit_organizer_permissions') and data.get('all_organizer_permissions'): + if full_data.get('limit_organizer_permissions') and full_data.get('all_organizer_permissions'): raise ValidationError('Do not set both limit_organizer_permissions and all_organizer_permissions.') if old_data_set: # Migrate with same logic as in migration 0297_plugable_permissions - if all(data.get("k") is True for k in OLD_TO_NEW_EVENT_MIGRATION.keys() if k != "can_checkin_orders"): + if all(full_data.get(k) is True for k in OLD_TO_NEW_EVENT_MIGRATION.keys() if k != "can_checkin_orders"): data["all_event_permissions"] = True - data["limit_event_permissions"] = [] + data["limit_event_permissions"] = {} else: data["all_event_permissions"] = False - data["limit_event_permissions"] = [] + data["limit_event_permissions"] = {} for k, v in OLD_TO_NEW_EVENT_MIGRATION.items(): - if data.get(k) is True: - data["limit_event_permissions"].extend(v) - if all(data.get("k") is True for k in OLD_TO_NEW_ORGANIZER_MIGRATION.keys() if k != "can_checkin_orders"): + if full_data.get(k) is True: + data["limit_event_permissions"].update({kk: True for kk in v}) + if all(full_data.get(k) is True for k in OLD_TO_NEW_ORGANIZER_MIGRATION.keys() if k != "can_checkin_orders"): data["all_organizer_permissions"] = True - data["limit_organizer_permissions"] = [] + data["limit_organizer_permissions"] = {} else: data["all_organizer_permissions"] = False - data["limit_organizer_permissions"] = [] - for k, v in OLD_TO_NEW_EVENT_MIGRATION.items(): - if data.get(k) is True: - data["limit_organizer_permissions"].extend(v) + data["limit_organizer_permissions"] = {} + for k, v in OLD_TO_NEW_ORGANIZER_MIGRATION.items(): + if full_data.get(k) is True: + data["limit_organizer_permissions"].update({kk: True for kk in v}) - full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {} - full_data.update(data) if full_data.get('limit_events') and full_data.get('all_events'): raise ValidationError('Do not set both limit_events and all_events.') return data diff --git a/src/pretix/helpers/permission_migration.py b/src/pretix/helpers/permission_migration.py index 9eb5db001c..f05ef17e87 100644 --- a/src/pretix/helpers/permission_migration.py +++ b/src/pretix/helpers/permission_migration.py @@ -30,7 +30,7 @@ OLD_TO_NEW_EVENT_MIGRATION = { "event.settings.payment:write", "event.settings.plugins:write", "event.settings.email.sender:write", - "event.settings.tax:write" + "event.settings.tax:write", "event.settings.invoicing:write", "event.subevents:write", ], diff --git a/src/tests/api/conftest.py b/src/tests/api/conftest.py index b96c3e278d..031b6a7c52 100644 --- a/src/tests/api/conftest.py +++ b/src/tests/api/conftest.py @@ -131,8 +131,9 @@ def user(): @pytest.fixture @scopes_disabled() def user_client(client, team, user): - team.limit_event_permissions["event.orders:read"] = True - team.limit_event_permissions["event.vouchers:read"] = True + if not team.all_event_permissions: + team.limit_event_permissions["event.orders:read"] = True + team.limit_event_permissions["event.vouchers:read"] = True team.all_events = True team.save() team.members.add(user) @@ -143,8 +144,9 @@ def user_client(client, team, user): @pytest.fixture @scopes_disabled() def token_client(client, team): - team.limit_event_permissions["event.orders:read"] = True - team.limit_event_permissions["event.vouchers:read"] = True + if not team.all_event_permissions: + team.limit_event_permissions["event.orders:read"] = True + team.limit_event_permissions["event.vouchers:read"] = True team.all_events = True team.save() t = team.tokens.create(name='Foo') diff --git a/src/tests/api/test_teams.py b/src/tests/api/test_teams.py index e687d96501..7792e6b1dd 100644 --- a/src/tests/api/test_teams.py +++ b/src/tests/api/test_teams.py @@ -31,6 +31,7 @@ def second_team(organizer, event): t = organizer.teams.create( name='User team', all_events=False, + limit_event_permissions={"event.orders:read": True}, ) t.limit_events.add(event) return t @@ -41,8 +42,10 @@ TEST_TEAM_RES = { 'can_change_teams': True, 'can_change_organizer_settings': True, 'can_manage_gift_cards': True, 'can_manage_customers': True, 'can_manage_reusable_media': True, '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, 'can_checkin_orders': False, + 'can_view_vouchers': True, 'can_change_vouchers': True, 'can_checkin_orders': True, 'require_2fa': False, + 'all_event_permissions': True, 'limit_event_permissions': [], + 'all_organizer_permissions': True, 'limit_organizer_permissions': [], } SECOND_TEAM_RES = { @@ -50,9 +53,11 @@ SECOND_TEAM_RES = { 'can_create_events': False, 'can_manage_customers': False, 'can_manage_reusable_media': False, 'can_change_teams': False, 'can_change_organizer_settings': False, 'can_manage_gift_cards': False, - 'can_change_event_settings': False, 'can_change_items': False, 'can_view_orders': False, 'can_change_orders': False, + 'can_change_event_settings': False, 'can_change_items': False, 'can_view_orders': True, 'can_change_orders': False, 'can_view_vouchers': False, 'can_change_vouchers': False, 'can_checkin_orders': False, 'require_2fa': False, + 'all_event_permissions': False, 'limit_event_permissions': ["event.orders:read"], + 'all_organizer_permissions': False, 'limit_organizer_permissions': [], } @@ -95,8 +100,68 @@ def test_team_create(token_client, organizer, event): @pytest.mark.django_db -def test_team_update(token_client, organizer, event, second_team): - assert not second_team.can_change_event_settings +def test_team_update(token_client, organizer, event, team, second_team): + resp = token_client.patch( + '/api/v1/organizers/{}/teams/{}/'.format(organizer.slug, second_team.pk), + { + 'all_event_permission': False, + 'limit_event_permissions': ["event.settings.general:write"], + }, + format='json' + ) + assert resp.status_code == 200 + second_team.refresh_from_db() + assert second_team.limit_event_permissions == { + 'event.settings.general:write': True, + } + assert not second_team.all_event_permissions + + resp = token_client.patch( + '/api/v1/organizers/{}/teams/{}/'.format(organizer.slug, second_team.pk), + { + 'all_event_permission': False, + 'limit_event_permissions': ["INVALID"], + }, + format='json' + ) + assert resp.status_code == 400 + assert "invalid" in str(resp.data) + + resp = token_client.patch( + '/api/v1/organizers/{}/teams/{}/'.format(organizer.slug, team.pk), + { + 'limit_event_permissions': ["event.settings.general:write"], + }, + format='json' + ) + assert resp.status_code == 400 + assert "Do not set both" in str(resp.data) + + resp = token_client.patch( + '/api/v1/organizers/{}/teams/{}/'.format(organizer.slug, second_team.pk), + { + 'all_organizer_permissions': True, + 'limit_organizer_permissions': ["organizer.events:create"], + }, + format='json' + ) + assert resp.status_code == 400 + assert "Do not set both" in str(resp.data) + + resp = token_client.patch( + '/api/v1/organizers/{}/teams/{}/'.format(organizer.slug, second_team.pk), + { + 'all_events': True, + }, + format='json' + ) + assert resp.status_code == 400 + assert "Do not set both" in str(resp.data) + + +@pytest.mark.django_db +@pytest.mark.filterwarnings("ignore") +def test_team_update_legacy_add_perm(token_client, organizer, event, second_team): resp = token_client.patch( '/api/v1/organizers/{}/teams/{}/'.format(organizer.slug, second_team.pk), { @@ -107,15 +172,95 @@ def test_team_update(token_client, organizer, event, second_team): assert resp.status_code == 200 second_team.refresh_from_db() assert second_team.can_change_event_settings + assert second_team.limit_event_permissions == { + "event.settings.general:write": True, + "event.settings.payment:write": True, + "event.settings.plugins:write": True, + "event.settings.email.sender:write": True, + "event.settings.tax:write": True, + "event.settings.invoicing:write": True, + "event.subevents:write": True, + "event.orders:read": True, + } + + +@pytest.mark.django_db +@pytest.mark.filterwarnings("ignore") +def test_team_update_legacy_add_all_perms(token_client, organizer, event, second_team): + resp = token_client.patch( + '/api/v1/organizers/{}/teams/{}/'.format(organizer.slug, second_team.pk), + { + 'can_change_event_settings': True, + 'can_change_items': True, + # can_view_orders omitted because already set + 'can_change_orders': True, + 'can_checkin_orders': True, + 'can_view_vouchers': True, + 'can_change_vouchers': True, + }, + format='json' + ) + assert resp.status_code == 200 + second_team.refresh_from_db() + assert second_team.all_event_permissions + assert second_team.limit_event_permissions == {} resp = token_client.patch( '/api/v1/organizers/{}/teams/{}/'.format(organizer.slug, second_team.pk), { - 'all_events': True, + 'can_create_events': True, + 'can_change_organizer_settings': True, + 'can_change_teams': True, + 'can_manage_gift_cards': True, + 'can_manage_customers': True, + 'can_manage_reusable_media': True, + }, + format='json' + ) + assert resp.status_code == 200 + second_team.refresh_from_db() + assert second_team.all_organizer_permissions + assert second_team.limit_organizer_permissions == {} + + resp = token_client.patch( + '/api/v1/organizers/{}/teams/{}/'.format(organizer.slug, second_team.pk), + { + 'can_change_teams': False, + }, + format='json' + ) + assert resp.status_code == 200 + second_team.refresh_from_db() + assert not second_team.all_organizer_permissions + assert second_team.limit_organizer_permissions == { + 'organizer.settings.general:write': True, + 'organizer.giftcards:read': True, + 'organizer.giftcards:write': True, + 'organizer.events:create': True, + 'organizer.customers:read': True, + 'organizer.customers:write': True, + 'organizer.reusablemedia:read': True, + 'organizer.reusablemedia:write': True, + 'organizer.devices:read': True, + 'organizer.devices:write': True, + } + assert resp.data["can_manage_customers"] is True + assert resp.data["can_change_teams"] is False + + +@pytest.mark.django_db +@pytest.mark.filterwarnings("ignore") +def test_team_update_legacy_and_new(token_client, organizer, event, second_team): + resp = token_client.patch( + '/api/v1/organizers/{}/teams/{}/'.format(organizer.slug, second_team.pk), + { + 'can_change_event_settings': True, + 'all_organizer_permissions': True, }, format='json' ) assert resp.status_code == 400 + assert "You cannot set deprecated and current permission attributes" in str(resp.data) @pytest.mark.django_db