# # This file is part of pretix (Community Edition). # # Copyright (C) 2014-2020 Raphael Michel and contributors # Copyright (C) 2020-today pretix GmbH and contributors # # This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General # Public License as published by the Free Software Foundation in version 3 of the License. # # ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are # applicable granting you additional permissions and placing additional restrictions on your usage of this software. # Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive # this file, see . # # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more # details. # # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # # This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of # the Apache License 2.0 can be obtained at . # # This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A # full history of changes and contributors is available at . # # This file contains Apache-licensed contributions copyrighted by: Tobias Kunze # # Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under the License. from urllib.parse import quote, urljoin, urlparse from django.conf import settings from django.contrib.auth import REDIRECT_FIELD_NAME, logout from django.http import Http404 from django.shortcuts import get_object_or_404, resolve_url from django.template.response import TemplateResponse from django.urls import get_script_prefix, resolve, reverse from django.utils.encoding import force_str from django.utils.translation import gettext as _ from django_scopes import scope from pretix.base.models import Event, Organizer from pretix.base.models.auth import ( EventPermissionSet, OrganizerPermissionSet, SuperuserPermissionSet, User, ) from pretix.helpers.http import redirect_to_url from pretix.helpers.security import ( Session2FASetupRequired, SessionInvalid, SessionPasswordChangeRequired, SessionReauthRequired, assert_session_valid, ) class PermissionMiddleware: """ This middleware enforces all requests to the control app to require login. Additionally, it enforces all requests to "control:event." URLs to be for an event the user has basic access to. """ EXCEPTIONS = ( "auth.login", "auth.login.2fa", "auth.register", "auth.forgot", "auth.forgot.recover", "auth.invite", "user.settings.notifications.off", "auth.bad_origin_report", ) EXCEPTIONS_FORCED_PW_CHANGE = ( "user.settings.password.change", "auth.logout" ) EXCEPTIONS_2FA = ( "user.settings.2fa", "user.settings.2fa.add", "user.settings.2fa.enable", "user.settings.2fa.disable", "user.settings.2fa.regenemergency", "user.settings.2fa.confirm.totp", "user.settings.2fa.confirm.webauthn", "user.settings.2fa.delete", "user.settings.2fa.leaveteams", "auth.logout", "user.reauth" ) def __init__(self, get_response=None): self.get_response = get_response super().__init__() def _login_redirect(self, request): # Taken from django/contrib/auth/decorators.py path = request.build_absolute_uri() # urlparse chokes on lazy objects in Python 3, force to str resolved_login_url = force_str( resolve_url(settings.LOGIN_URL_CONTROL)) # If the login url is the same scheme and net location then just # use the path as the "next" url. login_scheme, login_netloc = urlparse(resolved_login_url)[:2] current_scheme, current_netloc = urlparse(path)[:2] if ((not login_scheme or login_scheme == current_scheme) and (not login_netloc or login_netloc == current_netloc)): path = request.get_full_path() from django.contrib.auth.views import redirect_to_login return redirect_to_login( path, resolved_login_url, REDIRECT_FIELD_NAME) def __call__(self, request): url = resolve(request.path_info) url_name = url.url_name if not request.path.startswith(get_script_prefix() + 'control') and not (url.namespace.startswith("api-") and url_name == "authorize"): # This middleware should only touch the /control subpath return self.get_response(request) if hasattr(request, 'organizer'): # If the user is on a organizer's subdomain, he should be redirected to pretix return redirect_to_url(urljoin(settings.SITE_URL, request.get_full_path())) if url_name in self.EXCEPTIONS: return self.get_response(request) if not request.user.is_authenticated: return self._login_redirect(request) try: # If this logic is updated, make sure to also update the logic in pretix/api/auth/permission.py assert_session_valid(request) except SessionInvalid: logout(request) return self._login_redirect(request) except SessionReauthRequired: if url_name not in ('user.reauth', 'auth.logout'): return redirect_to_url(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path())) except SessionPasswordChangeRequired: if url_name not in self.EXCEPTIONS_FORCED_PW_CHANGE: return redirect_to_url(reverse('control:user.settings.password.change') + '?next=' + quote(request.get_full_path())) except Session2FASetupRequired: if url_name not in self.EXCEPTIONS_2FA: return redirect_to_url(reverse('control:user.settings.2fa')) if 'event' in url.kwargs and 'organizer' in url.kwargs: if url.kwargs['organizer'] == '-' and url.kwargs['event'] == '-': # This is a hack that just takes the user to ANY event. It's useful to link to features in support # or documentation. ev = request.user.get_events_with_any_permission().order_by('-date_from').first() if not ev: raise Http404(_("The selected event was not found or you " "have no permission to administrate it.")) k = dict(url.kwargs) k['organizer'] = ev.organizer.slug k['event'] = ev.slug return redirect_to_url(reverse(url.view_name, kwargs=k, args=url.args)) with scope(organizer=None): request.event = Event.objects.filter( slug=url.kwargs['event'], organizer__slug=url.kwargs['organizer'], ).select_related('organizer').first() if not request.event or not request.user.has_event_permission(request.event.organizer, request.event, request=request): raise Http404(_("The selected event was not found or you " "have no permission to administrate it.")) request.organizer = request.event.organizer if request.user.has_active_staff_session(request.session.session_key): request.eventpermset = SuperuserPermissionSet() else: request.eventpermset = EventPermissionSet(request.user.get_event_permission_set(request.organizer, request.event)) elif 'organizer' in url.kwargs: if url.kwargs['organizer'] == '-': # This is a hack that just takes the user to ANY organizer. It's useful to link to features in support # or documentation. org = request.user.get_organizers_with_any_permission().first() if not org: raise Http404(_("The selected organizer was not found or you " "have no permission to administrate it.")) k = dict(url.kwargs) k['organizer'] = org.slug return redirect_to_url(reverse(url.view_name, kwargs=k, args=url.args)) request.organizer = Organizer.objects.filter( slug=url.kwargs['organizer'], ).first() if not request.organizer or not request.user.has_organizer_permission(request.organizer, request=request): raise Http404(_("The selected organizer was not found or you " "have no permission to administrate it.")) if request.user.has_active_staff_session(request.session.session_key): request.orgapermset = SuperuserPermissionSet() else: request.orgapermset = OrganizerPermissionSet(request.user.get_organizer_permission_set(request.organizer)) with scope(organizer=getattr(request, 'organizer', None)): r = self.get_response(request) if isinstance(r, TemplateResponse): r = r.render() return r class AuditLogMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): if request.path.startswith(get_script_prefix() + 'control') and request.user.is_authenticated: if getattr(request.user, "is_hijacked", False): hijack_history = request.session.get('hijack_history', False) hijacker = get_object_or_404(User, pk=hijack_history[0]) ss = hijacker.get_active_staff_session(request.session.get('hijacker_session')) if ss: ss.logs.create( url=request.path, method=request.method, impersonating=request.user ) else: ss = request.user.get_active_staff_session(request.session.session_key) if ss: ss.logs.create( url=request.path, method=request.method ) response = self.get_response(request) return response