4747logger = logging .getLogger (__name__ )
4848
4949
50+ class ActiveDirectoryUPNException (Exception ):
51+ """Raised in case the user's login credentials cannot be mapped to a UPN"""
52+ pass
53+
54+
5055class LDAPMode (object ):
5156 SIMPLE = "simple" ,
5257 SEARCH = "search" ,
@@ -76,18 +81,48 @@ def __init__(self, config, account_handler):
7681 self .ldap_bind_password = config .bind_password
7782 self .ldap_filter = config .filter
7883
84+ self .ldap_active_directory = config .active_directory
85+ if self .ldap_active_directory :
86+ self .ldap_default_domain = config .default_domain
87+
88+ def get_supported_login_types (self ):
89+ return {'m.login.password' : ('password' ,)}
90+
7991 @defer .inlineCallbacks
80- def check_password (self , user_id , password ):
92+ def check_auth (self , username , login_type , login_dict ):
8193 """ Attempt to authenticate a user against an LDAP Server
8294 and register an account if none exists.
8395
8496 Returns:
85- True if authentication against LDAP was successful
97+ Canonical user ID if authentication against LDAP was successful
8698 """
99+ password = login_dict ['password' ]
100+ # According to section 5.1.2. of RFC 4513 an attempt to log in with
101+ # non-empty DN and empty password is called Unauthenticated
102+ # Authentication Mechanism of Simple Bind which is used to establish
103+ # an anonymous authorization state and not suitable for user
104+ # authentication.
87105 if not password :
88106 defer .returnValue (False )
89- # user_id is of the form @foo:bar.com
90- localpart = user_id .split (":" , 1 )[0 ][1 :]
107+
108+ if username .startswith ("@" ) and ":" in username :
109+ # username is of the form @foo:bar.com
110+ username = username .split (":" , 1 )[0 ][1 :]
111+
112+ # Used in LDAP queries as value of ldap_attributes['uid'] attribute.
113+ uid_value = username
114+ # Default display name for the user, if a new account is registered.
115+ default_display_name = username
116+ # Local part of Matrix ID which will be used in registration process
117+ localpart = username
118+
119+ if self .ldap_active_directory :
120+ try :
121+ (login , domain , localpart ) = self ._map_login_to_upn (username )
122+ uid_value = login + "@" + domain
123+ default_display_name = login
124+ except ActiveDirectoryUPNException :
125+ defer .returnValue (False )
91126
92127 try :
93128 tls = ldap3 .Tls (validate = ssl .CERT_REQUIRED )
@@ -102,7 +137,7 @@ def check_password(self, user_id, password):
102137 if self .ldap_mode == LDAPMode .SIMPLE :
103138 bind_dn = "{prop}={value},{base}" .format (
104139 prop = self .ldap_attributes ['uid' ],
105- value = localpart ,
140+ value = uid_value ,
106141 base = self .ldap_base
107142 )
108143 result , conn = yield self ._ldap_simple_bind (
@@ -117,7 +152,7 @@ def check_password(self, user_id, password):
117152 if not result :
118153 defer .returnValue (False )
119154 elif self .ldap_mode == LDAPMode .SEARCH :
120- filters = [(self .ldap_attributes ["uid" ], localpart )]
155+ filters = [(self .ldap_attributes ["uid" ], uid_value )]
121156 result , conn , _ = yield self ._ldap_authenticated_search (
122157 server = server , password = password , filters = filters
123158 )
@@ -148,41 +183,46 @@ def check_password(self, user_id, password):
148183 )
149184 defer .returnValue (False )
150185
186+ # Get full user id from localpart
187+ user_id = self .account_handler .get_qualified_user_id (localpart )
188+
151189 # check if user with user_id exists
152190 if (yield self .account_handler .check_user_exists (user_id )):
153191 # exists, authentication complete
154192 if hasattr (conn , "unbind" ):
155193 yield threads .deferToThread (conn .unbind )
156- defer .returnValue (True )
194+ defer .returnValue (user_id )
157195
158196 else :
159197 # does not exist, register
160198 if self .ldap_mode == LDAPMode .SEARCH :
161199 # search enabled, fetch metadata for account creation from
162200 # existing ldap connection
163- filters = [(self .ldap_attributes ['uid' ], localpart )]
201+ filters = [(self .ldap_attributes ['uid' ], uid_value )]
164202
165203 result , conn , response = yield self ._ldap_authenticated_search (
166204 server = server , password = password , filters = filters ,
167205 )
168206
169207 # These results will always return an array
170- givenName = response ["attributes" ].get (
208+ display_name = response ["attributes" ].get (
171209 self .ldap_attributes ["name" ], [localpart ]
172210 )
173- givenName = (
174- givenName [0 ] if len (givenName ) == 1 else localpart
211+ display_name = (
212+ display_name [0 ]
213+ if len (display_name ) == 1
214+ else default_display_name
175215 )
176216
177217 mail = response ["attributes" ].get ("mail" , [None ])
178218 mail = mail [0 ] if len (mail ) == 1 else None
179219 else :
180220 # search disabled, register account with basic information
181- givenName = localpart
221+ display_name = default_display_name
182222 mail = None
183223
184224 # Register the user
185- user_id = yield self .register_user (localpart , givenName , mail )
225+ user_id = yield self .register_user (localpart , display_name , mail )
186226
187227 defer .returnValue (user_id )
188228
@@ -237,6 +277,10 @@ def check_3pid_auth(self, medium, address, password):
237277 response
238278 )
239279
280+ # Close connection
281+ if hasattr (conn , "unbind" ):
282+ yield threads .deferToThread (conn .unbind )
283+
240284 if not result :
241285 defer .returnValue (None )
242286
@@ -245,6 +289,16 @@ def check_3pid_auth(self, medium, address, password):
245289 self .ldap_attributes ["uid" ], [None ]
246290 )
247291 localpart = localpart [0 ] if len (localpart ) == 1 else None
292+ if self .ldap_active_directory and localpart and "@" in localpart :
293+ (login , domain ) = localpart .lower ().rsplit ("@" , 1 )
294+ localpart = login + "/" + domain
295+
296+ if (
297+ self .ldap_default_domain
298+ and domain .lower () == self .ldap_default_domain .lower ()
299+ ):
300+ # Users in default AD domain don't have `/domain` suffix
301+ localpart = login
248302
249303 givenName = response ["attributes" ].get (
250304 self .ldap_attributes ["name" ], [localpart ]
@@ -347,6 +401,10 @@ class _LdapConfig(object):
347401 "mail" ,
348402 ])
349403
404+ ldap_config .active_directory = config .get ("active_directory" , False )
405+ if ldap_config .active_directory :
406+ ldap_config .default_domain = config .get ("default_domain" , None )
407+
350408 return ldap_config
351409
352410 @defer .inlineCallbacks
@@ -523,6 +581,45 @@ def _ldap_authenticated_search(self, server, password, filters):
523581 logger .warning ("Error during LDAP authentication: %s" , e )
524582 raise
525583
584+ def _map_login_to_upn (self , username ):
585+ """Maps user provided login to Active Directory UPN and
586+ local part of Matrix ID.
587+
588+ Args:
589+ username (str): The user's login
590+
591+ Raises:
592+ ActiveDirectoryUPNException: if username can not be
593+ mapped to userPrincipalName
594+
595+ Returns:
596+ Tuple[str, str, str]: a tuple of Active Directory login,
597+ Active Directory domain and local part of Matrix ID.
598+ """
599+ login = username .lower ()
600+ domain = self .ldap_default_domain
601+ localpart = username
602+
603+ if '\\ ' in username :
604+ (domain , login ) = username .lower ().rsplit ('\\ ' , 1 )
605+ elif "/" in username :
606+ (login , domain ) = username .lower ().rsplit ("/" , 1 )
607+ else :
608+ if not self .ldap_default_domain :
609+ logger .info (
610+ 'No LDAP separator "/" was found in uid "%s" '
611+ 'and LDAP default domain was not configured.' ,
612+ username
613+ )
614+ raise ActiveDirectoryUPNException ()
615+
616+ if self .ldap_default_domain and domain == self .ldap_default_domain .lower ():
617+ localpart = login
618+ else :
619+ localpart = login + "/" + domain
620+
621+ return (login , domain , localpart )
622+
526623
527624def _require_keys (config , required ):
528625 missing = [key for key in required if key not in config ]
0 commit comments