from typing import Optional import dns.resolver import ldap3 from zope import component from zope.interface import implementer from ceo_common.interfaces import IADLDAPService, IConfig from ceo_common.logger_factory import logger_factory from ceo_common.model import ADLDAPRecord from ceo_common.utils import is_in_development logger = logger_factory(__name__) @implementer(IADLDAPService) class ADLDAPService: def __init__(self): cfg = component.getUtility(IConfig) if is_in_development(): self.adldap_dns_srv_name = None self.adldap_server_url = cfg.get('adldap_server_url') else: self.adldap_dns_srv_name = cfg.get('adldap_dns_srv_name') # Perform the actual DNS query later so that we don't delay startup self.adldap_server_url = None self.adldap_base = cfg.get('adldap_base') def _get_server_url(self) -> str: assert self.adldap_dns_srv_name is not None answers = dns.resolver.resolve(self.adldap_dns_srv_name, 'SRV') target = answers[0].target.to_text() # Strip the trailing '.' target = target[:-1] logger.debug('Using AD LDAP server %s', target) # ldaps doesn't seem to work return 'ldap://' + target def _get_conn(self) -> ldap3.Connection: if self.adldap_server_url is None: self.adldap_server_url = self._get_server_url() # When get_info=ldap3.SCHEMA (the default), ldap3 tries to search # for schema information in 'CN=Schema,CN=Configuration,DC=ds,DC=uwaterloo,DC=ca', # which doesn't exist so a LDAPNoSuchObjectResult exception is raised. # To avoid this, we tell ldap3 not to look up any server info. server = ldap3.Server(self.adldap_server_url, get_info=ldap3.NONE) return ldap3.Connection( server, auto_bind=True, read_only=True, raise_exceptions=True) def get_user(self, username: str) -> Optional[ADLDAPRecord]: conn = self._get_conn() conn.search( self.adldap_base, f'(sAMAccountName={username})', attributes=ADLDAPRecord.ldap_attributes, size_limit=1) if not conn.entries: return None return ADLDAPRecord.deserialize_from_ldap(conn.entries[0])