forked from CGM_Public/pretix_original
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:
@@ -75,7 +75,10 @@ from pretix.base.models import (
|
||||
ReusableMedium, SalesChannel, Team,
|
||||
)
|
||||
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
|
||||
from pretix.base.models.organizer import OrganizerFooterLink
|
||||
from pretix.base.models.organizer import OrganizerFooterLink, TeamQuerySet
|
||||
from pretix.base.permissions import (
|
||||
get_all_event_permission_groups, get_all_organizer_permission_groups,
|
||||
)
|
||||
from pretix.base.settings import (
|
||||
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, validate_organizer_settings,
|
||||
)
|
||||
@@ -297,7 +300,34 @@ class MembershipTypeForm(I18nModelForm):
|
||||
fields = ['name', 'transferable', 'allow_parallel_usage', 'max_usages']
|
||||
|
||||
|
||||
class PermissionMultipleChoiceField(forms.MultipleChoiceField):
|
||||
def to_python(self, value):
|
||||
return {
|
||||
k: True for k in super().to_python(value) if k
|
||||
}
|
||||
|
||||
def prepare_value(self, value):
|
||||
if isinstance(value, dict):
|
||||
return [k for k, v in value.items() if v is True]
|
||||
return super().prepare_value(value)
|
||||
|
||||
|
||||
class TeamForm(forms.ModelForm):
|
||||
def _make_label(self, p):
|
||||
source = '{}'
|
||||
params = [p.label]
|
||||
|
||||
if p.plugin_name:
|
||||
source = '<span class="fa fa-puzzle-piece text-muted" data-toggle="tooltip" title="{}"></span> ' + source
|
||||
params.insert(0, _("Provided by a plugin"))
|
||||
|
||||
if p.help_text:
|
||||
source += ' <span class="fa fa-info-circle text-muted" data-toggle="tooltip" title="{}"></span>'
|
||||
params.append(p.help_text)
|
||||
|
||||
source += ' (<code>{}</code>)'
|
||||
params.append(p.name)
|
||||
return format_html(source, *params)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
organizer = kwargs.pop('organizer')
|
||||
@@ -305,16 +335,62 @@ class TeamForm(forms.ModelForm):
|
||||
self.fields['limit_events'].queryset = organizer.events.all().order_by(
|
||||
'-has_subevents', '-date_from'
|
||||
)
|
||||
self.event_field_names = []
|
||||
for pg in get_all_event_permission_groups().values():
|
||||
initial = ",".join(sorted(
|
||||
a for a in pg.actions if self.instance and self.instance.limit_event_permissions.get(f"{pg.name}:{a}")
|
||||
)) or "EMPTY"
|
||||
self.fields[f'event_{pg.name}'] = forms.ChoiceField(
|
||||
choices=[
|
||||
(
|
||||
",".join(sorted(opt.actions)) or "EMPTY",
|
||||
format_html(
|
||||
'{label} '
|
||||
'<span class="fa fa-question-circle fa-fw text-muted" data-toggle="tooltip"'
|
||||
' data-placement="right" title="{help_text}"></span>',
|
||||
label=opt.label,
|
||||
help_text=opt.help_text,
|
||||
) if opt.help_text else opt.label,
|
||||
)
|
||||
for opt in pg.options
|
||||
],
|
||||
label=pg.label,
|
||||
help_text=pg.help_text,
|
||||
initial=initial,
|
||||
widget=forms.RadioSelect,
|
||||
)
|
||||
self.event_field_names.append(f'event_{pg.name}')
|
||||
self.organizer_field_names = []
|
||||
for pg in get_all_organizer_permission_groups().values():
|
||||
initial = ",".join(sorted(
|
||||
a for a in pg.actions if self.instance and self.instance.limit_organizer_permissions.get(f"{pg.name}:{a}")
|
||||
)) or "EMPTY"
|
||||
self.fields[f'organizer_{pg.name}'] = forms.ChoiceField(
|
||||
choices=[
|
||||
(
|
||||
",".join(sorted(opt.actions)) or "EMPTY",
|
||||
format_html(
|
||||
'{label} '
|
||||
'<span class="fa fa-question-circle fa-fw text-muted" data-toggle="tooltip"'
|
||||
' data-placement="right" title="{help_text}"></span>',
|
||||
label=opt.label,
|
||||
help_text=opt.help_text,
|
||||
) if opt.help_text else opt.label,
|
||||
)
|
||||
for opt in pg.options
|
||||
],
|
||||
label=pg.label,
|
||||
help_text=pg.help_text,
|
||||
initial=initial,
|
||||
widget=forms.RadioSelect,
|
||||
)
|
||||
self.organizer_field_names.append(f'organizer_{pg.name}')
|
||||
|
||||
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',
|
||||
'all_organizer_permissions',]
|
||||
widgets = {
|
||||
'limit_events': forms.CheckboxSelectMultiple(attrs={
|
||||
'data-inverse-dependency': '#id_all_events',
|
||||
@@ -327,15 +403,57 @@ class TeamForm(forms.ModelForm):
|
||||
|
||||
def clean(self):
|
||||
data = super().clean()
|
||||
if self.instance.pk and not data['can_change_teams']:
|
||||
|
||||
data['limit_event_permissions'] = {}
|
||||
if not data['all_event_permissions']:
|
||||
for pg in get_all_event_permission_groups().values():
|
||||
selected = data.get(f'event_{pg.name}', 'EMPTY')
|
||||
if selected == "EMPTY":
|
||||
selected_actions = []
|
||||
else:
|
||||
selected_actions = selected.split(',')
|
||||
for action in pg.actions:
|
||||
if action in selected_actions:
|
||||
data['limit_event_permissions'][f"{pg.name}:{action}"] = True
|
||||
self.instance.limit_event_permissions = data['limit_event_permissions']
|
||||
|
||||
data['limit_organizer_permissions'] = {}
|
||||
if not data['all_organizer_permissions']:
|
||||
for pg in get_all_organizer_permission_groups().values():
|
||||
selected = data.get(f'organizer_{pg.name}', 'EMPTY')
|
||||
if selected == "EMPTY":
|
||||
selected_actions = []
|
||||
else:
|
||||
selected_actions = selected.split(',')
|
||||
for action in pg.actions:
|
||||
if action in selected_actions:
|
||||
data['limit_organizer_permissions'][f"{pg.name}:{action}"] = True
|
||||
self.instance.limit_organizer_permissions = data['limit_organizer_permissions']
|
||||
|
||||
if self.instance.pk and not data['all_organizer_permissions'] and 'organizer.teams:write' not in data.get('limit_organizer_permissions', []):
|
||||
if not self.instance.organizer.teams.exclude(pk=self.instance.pk).filter(
|
||||
can_change_teams=True, members__isnull=False
|
||||
TeamQuerySet.organizer_permission_q("organizer.teams:write"),
|
||||
members__isnull=False
|
||||
).exists():
|
||||
raise ValidationError(_('The changes could not be saved because there would be no remaining team with '
|
||||
'the permission to change teams and permissions.'))
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def changed_data_for_log(self):
|
||||
r = {}
|
||||
for k in self.changed_data:
|
||||
if k == "limit_events":
|
||||
r[k] = [e.id for e in getattr(self.instance, k).all()]
|
||||
elif k.startswith("event_"):
|
||||
r["limit_event_permissions"] = self.instance.limit_event_permissions
|
||||
elif k.startswith("organizer_"):
|
||||
r["limit_organizer_permissions"] = self.instance.limit_organizer_permissions
|
||||
else:
|
||||
r[k] = getattr(self.instance, k)
|
||||
return r
|
||||
|
||||
|
||||
class GateForm(forms.ModelForm):
|
||||
|
||||
|
||||
Reference in New Issue
Block a user