85 lines
2.8 KiB
Python
85 lines
2.8 KiB
Python
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
|