From f597d8cf069155cd58239bec5c891fc7ce9ed39b Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Thu, 26 Aug 2021 18:09:44 +0200 Subject: [PATCH] ldap: add useful output to sync-ldap-users command (#54078) --- src/authentic2/backends/ldap_backend.py | 30 +++++++++++++-- .../management/commands/sync-ldap-users.py | 31 ++++++++++++++-- tests/test_ldap.py | 37 +++++++++++++++---- 3 files changed, 84 insertions(+), 14 deletions(-) diff --git a/src/authentic2/backends/ldap_backend.py b/src/authentic2/backends/ldap_backend.py index 6a26a7ae..802e2cd0 100644 --- a/src/authentic2/backends/ldap_backend.py +++ b/src/authentic2/backends/ldap_backend.py @@ -346,6 +346,7 @@ LDAP_DEACTIVATION_REASON_OLD_SOURCE = 'ldap-old-source' class LDAPUser(User): SESSION_LDAP_DATA_KEY = 'ldap-data' _changed = False + _created = False class Meta: proxy = True @@ -666,7 +667,6 @@ class LDAPBackend: # First get our configuration into a standard format for block in blocks: cls.update_default(block) - log.debug('got config %r', blocks) return blocks @classmethod @@ -1499,6 +1499,7 @@ class LDAPBackend: user.keep_password(password) self.populate_user(user, dn, username, conn, block, attributes) if not user.pk or getattr(user, '_changed', False): + user._created = bool(not user.pk) user.save() if not is_user_authenticable(user): @@ -1543,7 +1544,12 @@ class LDAPBackend: @classmethod def get_users(cls): - for block in cls.get_config(): + blocks = cls.get_config() + if not blocks: + log.info('No LDAP server configured.') + return + for block in blocks: + log.info('Synchronising users from realm "%s"', block['realm']) conn = cls.get_connection(block) if conn is None: log.warning('unable to synchronize with LDAP servers %s', force_text(block['url'])) @@ -1556,8 +1562,20 @@ class LDAPBackend: conn, user_basedn, ldap.SCOPE_SUBTREE, user_filter, attrlist=attribute_names ) backend = cls() + count = 0 for dn, attrs in results: - yield backend._return_user(dn, None, conn, block, attrs) + count += 1 + user = backend._return_user(dn, None, conn, block, attrs) + if user._changed or user._created: + log.info( + '%s user %s (uuid %s) from %s', + 'Created' if user._created else 'Updated', + user.get_username(), + user.uuid, + ', '.join('%s=%s' % (k, v) for k, v in attrs.items()), + ) + yield user + log.info('Search for %s returned %s users.', user_filter, count) @classmethod def deactivate_orphaned_users(cls): @@ -1721,22 +1739,28 @@ class LDAPBackend: @classmethod def bind(cls, block, conn, credentials=()): '''Bind to the LDAP server''' + ldap_uri = conn.get_option(ldap.OPT_URI) try: if credentials: who, password = credentials[0], credentials[1] password = force_text(password) conn.simple_bind_s(who, password) + log_message = 'with user %s' % who elif block['bindsasl']: sasl_mech, who, sasl_params = map_text(block['bindsasl']) handler_class = getattr(ldap.sasl, sasl_mech) auth = handler_class(*sasl_params) conn.sasl_interactive_bind_s(who, auth) + log_message = 'with account %s' % who elif block['binddn'] and block['bindpw']: who = force_text(block['binddn']) conn.simple_bind_s(who, force_text(block['bindpw'])) + log_message = 'with binddn %s' % who else: who = 'anonymous' conn.simple_bind_s() + log_message = 'anonymously' + log.info('Binding to server %s (%s)', ldap_uri, log_message) return True, None except ldap.INVALID_CREDENTIALS: return False, 'invalid credentials' diff --git a/src/authentic2/management/commands/sync-ldap-users.py b/src/authentic2/management/commands/sync-ldap-users.py index 9d106db1..e7b97fd0 100644 --- a/src/authentic2/management/commands/sync-ldap-users.py +++ b/src/authentic2/management/commands/sync-ldap-users.py @@ -21,6 +21,8 @@ try: except ImportError: ldap = None +import logging + from django.core.management.base import BaseCommand from authentic2.backends.ldap_backend import LDAPBackend @@ -28,9 +30,30 @@ from authentic2.backends.ldap_backend import LDAPBackend class Command(BaseCommand): def handle(self, *args, **kwargs): + root_logger = logging.getLogger() + ldap_logger = logging.getLogger('authentic2.backends.ldap_backend') + + # ensure log messages are displayed only once on terminal + stream_handlers = [ + x for x in root_logger.handlers if isinstance(x, logging.StreamHandler) if x.stream.isatty() + ] + if stream_handlers: + handler = stream_handlers[0] + else: + handler = logging.StreamHandler() + ldap_logger.addHandler(handler) + + # add timestamp to messages + formatter = logging.Formatter(fmt='%(asctime)s %(message)s', datefmt='%Y-%m-%d %H:%M:%S') + handler.setFormatter(formatter) + verbosity = int(kwargs['verbosity']) - if verbosity > 1: - print('Updated users :') + if verbosity == 1: + ldap_logger.setLevel(logging.ERROR) + elif verbosity == 2: + ldap_logger.setLevel(logging.INFO) + elif verbosity == 3: + ldap_logger.setLevel(logging.DEBUG) + for user in LDAPBackend.get_users(): - if getattr(user, '_changed', False) and verbosity > 1: - print(' -', user.uuid, user.get_username(), user.get_full_name()) + continue diff --git a/tests/test_ldap.py b/tests/test_ldap.py index a3189a44..cd0cb1f0 100644 --- a/tests/test_ldap.py +++ b/tests/test_ldap.py @@ -15,6 +15,7 @@ # along with this program. If not, see . import json +import logging import os import time import urllib.parse @@ -1605,7 +1606,16 @@ def test_ou_selector_default_ou(slapd, settings, app, ou1): assert '_auth_user_id' in app.session -def test_sync_ldap_users(slapd, settings, app, db, capsys): +def test_sync_ldap_users(slapd, settings, app, db, caplog): + caplog.set_level(logging.DEBUG) # force pytest to reset log level after test + + management.call_command('sync-ldap-users') + assert len(caplog.records) == 0 + + management.call_command('sync-ldap-users', verbosity=2) + assert len(caplog.records) == 1 + assert caplog.records[0].message == 'No LDAP server configured.' + settings.LDAP_AUTH_SETTINGS = [ { 'url': [slapd.ldap_url], @@ -1633,9 +1643,18 @@ def test_sync_ldap_users(slapd, settings, app, db, capsys): ) assert User.objects.count() == 0 - capsys.readouterr() + caplog.clear() management.call_command('sync-ldap-users', verbosity=2) - assert len(capsys.readouterr().out.splitlines()) == 7 + assert len(caplog.records) == 9 + assert caplog.messages[0] == 'Synchronising users from realm "ldap"' + assert caplog.messages[1] == 'Binding to server %s (anonymously)' % slapd.ldap_url + assert ( + caplog.messages[2] + == "Updated user etienne.michu@ldap (uuid %s) from dn=cn=Étienne Michu,o=ôrga, uid=['etienne.michu'], sn=['Michu'], givenname=['Étienne'], l=['Paris'], mail=['etienne.michu@example.net']" + % User.objects.first().uuid + ) + assert caplog.messages[-1] == 'Search for (|(mail=*)(uid=*)) returned 6 users.' + assert User.objects.count() == 6 assert all(user.first_name == 'Étienne' for user in User.objects.all()) assert all(user.attributes.first_name == 'Étienne' for user in User.objects.all()) @@ -1652,9 +1671,11 @@ def test_sync_ldap_users(slapd, settings, app, db, capsys): for user in User.objects.all() ] ) - capsys.readouterr() - management.call_command('sync-ldap-users', verbosity=2) - assert len(capsys.readouterr().out.splitlines()) == 1 + + User.objects.update(first_name='John') + caplog.clear() + management.call_command('sync-ldap-users', verbosity=3) + assert len(caplog.records) == 39 def test_alert_on_wrong_user_filter(slapd, settings, client, db, caplog): @@ -1819,7 +1840,9 @@ def test_config_to_lowercase(): } -def test_switch_user_ldap_user(slapd, settings, app, db): +def test_switch_user_ldap_user(slapd, settings, app, db, caplog): + caplog.set_level(logging.DEBUG) # force pytest to reset log level after test + settings.LDAP_AUTH_SETTINGS = [ { 'url': [slapd.ldap_url], -- 2.30.2