From 2a62b0e41cbfd05b3f671ab26eba02965cea79c1 Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Thu, 20 Oct 2022 14:45:25 +0200 Subject: [PATCH 2/2] auth_saml: display xml metadata in separate view (#70492) --- src/authentic2/apps/authenticators/models.py | 10 +++++---- src/authentic2_auth_saml/models.py | 9 ++++++++ src/authentic2_auth_saml/urls.py | 11 ++++++++-- src/authentic2_auth_saml/views.py | 23 ++++++++++++++++++++ tests/test_manager_authenticators.py | 19 ++++++++++++++++ 5 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src/authentic2/apps/authenticators/models.py b/src/authentic2/apps/authenticators/models.py index b0cf3dc2d..cfcc29612 100644 --- a/src/authentic2/apps/authenticators/models.py +++ b/src/authentic2/apps/authenticators/models.py @@ -24,6 +24,7 @@ from django.db import models from django.db.models import Max from django.shortcuts import render, reverse from django.utils.formats import date_format +from django.utils.html import format_html from django.utils.text import capfirst from django.utils.translation import gettext_lazy as _ from django.utils.translation import pgettext_lazy @@ -126,10 +127,11 @@ class BaseAuthenticator(models.Model): if isinstance(value, datetime.datetime): value = date_format(value, 'DATETIME_FORMAT') - yield _('%(field)s: %(value)s') % { - 'field': capfirst(self._meta.get_field(field).verbose_name), - 'value': value, - } + yield format_html( + _('{field}: {value}'), + field=capfirst(self._meta.get_field(field).verbose_name), + value=value, + ) def shown(self, ctx=()): if not self.show_condition: diff --git a/src/authentic2_auth_saml/models.py b/src/authentic2_auth_saml/models.py index 8730998fd..3aabf1e45 100644 --- a/src/authentic2_auth_saml/models.py +++ b/src/authentic2_auth_saml/models.py @@ -21,6 +21,8 @@ from django.conf import settings from django.contrib.postgres.fields import JSONField from django.core.exceptions import ValidationError from django.db import models +from django.urls import reverse +from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from authentic2.apps.authenticators.models import ( @@ -221,6 +223,13 @@ class SAMLAuthenticator(BaseAuthenticator): getattr(settings, 'MELLON_PRIVATE_KEY', '') and getattr(settings, 'MELLON_PUBLIC_KEYS', '') ) + def get_metadata_display(self): + if not self.metadata: + return '' + + url = reverse('a2-manager-saml-authenticator-metadata', kwargs={'pk': self.pk}) + return mark_safe('%s' % (url, _('View metadata'))) + def login(self, request, *args, **kwargs): from . import views diff --git a/src/authentic2_auth_saml/urls.py b/src/authentic2_auth_saml/urls.py index 1530de3ad..ffd0579a1 100644 --- a/src/authentic2_auth_saml/urls.py +++ b/src/authentic2_auth_saml/urls.py @@ -14,12 +14,19 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.urls import include, re_path +from django.urls import include, path, re_path + +from . import views urlpatterns = [ re_path( r'^accounts/saml/', include('mellon.urls'), kwargs={'template_base': 'authentic2/base.html', 'logout_next_url': '/logout/'}, - ) + ), + path( + 'authenticators//metadata.xml', + views.authenticator_metadata, + name='a2-manager-saml-authenticator-metadata', + ), ] diff --git a/src/authentic2_auth_saml/views.py b/src/authentic2_auth_saml/views.py index 1cb44cf3d..16c3d8525 100644 --- a/src/authentic2_auth_saml/views.py +++ b/src/authentic2_auth_saml/views.py @@ -1,9 +1,15 @@ +import xml.etree.ElementTree as ET + +from django.http import Http404, HttpResponse from django.shortcuts import render from django.template.loader import render_to_string +from django.views.generic import DetailView from mellon.utils import get_idp from authentic2.utils.misc import redirect_to_login +from .models import SAMLAuthenticator + def login(request, authenticator, *args, **kwargs): context = kwargs.pop('context', {}).copy() @@ -34,3 +40,20 @@ def profile(request, *args, **kwargs): user_saml_identifier.idp = get_idp(user_saml_identifier.issuer.entity_id) context['user_saml_identifiers'] = user_saml_identifiers return render_to_string('authentic2_auth_saml/profile.html', context, request=request) + + +class SAMLAuthenticatorMetadataView(DetailView): + model = SAMLAuthenticator + + def get(self, *args, **kwargs): + authenticator = self.get_object() + if not authenticator.metadata: + raise Http404() + + metadata = ET.fromstring(authenticator.metadata) + ET.indent(metadata) + + return HttpResponse(ET.tostring(metadata, encoding='utf-8'), content_type='text/xml') + + +authenticator_metadata = SAMLAuthenticatorMetadataView.as_view() diff --git a/tests/test_manager_authenticators.py b/tests/test_manager_authenticators.py index 53bb78b25..3a31c4587 100644 --- a/tests/test_manager_authenticators.py +++ b/tests/test_manager_authenticators.py @@ -505,6 +505,25 @@ def test_authenticators_saml_validate_metadata(app, superuser): resp.form.submit(status=302) +def test_authenticators_saml_view_metadata(app, superuser): + authenticator = SAMLAuthenticator.objects.create(slug='idp1') + + resp = login(app, superuser) + resp = app.get('/manage/authenticators/%s/detail/' % authenticator.pk) + + assert 'Metadata (XML):' not in resp.text + assert app.get('/manage/authenticators/%s/metadata.xml' % authenticator.pk, status=404) + + authenticator.metadata = '' + authenticator.save() + + resp = app.get('/manage/authenticators/%s/detail/' % authenticator.pk) + assert 'Metadata (XML):' in resp.text + + resp = resp.click('View metadata') + assert resp.text == '\n \n' + + def test_authenticators_saml_missing_signing_key(app, superuser, settings): authenticator = SAMLAuthenticator.objects.create(slug='idp1') -- 2.35.1