import logging import ldap from django.contrib.auth import get_user_model, load_backend from django.core.management.base import BaseCommand from django_auth_ldap.config import LDAPSearch import django.conf from urllib.error import HTTPError from postorius.models import List, MailmanUser from postorius.utils import get_mailman_client from allauth.account.models import EmailAddress logger = logging.getLogger(__name__) def execute_ldap_search_without_hiding_errors(search, connection, filterargs=(), escape=True): '''If an LDAPError occurs during search.execute, it logs the error and returns an empty list of results. This behavor is indistinguishable from a query with no results.''' if not isinstance(search, LDAPSearch): raise NotImplementedError(f'{type(search)} is not supported by postorius_ldap_membership_management') # This is a copy of django_auth_ldap.config.LDAPSearch.execute without the # ldap.LDAPError eating try-except block if escape: filterargs = search._escape_filterargs(filterargs) filterstr = search.filterstr % filterargs results = connection.search_s(search.base_dn, search.scope, filterstr, search.attrlist) return search._process_results(results) class Command(BaseCommand): can_import_settings = True help = 'Synchronize mailing list memberships from a LDAP server' def handle(self, *args, **options): ldap_backend = load_backend('django_auth_ldap.backend.LDAPBackend') conn = ldap_backend.ldap.initialize(ldap_backend.settings.SERVER_URI, bytes_mode=False) for opt, value in ldap_backend.settings.CONNECTION_OPTIONS.items(): conn.set_option(opt, value) if ldap_backend.settings.START_TLS: conn.start_tls_s() conn.simple_bind_s(ldap_backend.settings.BIND_DN, ldap_backend.settings.BIND_PASSWORD) client = get_mailman_client() django_users = get_user_model().objects.filter(is_active=True) backref_mapping = {'member': 'members', 'moderator': 'moderators', 'owner': 'owners'} mm_users = {} membership_settings = getattr(django.conf.settings, 'LDAP_MEMBERSHIP_SYNC', {}) # create all django user in mailman for user in django_users: mm_users[user.username] = MailmanUser.objects.get_or_create_from_django(user) if mm_users[user.username].display_name != user.get_full_name(): logger.warning("update display_name on {} to {}".format(user.username, user.get_full_name())) mm_users[user.username].display_name = user.get_full_name() mm_users[user.username].save() # set user mail adresses to verified if they match those in ldap user_emails = EmailAddress.objects.filter(user=user) for mail in user_emails: if mail.email == user.email and not mail.verified: logger.warning("update email.verified on user {} for address {} to True".format(user.username, mail.email)) mail.verified = True mail.save() mailman_id2username = {mm_users[i].user_id: i for i in mm_users} for list_name in membership_settings: ldap_setting = membership_settings[list_name].get('ldap', {}) for membership_type in ldap_setting: if not ldap_setting[membership_type].get('enabled'): continue ldap_members = execute_ldap_search_without_hiding_errors(LDAPSearch( ldap_setting[membership_type]['dn'], ldap.SCOPE_SUBTREE, ldap_setting[membership_type]['filter'], [ldap_setting[membership_type]['username_attr']]), conn) ldap_member_names = [list(attr.values())[0][0] for dn, attr in ldap_members] # we refetch the mm_list each time because of wired caching problems mm_list = client.get_list(list_name) mm_members = getattr(mm_list, backref_mapping[membership_type], []) for mm_member in mm_members: try: username = mailman_id2username.get(mm_member.user.user_id, None) if not username or not username in ldap_member_names: # user should not be subscribed -> remove logger.warning("remove {} ( {} ) as {} on {}".format(username, mm_member.user, membership_type, list_name)) mm_member.unsubscribe() except Exception as e: logger.exception(e) continue for username in mm_users: if not username in ldap_member_names: continue try: mm_user = mm_users[username] displayname = mm_users[username].display_name or username try: # user might not be subscribed but should be subscribed -> subscribe # we do not test if he is subscibed already because the mailman api is inconsistent and reports wrong state information for this... # instead we always try to subscribe and catch the error if we are subscribed already for address in mm_user.addresses: user_mail = address.email if membership_type == 'member': # discard a subscription request before trying to subscribe else it will fail for request in mm_list.requests: if request['email'] == user_mail: mm_list.discard_request(request['token']) mm_list.subscribe(user_mail, display_name=displayname, pre_verified=True, pre_confirmed=True, pre_approved=True) elif membership_type == 'moderator': mm_list.add_moderator(user_mail, display_name=displayname) elif membership_type == 'owner': mm_list.add_owner(user_mail, display_name=displayname) logger.warning("subscribe {} ( {} ) as {} on {}".format(username, user_mail, membership_type, list_name)) except HTTPError as e: # We get a http error if the user is already subscribed. # It is silently ignored if e.code == 409 and e.reason == b'Member already subscribed': continue except Exception as e: logger.exception(e) continue