from ldap3 import Server, Connection, ALL, SUBTREE from flask import current_app class LDAPService: """Wraps ldap3 to sync users from Active Directory.""" ATTRIBUTES = [ 'employeeID', 'sAMAccountName', 'givenName', 'sn', 'mail', 'department', 'title', 'telephoneNumber', 'distinguishedName', 'physicalDeliveryOfficeName', 'userAccountControl', ] def _connect(self): cfg = current_app.config server = Server( cfg['LDAP_SERVER'], port=cfg['LDAP_PORT'], use_ssl=cfg['LDAP_USE_SSL'], get_info=ALL, ) conn = Connection( server, user=cfg['LDAP_BIND_USER'], password=cfg['LDAP_BIND_PASSWORD'], auto_bind=True, ) return conn def sync_users(self): """ Query AD and return a list of dicts ready to be upserted into the User model. Raises an exception if the connection fails. """ cfg = current_app.config conn = self._connect() conn.search( search_base=cfg['LDAP_BASE_DN'], search_filter=cfg['LDAP_USER_SEARCH_FILTER'], search_scope=SUBTREE, attributes=self.ATTRIBUTES, ) wid_attr = cfg['LDAP_WINDOWS_ID_ATTR'] users = [] for entry in conn.entries: # Resolve windows_id from the configured attribute, fall back to sAMAccountName raw_wid = str(getattr(entry, wid_attr, '') or '') if not raw_wid: raw_wid = str(entry.sAMAccountName or '') if not raw_wid: continue # skip entries with no identifier # userAccountControl bit 2 = disabled account uac = 0 try: uac = int(str(entry.userAccountControl or 0)) except (ValueError, TypeError): pass is_active = not bool(uac & 2) users.append({ 'windows_id': raw_wid.strip(), 'first_name': str(entry.givenName or '').strip(), 'last_name': str(entry.sn or '').strip(), 'email': str(entry.mail or '').strip(), 'department': str(entry.department or '').strip(), 'job_title': str(entry.title or '').strip(), 'phone': str(entry.telephoneNumber or '').strip(), 'location': str(entry.physicalDeliveryOfficeName or '').strip(), 'ldap_dn': str(entry.distinguishedName or '').strip(), 'is_active': is_active, }) conn.unbind() return users def test_connection(self): """Returns True if a bind succeeds, raises on failure.""" conn = self._connect() conn.unbind() return True