Skip to content
Snippets Groups Projects
syncldapmemberships.py 5.57 KiB
Newer Older
  • Learn to ignore specific revisions
  • nd's avatar
    nd committed
    import logging
    
    nd's avatar
    nd committed
    import ldap
    
    nd's avatar
    nd committed
    
    
    nd's avatar
    nd committed
    from django.contrib.auth import get_user_model, load_backend
    
    nd's avatar
    nd committed
    from django.core.management.base import BaseCommand
    
    nd's avatar
    nd committed
    from django_auth_ldap.config import LDAPSearch
    
    nd's avatar
    nd committed
    import django.conf
    
    
    from urllib.error import HTTPError
    
    
    nd's avatar
    nd committed
    from postorius.models import List, MailmanUser
    from postorius.utils import get_mailman_client
    
    from allauth.account.models import EmailAddress
    
    nd's avatar
    nd committed
    
    
    nd's avatar
    nd committed
    logger = logging.getLogger(__name__)
    
    nd's avatar
    nd committed
    
    
    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 django_auth_ldap_remoteuser')
    	# 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)
    
    
    nd's avatar
    nd committed
    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.all()
    
    		backref_mapping = {'member': 'members', 'moderator': 'moderators', 'owner': 'owners'}
    
    nd's avatar
    nd committed
    		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():
    
    nd's avatar
    nd committed
    				logger.warning("update display_name on {} to {}".format(user.username, user.get_full_name()))
    
    nd's avatar
    nd committed
    				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()
    
    
    nd's avatar
    nd committed
    
    
    		mailman_id2username = {mm_users[i].user_id: i for i in mm_users}
    
    
    nd's avatar
    nd committed
    		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(
    
    nd's avatar
    nd committed
    						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]
    
    nd's avatar
    nd committed
    
    				# 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
    
    nd's avatar
    nd committed
    							logger.warning("remove {} ( {} ) as {} on {}".format(username, mm_member.user, membership_type, list_name))
    
    nd's avatar
    nd committed
    							mm_member.unsubscribe()
    					except Exception as e:
    						logger.exception(e)
    						continue
    
    nd's avatar
    nd committed
    
    
    nd's avatar
    nd committed
    				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
    
    								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':
    
    psy's avatar
    psy committed
    									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
    
    nd's avatar
    nd committed
    					except Exception as e:
    						logger.exception(e)
    						continue