From dbdd6fd70b26bc32cdfbd4917f5a0bf4fafaa276 Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Tue, 3 Aug 2021 11:15:57 +0200 Subject: [PATCH 2/2] views: add debug login view (#55557) --- mellon/templates/mellon/debug_login.html | 10 +++++ mellon/urls.py | 1 + mellon/views.py | 55 +++++++++++++++++++++++- tests/test_sso_slo.py | 19 ++++++++ 4 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 mellon/templates/mellon/debug_login.html diff --git a/mellon/templates/mellon/debug_login.html b/mellon/templates/mellon/debug_login.html new file mode 100644 index 0000000..a5bf180 --- /dev/null +++ b/mellon/templates/mellon/debug_login.html @@ -0,0 +1,10 @@ +{% load i18n %} + +{% block content %} +

{% trans "Try again" %}

+

{% trans "Attributes:" %}

{{ attributes|pprint }}

+

{% trans "SAML assertion:" %}

{{ assertion_dump }}

+

{% trans "SAML response:" %}

{{ response_dump }}

+

{% trans "SAML artifact:" %}

{{ login.msgBody }}

+

{% trans "Logs:" %}

{{ logs }}

+{% endblock %} diff --git a/mellon/urls.py b/mellon/urls.py index 1660264..dca25b5 100644 --- a/mellon/urls.py +++ b/mellon/urls.py @@ -8,6 +8,7 @@ from . import views urlpatterns = [ url('login/$', views.login, name='mellon_login'), + url('login/debug/$', views.debug_login, name='mellon_debug_login'), url('logout/$', views.logout, name='mellon_logout'), url('metadata/$', views.metadata, name='mellon_metadata'), ] diff --git a/mellon/views.py b/mellon/views.py index 04b96b7..a22be3d 100644 --- a/mellon/views.py +++ b/mellon/views.py @@ -15,7 +15,9 @@ from __future__ import unicode_literals +from contextlib import contextmanager, nullcontext from importlib import import_module +from io import StringIO import logging import requests import lasso @@ -26,7 +28,8 @@ import xml.etree.ElementTree as ET import django.http from django.views.generic import View -from django.http import HttpResponseRedirect, HttpResponse +from django.views.generic.base import RedirectView +from django.http import HttpResponseRedirect, HttpResponse, HttpResponseForbidden from django.contrib import auth from django.contrib.auth import get_user_model from django.conf import settings @@ -142,6 +145,11 @@ class ProfileMixin(object): class LoginView(ProfileMixin, LogMixin, View): + def dispatch(self, request, *args, **kwargs): + self.debug_login = request.session.get('mellon_debug_login') + with self.capture_logs() if self.debug_login else nullcontext(): + return super().dispatch(request, *args, **kwargs) + @property def template_base(self): return self.kwargs.get('template_base', 'base.html') @@ -290,9 +298,27 @@ class LoginView(ProfileMixin, LogMixin, View): return self.render(request, 'mellon/user_not_found.html', {'saml_attributes': attributes}) request.session['lasso_session_dump'] = login.session.dump() - return HttpResponseRedirect(next_url) + if self.debug_login: + return self.render_debug_template(request, login, attributes) + else: + return HttpResponseRedirect(next_url) + + def render_debug_template(self, request, login, attributes): + request.session['mellon_debug_login'] = False + context = { + 'logs': self.stream.getvalue(), + 'attributes': attributes, + 'login': login, + 'response_dump': login.response and login.response.debug(4), + 'assertion_dump': login.assertion and login.assertion.debug(4), + } + return self.render(request, 'mellon/debug_login.html', context) def login(self, user, attributes): + if self.debug_login: + self.log.info('mellon: would login user %s (username %s)', user.get_full_name(), user) + return + utils.login(self.request, user) session_index = attributes['session_index'] if session_index: @@ -540,6 +566,17 @@ class LoginView(ProfileMixin, LogMixin, View): node.text = hint self.add_extension_node(authn_request, node) + @contextmanager + def capture_logs(self): + self.stream = StringIO() + handler = logging.StreamHandler(self.stream) + handler.setLevel(logging.DEBUG) + self.log.root.addHandler(handler) + try: + yield + finally: + self.log.root.removeHandler(handler) + # we need fine control of transactions to prevent double user creations login = transaction.non_atomic_requests(csrf_exempt(LoginView.as_view())) @@ -718,3 +755,17 @@ logout = csrf_exempt(LogoutView.as_view()) def metadata(request, **kwargs): metadata = utils.create_metadata(request) return HttpResponse(metadata, content_type='text/xml') + + +class DebugLoginView(RedirectView): + pattern_name = 'mellon_login' + query_string = True + + def dispatch(self, request, *args, **kwargs): + if not settings.DEBUG: + return HttpResponseForbidden() + request.session['mellon_debug_login'] = True + return super().dispatch(request, *args, **kwargs) + + +debug_login = csrf_exempt(DebugLoginView.as_view()) diff --git a/tests/test_sso_slo.py b/tests/test_sso_slo.py index e7fd819..f9ec6b1 100644 --- a/tests/test_sso_slo.py +++ b/tests/test_sso_slo.py @@ -16,6 +16,7 @@ from __future__ import unicode_literals import datetime +from html import unescape import re import base64 import zlib @@ -713,3 +714,21 @@ def test_sso_user_change(db, app, idp, caplog, sp_settings): reverse('mellon_login'), params={'SAMLResponse': other_body, 'RelayState': other_relay_state} ) assert 'created new user' in caplog.text + + +def test_debug_sso(db, app, idp, caplog, sp_settings, settings): + response = app.get(reverse('mellon_debug_login') + '?next=/whatever/', status=403) + + settings.DEBUG = True + response = app.get(reverse('mellon_debug_login') + '?next=/whatever/') + assert urlparse.urlparse(response['Location']).path == '/login/' + response = response.follow() + url, body, relay_state = idp.process_authn_request_redirect(response['Location']) + assert url.endswith(reverse('mellon_login')) + response = app.post(reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state}) + response_text = unescape(response.text) + assert 'Attributes' in response.text + assert "'email': ['john.doe@gmail.com']" in response_text + assert '