Compare commits

...

1 Commits

Author SHA1 Message Date
Raphael Michel
902acda340 Bank transfer: More restrictive approach to permissions (Z#23235727) 2026-06-09 11:12:12 +02:00
5 changed files with 114 additions and 46 deletions

View File

@@ -19,6 +19,7 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django.db.models import Exists, OuterRef, Q
from django.dispatch import receiver
from django.template.loader import get_template
from django.urls import resolve, reverse
@@ -76,10 +77,18 @@ def control_nav_import(sender, request=None, **kwargs):
@receiver(nav_organizer, dispatch_uid="payment_banktransfer_organav")
def control_nav_orga_import(sender, request=None, **kwargs):
url = resolve(request.path_info)
has_any_event_perm = request.user.get_events_with_permission(
"event.orders:write", request=request
).filter(organizer=request.organizer).exists()
if not has_any_event_perm:
events_without_permission = request.organizer.events.filter(
~Exists(
request.user.teams.with_event_permission(
"event.orders:read"
).filter(
Q(all_events=True) | Q(limit_events=OuterRef("pk")),
organizer_id=OuterRef("organizer_id"),
)
)
).exists()
if events_without_permission:
return []
return [
{

View File

@@ -10,7 +10,7 @@
Therefore, you won't be able to mark any order as paid here.
{% endblocktrans %}
</div>
{% else %}
{% elif can_write %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Upload a new file" %}</h3>
@@ -93,7 +93,7 @@
</div>
</form>
<div class="col-md-2">
{% if not filter_form.is_valid %}
{% if not filter_form.is_valid and can_write %}
<form action="" method="post" class="helper-display-inline pull-right flip">
{% csrf_token %}
<button class="btn btn-danger" type="submit" name="discard" value="all">

View File

@@ -25,7 +25,7 @@
</div>
{% endif %}
{% if num_new > 0 %}
{% if num_new > 0 and can_write %}
<form action="" method="post">
{% csrf_token %}
<button class="btn btn-primary">

View File

@@ -20,24 +20,24 @@
{% for trans in list %}
<tr data-id="{{ trans.id }}">
<td class="actions">
{% if trans.order and trans.state == 'invalid' %}
{% if trans.order and trans.state == 'invalid' and can_write %}
<button type="button" class="btn btn-default" name="action_{{ trans.id }}" value="accept"
data-toggle="tooltip" title="{% trans "Accept anyway" %}" data-placement="right">
<span class="fa fa-check"></span>
</button>
{% elif trans.state == 'nomatch' %}
{% elif trans.state == 'nomatch' and can_write %}
<input type="text" class="form-control" placeholder="{% trans "Order code" %}">
<button class="btn btn-default" type="button" name="action_{{ trans.id }}"
value="assign" data-toggle="tooltip" title="{% trans "Assign to order" %}"
data-placement="right">
<span class="fa fa-check"></span>
</button>
{% elif trans.state == 'error' %}
{% elif trans.state == 'error' and can_write %}
<button type="button" class="btn btn-default" name="action_{{ trans.id }}" value="retry"
data-toggle="tooltip" title="{% trans "Retry" %}" data-placement="right">
<span class="fa fa-refresh"></span>
</button>
{% elif trans.state == 'already' %}
{% elif trans.state == 'already' and can_write %}
<input type="text" class="form-control" placeholder="{% trans "Order code" %}">
<div class="btn-group" role="group">
<button class="btn btn-default" type="button" name="action_{{ trans.id }}"
@@ -76,13 +76,15 @@
{% endif %}
</div>
{{ trans.reference }}
<div class="comment-box" data-plain="{{ trans.comment }}">
<strong>{% trans "Comment:" %}</strong>
<span class="comment">{{ trans.comment|rich_text }}</span>
<a href="#" class="comment-modify btn btn-default btn-xs">
<span class="fa fa-edit"></span>
</a>
</div>
{% if can_write %}
<div class="comment-box" data-plain="{{ trans.comment }}">
<strong>{% trans "Comment:" %}</strong>
<span class="comment">{{ trans.comment|rich_text }}</span>
<a href="#" class="comment-modify btn btn-default btn-xs">
<span class="fa fa-edit"></span>
</a>
</div>
{% endif %}
</td>
<td>
{% if trans.currency %}
@@ -119,10 +121,12 @@
{% endif %}
</td>
<td class="discard">
<button type="button" class="btn btn-default" name="action_{{ trans.id }}" value="discard"
data-toggle="tooltip" title="{% trans "Discard" %}">
<span class="fa fa-trash"></span>
</button>
{% if can_write %}
<button type="button" class="btn btn-default" name="action_{{ trans.id }}" value="discard"
data-toggle="tooltip" title="{% trans "Discard" %}">
<span class="fa fa-trash"></span>
</button>
{% endif %}
</td>
</tr>
{% endfor %}

View File

@@ -46,7 +46,7 @@ from django import forms
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import Count, Q, QuerySet
from django.db.models import Count, Exists, OuterRef, Q, QuerySet
from django.http import FileResponse, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
@@ -586,6 +586,7 @@ class ImportView(ListView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['job_running'] = self.job_running
ctx['can_write'] = self.can_write
ctx['no_more_payments'] = False
ctx['filter_form'] = BankTransactionFilterForm(self.request.GET or None)
@@ -623,45 +624,94 @@ class ImportView(ListView):
return ctx
class OrganizerBanktransferView:
class EventPermissionOnAllEventsRequiredMixin:
@cached_property
def can_write(self):
perm_name = self.event_permission
if hasattr(self, 'write_event_permission'):
perm_name = self.write_event_permission
events_without_permission = self.request.organizer.events.filter(
~Exists(
self.request.user.teams.with_event_permission(
perm_name
).filter(
Q(all_events=True) | Q(limit_events=OuterRef("pk")),
organizer_id=OuterRef("organizer_id"),
)
)
).exists()
return not events_without_permission
def dispatch(self, request, *args, **kwargs):
has_any_event_perm = request.user.get_events_with_permission(
"event.orders:write", request=request
).filter(organizer=request.organizer).exists()
if not has_any_event_perm:
perm_name = self.event_permission
if request.method not in ("GET", "HEAD") and hasattr(self, 'write_event_permission'):
perm_name = self.write_event_permission
events_without_permission = self.request.organizer.events.filter(
~Exists(
self.request.user.teams.with_event_permission(
perm_name
).filter(
Q(all_events=True) | Q(limit_events=OuterRef("pk")),
organizer_id=OuterRef("organizer_id"),
)
)
).exists()
if events_without_permission:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
class EventImportView(EventPermissionRequiredMixin, ImportView):
class PostEventPermissionRequiredMixin(EventPermissionRequiredMixin):
@cached_property
def can_write(self):
return self.request.user.has_event_permission(
self.request.organizer, self.request.event, self.write_permission, request=self.request
)
def dispatch(self, request, *args, **kwargs):
if request.method not in ("GET", "HEAD"):
if not self.can_write:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
class EventImportView(PostEventPermissionRequiredMixin, ImportView):
permission = 'event.orders:write'
write_permission = 'event.orders:write'
class OrganizerImportView(OrganizerBanktransferView, OrganizerDetailViewMixin,
class OrganizerImportView(EventPermissionOnAllEventsRequiredMixin, OrganizerDetailViewMixin,
ImportView):
pass
event_permission = 'event.orders:read'
write_event_permission = 'event.orders:write'
class EventJobDetailView(EventPermissionRequiredMixin, JobDetailView):
permission = 'event.orders:write'
permission = 'event.orders:read'
class OrganizerJobDetailView(OrganizerBanktransferView, OrganizerDetailViewMixin,
class OrganizerJobDetailView(EventPermissionOnAllEventsRequiredMixin, OrganizerDetailViewMixin,
JobDetailView):
pass
event_permission = 'event.orders:read'
class EventActionView(EventPermissionRequiredMixin, ActionView):
permission = 'event.orders:write'
class EventActionView(PostEventPermissionRequiredMixin, ActionView):
permission = 'event.orders:read'
write_permission = 'event.orders:write'
class OrganizerActionView(OrganizerBanktransferView, OrganizerDetailViewMixin,
class OrganizerActionView(EventPermissionOnAllEventsRequiredMixin, OrganizerDetailViewMixin,
ActionView):
event_permission = "event.orders:read"
write_event_permission = "event.orders:write"
def order_qs(self):
# The filters here are basically pointless with EventPermissionOnAllEventsRequiredMixin
# but let's keep them for safety with future refactorings
all = self.request.user.teams.filter(
TeamQuerySet.event_permission_q("event.orders:read"),
TeamQuerySet.event_permission_q("event.orders:write"),
all_events=True,
organizer=self.request.organizer,
).exists()
@@ -671,7 +721,6 @@ class OrganizerActionView(OrganizerBanktransferView, OrganizerDetailViewMixin,
return Order.objects.filter(
event_id__in=self.request.user.teams.filter(
TeamQuerySet.event_permission_q("event.orders:read"),
TeamQuerySet.event_permission_q("event.orders:write"),
organizer=self.request.organizer,
).values_list('limit_events__id', flat=True)
)
@@ -712,6 +761,7 @@ class RefundExportListView(ListView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['num_new'] = self.get_unexported().count()
ctx['can_write'] = self.can_write
ctx['basetpl'] = "pretixcontrol/event/base.html"
if not hasattr(self.request, 'event'):
ctx['basetpl'] = "pretixcontrol/organizers/base.html"
@@ -764,8 +814,9 @@ class RefundExportListView(ListView):
return redirect(self.get_success_url())
class EventRefundExportListView(EventPermissionRequiredMixin, RefundExportListView):
permission = 'event.orders:write'
class EventRefundExportListView(PostEventPermissionRequiredMixin, RefundExportListView):
permission = 'event.orders:read'
write_permission = 'event.orders:write'
def get_success_url(self):
return reverse('plugins:banktransfer:refunds.list', kwargs={
@@ -787,7 +838,9 @@ class EventRefundExportListView(EventPermissionRequiredMixin, RefundExportListVi
)
class OrganizerRefundExportListView(OrganizerBanktransferView, RefundExportListView):
class OrganizerRefundExportListView(EventPermissionOnAllEventsRequiredMixin, RefundExportListView):
event_permission = 'event.orders:read'
write_event_permission = 'event.orders:write'
def get_success_url(self):
return reverse('plugins:banktransfer:refunds.list', kwargs={
@@ -820,7 +873,7 @@ class DownloadRefundExportView(DetailView):
class EventDownloadRefundExportView(EventPermissionRequiredMixin, DownloadRefundExportView):
permission = 'event.orders:write'
permission = 'event.orders:read'
def get_object(self, *args, **kwargs):
return get_object_or_404(
@@ -830,7 +883,8 @@ class EventDownloadRefundExportView(EventPermissionRequiredMixin, DownloadRefund
)
class OrganizerDownloadRefundExportView(OrganizerBanktransferView, OrganizerDetailViewMixin, DownloadRefundExportView):
class OrganizerDownloadRefundExportView(EventPermissionOnAllEventsRequiredMixin, OrganizerDetailViewMixin, DownloadRefundExportView):
event_permission = 'event.orders:read'
def get_object(self, *args, **kwargs):
return get_object_or_404(
@@ -877,7 +931,7 @@ class SepaXMLExportView(SingleObjectMixin, FormView):
class EventSepaXMLExportView(EventPermissionRequiredMixin, SepaXMLExportView):
permission = 'event.orders:write'
permission = 'event.orders:read'
def get_object(self, *args, **kwargs):
return get_object_or_404(
@@ -892,7 +946,8 @@ class EventSepaXMLExportView(EventPermissionRequiredMixin, SepaXMLExportView):
return form
class OrganizerSepaXMLExportView(OrganizerBanktransferView, OrganizerDetailViewMixin, SepaXMLExportView):
class OrganizerSepaXMLExportView(EventPermissionOnAllEventsRequiredMixin, OrganizerDetailViewMixin, SepaXMLExportView):
permission = 'event.orders:read'
def get_object(self, *args, **kwargs):
return get_object_or_404(