views: add debug login view (#55557)

This commit is contained in:
Valentin Deniaud 2021-08-03 11:15:57 +02:00
parent dde8fa5d02
commit dbdd6fd70b
4 changed files with 83 additions and 2 deletions

View File

@ -0,0 +1,10 @@
{% load i18n %}
{% block content %}
<p><a class="button" href="{% url 'mellon_debug_login' %}">{% trans "Try again" %}</a></p>
<p><strong>{% trans "Attributes:" %}</strong> <pre>{{ attributes|pprint }}</pre></p>
<p><strong>{% trans "SAML assertion:" %}</strong> <pre>{{ assertion_dump }}</pre></p>
<p><strong>{% trans "SAML response:" %}</strong> <pre>{{ response_dump }}</pre></p>
<p><strong>{% trans "SAML artifact:" %}</strong> <pre>{{ login.msgBody }}</pre></p>
<p><strong>{% trans "Logs:" %}</strong> <pre>{{ logs }}</pre></p>
{% endblock %}

View File

@ -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'),
]

View File

@ -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())

View File

@ -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 '<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"' in response_text
assert '<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"' in response_text
assert 'mellon: created new user _' in response_text