From 98c08e46ad50de83c6a148a63cb6630576a65db2 Mon Sep 17 00:00:00 2001
From: Paul Marillonnet <pmarillonnet@entrouvert.com>
Date: Tue, 11 Jan 2022 11:34:27 +0100
Subject: [PATCH] ldap: add an option to indicate provisionning only purposes
 (#60492)

---
 src/authentic2/backends/ldap_backend.py        | 10 +++++++++-
 .../management/commands/sync-ldap-users.py     |  2 +-
 tests/test_ldap.py                             | 18 ++++++++++++++++++
 3 files changed, 28 insertions(+), 2 deletions(-)

diff --git a/src/authentic2/backends/ldap_backend.py b/src/authentic2/backends/ldap_backend.py
index a4b19b29..3cd45378 100644
--- a/src/authentic2/backends/ldap_backend.py
+++ b/src/authentic2/backends/ldap_backend.py
@@ -517,6 +517,8 @@ class LDAPBackend:
         # https://www.python-ldap.org/en/python-ldap-3.3.0/reference/ldap.html#ldap-controls
         'use_controls': False,
         'ppolicy_dn': '',
+        # used to indicated that this directory serves provisionning purposes only (no direct authn)
+        'provisionning_only': False,
     }
     _REQUIRED = ('url', 'basedn')
     _TO_ITERABLE = ('url', 'groupsu', 'groupstaff', 'groupactive')
@@ -603,6 +605,8 @@ class LDAPBackend:
 
         # Now we can try to authenticate
         for block in config:
+            if block.get('provisionning_only', False):
+                continue
             uid = username
             # if ou is provided, ignore LDAP server for other OU
             if ou:
@@ -1458,7 +1462,7 @@ class LDAPBackend:
             yield from cls.normalize_ldap_results(data)
 
     @classmethod
-    def get_users(cls, realm=None):
+    def get_users(cls, realm=None, provisionning=False):
         blocks = cls.get_config()
         if not blocks:
             log.info('No LDAP server configured.')
@@ -1466,6 +1470,8 @@ class LDAPBackend:
         for block in blocks:
             if realm and realm != block['realm']:
                 continue
+            if block.get('provisionning_only', False) and not provisionning:
+                continue
 
             log.info('Synchronising users from realm "%s"', block['realm'])
             conn = cls.get_connection(block)
@@ -1820,6 +1826,8 @@ class LDAPBackendPasswordLost(LDAPBackend):
         for user_external_id in user.userexternalid_set.all():
             external_id = user_external_id.external_id
             for block in config:
+                if block.get('provisionning_only', False):
+                    continue
                 if user_external_id.source != force_text(block['realm']):
                     continue
                 for external_id_tuple in map_text(block['external_id_tuples']):
diff --git a/src/authentic2/management/commands/sync-ldap-users.py b/src/authentic2/management/commands/sync-ldap-users.py
index 4fc82b30..6d450e7b 100644
--- a/src/authentic2/management/commands/sync-ldap-users.py
+++ b/src/authentic2/management/commands/sync-ldap-users.py
@@ -58,5 +58,5 @@ class Command(BaseCommand):
         elif verbosity == 3:
             ldap_logger.setLevel(logging.DEBUG)
 
-        for dummy in LDAPBackend.get_users(realm=kwargs['realm']):
+        for dummy in LDAPBackend.get_users(realm=kwargs['realm'], provisionning=True):
             continue
diff --git a/tests/test_ldap.py b/tests/test_ldap.py
index 2a3eefed..4628b91a 100644
--- a/tests/test_ldap.py
+++ b/tests/test_ldap.py
@@ -2208,3 +2208,21 @@ def test_technical_info_ldap(app, admin, superuser, slapd, settings, monkeypatch
 
     assert 'Base ldapsearch command' not in ldap_config_text
     assert 'Error while attempting to connect to LDAP server' in ldap_config_text
+
+
+def test_provisionning_only(slapd, settings, client, db):
+    settings.LDAP_AUTH_SETTINGS = [
+        {
+            'url': [slapd.ldap_url],
+            'basedn': 'o=ôrga',
+            'use_tls': False,
+            'attributes': ['jpegPhoto'],
+            'provisionning_only': True,
+        }
+    ]
+    result = client.post(
+        '/login/', {'login-password-submit': '1', 'username': USERNAME, 'password': PASS}, follow=True
+    )
+    assert result.status_code == 200
+    assert force_bytes('Étienne Michu') not in result.content
+    assert User.objects.count() == 0
-- 
2.34.1

