mirror of
https://github.com/pretix/pretix.git
synced 2026-05-04 15:04:03 +00:00
* Allow team admins to require two-factor authentication * Add API tests * Improve logic * ADd button tooltip
231 lines
11 KiB
Python
231 lines
11 KiB
Python
#
|
|
# This file is part of pretix (Community Edition).
|
|
#
|
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
# Copyright (C) 2020-2021 rami.io 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 <https://pretix.eu/about/en/license>.
|
|
#
|
|
# 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
|
|
# <https://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
# 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 <http://www.apache.org/licenses/LICENSE-2.0>.
|
|
#
|
|
# 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 <https://github.com/pretix/pretix>.
|
|
#
|
|
# 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 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",
|
|
"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') + '?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 = 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 = 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
|