From f752486294039c4a34d0a68ed9a324c267856062 Mon Sep 17 00:00:00 2001
From: Paul Marillonnet <pmarillonnet@entrouvert.com>
Date: Wed, 24 Nov 2021 16:12:00 +0100
Subject: [PATCH] manager: add a technical info page with ldap configs (#58938)

---
 .../static/authentic2/manager/css/style.css   |   9 ++
 .../authentic2/manager/homepage.html          |   3 +
 .../authentic2/manager/ldap_details.html      |  14 +++
 .../authentic2/manager/tech_info.html         |  17 +++
 src/authentic2/manager/urls.py                |   2 +
 src/authentic2/manager/views.py               |  37 +++++++
 tests/test_ldap.py                            | 100 ++++++++++++++++++
 7 files changed, 182 insertions(+)
 create mode 100644 src/authentic2/manager/templates/authentic2/manager/ldap_details.html
 create mode 100644 src/authentic2/manager/templates/authentic2/manager/tech_info.html

diff --git a/src/authentic2/manager/static/authentic2/manager/css/style.css b/src/authentic2/manager/static/authentic2/manager/css/style.css
index a9e6441a..6793ab00 100644
--- a/src/authentic2/manager/static/authentic2/manager/css/style.css
+++ b/src/authentic2/manager/static/authentic2/manager/css/style.css
@@ -277,3 +277,12 @@ table.main.plaintable tr:hover td {
 a.role-inheritance-view-all {
     font-style: italic;
 }
+
+div.a2-manager-ldap pre {
+	background: white;
+	border: 1px solid black;
+	padding: .3em;
+	overflow-x: auto;
+	white-space: pre-wrap;
+	word-wrap: break-word;
+}
diff --git a/src/authentic2/manager/templates/authentic2/manager/homepage.html b/src/authentic2/manager/templates/authentic2/manager/homepage.html
index 3f9ea2b1..36f39908 100644
--- a/src/authentic2/manager/templates/authentic2/manager/homepage.html
+++ b/src/authentic2/manager/templates/authentic2/manager/homepage.html
@@ -17,6 +17,9 @@
     {% if user.is_superuser or can_view_journal %}
     <li><a href="{% url 'a2-manager-journal' %}">{% trans 'Journal' %}</a></li>
     {% endif %}
+    {% if user.is_superuser %}
+    <li><a href="{% url 'a2-manager-tech-info' %}">{% trans 'Technical information' %}</a></li>
+    {% endif %}
   </ul>
   </span>
   {% endif %}
diff --git a/src/authentic2/manager/templates/authentic2/manager/ldap_details.html b/src/authentic2/manager/templates/authentic2/manager/ldap_details.html
new file mode 100644
index 00000000..97cd7535
--- /dev/null
+++ b/src/authentic2/manager/templates/authentic2/manager/ldap_details.html
@@ -0,0 +1,14 @@
+{% load i18n %}
+<h4>{% trans "Realm:" %} {{ ldap.realm }}</h4>
+<div class="a2-manager-ldap-{{ ldap.realm }}">
+{% if not error %}
+<h5>{% blocktrans %}Base ldapsearch command:{% endblocktrans %}</h5>
+ <pre>ldapsearch -v -H {{ ldap.ldap_uri }} -D "{{ ldap.binddn }}" -w "{{ ldap.bindpw }}" -b "{{ ldap.basedn }}"{% if ldap.user_filter or ldap.sync_ldap_users_filter %} "{% firstof ldap.sync_ldap_users_filter ldap.user_filter %}"{% endif %}</pre>
+{% else %}
+<div class="error">
+{% blocktrans %}Error while attempting to connect to LDAP server, base ldapsearch command won't be displayed.{% endblocktrans %}
+</div>
+{% endif %}
+<h5>{% trans "Configuration:" %}</h5>
+<pre>{{ ldap|pprint }}</pre>
+</div>
diff --git a/src/authentic2/manager/templates/authentic2/manager/tech_info.html b/src/authentic2/manager/templates/authentic2/manager/tech_info.html
new file mode 100644
index 00000000..cbcb1b61
--- /dev/null
+++ b/src/authentic2/manager/templates/authentic2/manager/tech_info.html
@@ -0,0 +1,17 @@
+{% extends "authentic2/manager/base.html" %}
+{% load i18n %}
+
+{% block appbar %}
+  <h2>{% blocktrans %}{{ title }}{% endblocktrans %}</h2>
+{% endblock %}
+
+{% block content %}
+  {% if ldap_list %}
+  <h3>{% trans "LDAP information" %}</h3>
+  <div id="a2-manager-tech-info-ldap-list">
+    {% for ldap in ldap_list %}
+     {% include "authentic2/manager/ldap_details.html" with ldap=ldap %}
+    {% endfor %}
+  </div>
+{% endif %}
+{% endblock %}
diff --git a/src/authentic2/manager/urls.py b/src/authentic2/manager/urls.py
index b8fb76bc..7147a9ee 100644
--- a/src/authentic2/manager/urls.py
+++ b/src/authentic2/manager/urls.py
@@ -181,6 +181,8 @@ urlpatterns = required(
         # general management
         url(r'^site-export/$', views.site_export, name='a2-manager-site-export'),
         url(r'^site-import/$', views.site_import, name='a2-manager-site-import'),
+        # technical information including ldap config
+        url(r'^tech-info/$', views.tech_info, name='a2-manager-tech-info'),
     ],
 )
 
diff --git a/src/authentic2/manager/views.py b/src/authentic2/manager/views.py
index cdde0a2e..2b83300f 100644
--- a/src/authentic2/manager/views.py
+++ b/src/authentic2/manager/views.py
@@ -661,6 +661,43 @@ class HomepageView(TitleMixin, PermissionMixin, MediaMixin, TemplateView):
 homepage = HomepageView.as_view()
 
 
+class TechnicalInformationView(TitleMixin, MediaMixin, TemplateView):
+    template_name = 'authentic2/manager/tech_info.html'
+    title = _("Technical information")
+
+    def get(self, request, *args, **kwargs):
+        if not request.user.is_superuser:
+            raise PermissionDenied
+        return super().get(request, *args, **kwargs)
+
+    def get_context_data(self, **kwargs):
+        import ldap
+
+        from authentic2.backends import ldap_backend
+
+        backend = ldap_backend.LDAPBackend
+        kwargs['ldap_list'] = []
+        for block in backend.get_config():
+            config = block.copy()
+            conn = backend.get_connection(config)
+            if not conn:
+                kwargs['error'] = True
+            else:
+                # retrieve ldap uri, not directly visible in configuration block
+                config['ldap_uri'] = conn.get_option(ldap.OPT_URI)
+                # user filters need to be formatted to ldapsearch syntax
+                config['user_filter'] = force_text(block.get('user_filter'), '').replace('%s', '*')
+                config['sync_ldap_users_filter'] = (
+                    force_text(block.get('sync_ldap_users_filter'), '').replace('%s', '*').replace('%s', '*')
+                )
+
+            kwargs['ldap_list'].append(config)
+        return super().get_context_data(**kwargs)
+
+
+tech_info = TechnicalInformationView.as_view()
+
+
 class MenuJson(HomepageView):
     def get(self, request, *args, **kwargs):
         menu_entries = []
diff --git a/tests/test_ldap.py b/tests/test_ldap.py
index 553fb28f..50bbe13f 100644
--- a/tests/test_ldap.py
+++ b/tests/test_ldap.py
@@ -26,6 +26,7 @@ import pytest
 from django.contrib.auth import get_user_model
 from django.core import mail, management
 from django.core.exceptions import ImproperlyConfigured
+from django.urls import reverse
 from django.utils import timezone
 from django.utils.encoding import force_bytes, force_text
 from ldap.dn import escape_dn_chars
@@ -2108,3 +2109,102 @@ def test_user_journal_login_failure(slapd, settings, client, db, monkeypatch, ex
     )
     user = ldap_backend.LDAPUser.objects.get(username='%s@ldap' % UID)
     utils.assert_event('user.login.failure', user=user, username=UID, reason=exception[1])
+
+
+def test_technical_info_ldap(app, admin, superuser, slapd, settings, monkeypatch):
+    settings.LDAP_AUTH_SETTINGS = [
+        {
+            'url': [slapd.ldap_url],
+            'binddn': force_text('cn=%s,o=ôrga' % escape_dn_chars('Étienne Michu')),
+            'bindpw': 'passé',
+            'basedn': 'o=ôrga',
+            'use_tls': False,
+        }
+    ]
+
+    utils.login(app, admin, 'a2-manager-homepage')
+    app.get(reverse('a2-manager-tech-info'), status=403)
+    utils.logout(app)
+
+    resp = utils.login(app, superuser, 'a2-manager-tech-info')
+    ldap_config_text = resp.pyquery('div#a2-manager-tech-info-ldap-list').text()
+
+    assert 'Base ldapsearch command' in ldap_config_text
+    assert 'ldapsearch -v -H ldapi://' in ldap_config_text
+    assert '-D "cn=Étienne Michu,o=ôrga" -w "passé" -b "o=ôrga" "(|(mail=*)(uid=*))"' in ldap_config_text
+
+    for opt in [
+        'active_directory',
+        'attribute_mappings',
+        'attributes',
+        'basedn',
+        'bind_with_username',
+        'binddn',
+        'bindpw',
+        'bindsasl',
+        'cacertdir',
+        'cacertfile',
+        'can_reset_password',
+        'certfile',
+        'clean_external_id_on_update',
+        'connect_with_user_credentials',
+        'create_group',
+        'disable_update',
+        'email_field',
+        'external_id_tuples',
+        'extra_attributes',
+        'fname_field',
+        'global_ldap_options',
+        'group_basedn',
+        'group_filter',
+        'group_mapping',
+        'group_to_role_mapping',
+        'groupactive',
+        'groupstaff',
+        'groupsu',
+        'is_staff',
+        'is_superuser',
+        'keep_password',
+        'keep_password_in_session',
+        'keyfile',
+        'ldap_options',
+        'ldap_uri',
+        'limit_to_realm',
+        'lname_field',
+        'lookups',
+        'mandatory_attributes_values',
+        'member_of_attribute',
+        'multimatch',
+        'ou_slug',
+        'ppolicy_dn',
+        'realm',
+        'referrals',
+        'replicas',
+        'require_cert',
+        'set_mandatory_groups',
+        'set_mandatory_roles',
+        'shuffle_replicas',
+        'sync_ldap_users_filter',
+        'timeout',
+        'update_username',
+        'url',
+        'use_controls',
+        'use_first_url_for_external_id',
+        'use_password_modify',
+        'use_tls',
+        'user_attributes',
+        'user_basedn',
+        'user_can_change_password',
+        'user_dn_template',
+        'user_filter',
+        'username_template',
+    ]:
+        assert opt in ldap_config_text
+
+    # mock a buggy connection
+    monkeypatch.setattr(ldap_backend.LDAPBackend, 'get_connection', lambda x: None)
+    resp = app.get(reverse('a2-manager-tech-info'))
+    ldap_config_text = resp.pyquery('div#a2-manager-tech-info-ldap-list').text()
+
+    assert 'Base ldapsearch command' not in ldap_config_text
+    assert 'Error while attempting to connect to LDAP server' in ldap_config_text
-- 
2.34.1

