| 1 | # -*- coding: utf-8 -*- |
| 2 | ## |
| 3 | ## LDAP authentication fo Indico |
| 4 | ## |
| 5 | ## written by Martin Kuba makub@ics.muni.cz |
| 6 | # |
| 7 | # This code expects a simple LDAP structure with users on one level like: |
| 8 | # |
| 9 | # dn: uid=john,ou=people,dc=example,dc=com |
| 10 | # objectClass: inetOrgPerson |
| 11 | # uid: john |
| 12 | # cn: John Doe |
| 13 | # mail: john@example.com |
| 14 | # o: Example Inc. |
| 15 | # postalAddress: Example Inc., Some City, Some Country |
| 16 | # |
| 17 | # and groups listing their members by DNs, like: |
| 18 | # |
| 19 | # dn: cn=somegroup,ou=groups,dc=example,dc=com |
| 20 | # objectClass: groupOfNames |
| 21 | # cn: somegroup |
| 22 | # member: uid=john,ou=people,dc=example,dc=com |
| 23 | # member: uid=alice,ou=people,dc=example,dc=com |
| 24 | # member: uid=bob,ou=people,dc=example,dc=com |
| 25 | # description: Just a group of people ... |
| 26 | # |
| 27 | # Adjust it to your needs if your LDAP structure is different. |
| 28 | |
| 29 | |
| 30 | import ldap |
| 31 | import os |
| 32 | import time |
| 33 | import sys |
| 34 | import re |
| 35 | |
| 36 | from MaKaC.common.general import * |
| 37 | from MaKaC.authentication.baseAuthentication import Authenthicator, PIdentity |
| 38 | from MaKaC.errors import MaKaCError |
| 39 | |
| 40 | from MaKaC.common.logger import Logger |
| 41 | from MaKaC.common import Configuration |
| 42 | from MaKaC.common.Configuration import Config |
| 43 | |
| 44 | |
| 45 | class LdapAuthenticator(Authenthicator): |
| 46 | idxName = "LdapIdentities" |
| 47 | id = 'LDAP' |
| 48 | name = 'LDAP' |
| 49 | description = "LDAP Login" |
| 50 | |
| 51 | def __init__(self): |
| 52 | Authenthicator.__init__(self) |
| 53 | self.UserCreator = LdapUserCreator() |
| 54 | |
| 55 | def createIdentity(self, li, avatar): |
| 56 | Logger.get("LdapAuthenticator").info("createIdentity(login="+li.getLogin()+",avatar="+avatar.getName()+" "+avatar.getSurName()+")"); |
| 57 | if LdapChecker().check(li.getLogin(), li.getPassword()): |
| 58 | return LdapIdentity( li.getLogin(), avatar ) |
| 59 | else: |
| 60 | return None |
| 61 | |
| 62 | class LdapIdentity(PIdentity): |
| 63 | def __str__(self): |
| 64 | return '<LdapIdentity{login:'+self.getLogin()+',tag:'+self.getAuthenticatorTag()+'}>' |
| 65 | def authenticate( self, id ): |
| 66 | "id is MaKaC.user.LoginInfo instance, self.user is Avatar" |
| 67 | Logger.get('LdapIdentity').info("authenticate("+id.getLogin()+")") |
| 68 | data = LdapChecker().check(id.getLogin(), id.getPassword()) |
| 69 | if data: |
| 70 | if self.getLogin() == id.getLogin(): |
| 71 | #modify Avatar with the up-to-date info from LDAP |
| 72 | av = self.user |
| 73 | if 'postalAddress' in data: |
| 74 | postalAddress=fromLDAPmultiline(data['postalAddress']) |
| 75 | if av.getAddress()!=postalAddress: |
| 76 | av.setAddress(postalAddress) |
| 77 | Logger.get('LdapIdentity').debug("authenticate("+id.getLogin()+") modified address to "+postalAddress); |
| 78 | if 'cn' in data: |
| 79 | name = data.get('cn'); |
| 80 | firstName = name.split(None,1)[0] |
| 81 | surName = name.split(None,1)[-1] |
| 82 | if av.getName()!=firstName: |
| 83 | av.setName(firstName) |
| 84 | Logger.get('LdapIdentity').debug("authenticate("+id.getLogin()+") modified name to "+firstName) |
| 85 | if av.getSurName()!=surName: |
| 86 | av.setSurName(surName) |
| 87 | Logger.get('LdapIdentity').debug("authenticate("+id.getLogin()+") modified surName to "+surName) |
| 88 | org = None |
| 89 | if 'ou' in data and data.get('ou')!='other' and data.get('ou'): |
| 90 | org = data.get('ou') |
| 91 | elif 'o' in data: |
| 92 | org = data.get('o') |
| 93 | if org!=av.getOrganisation(): |
| 94 | av.setOrganisation(org) |
| 95 | Logger.get('LdapIdentity').debug("authenticate("+id.getLogin()+") modified org to "+org) |
| 96 | if 'mail' in data: |
| 97 | mail = data['mail'] |
| 98 | if mail!= av.getEmail(): |
| 99 | av.setEmail(mail) |
| 100 | Logger.get('LdapIdentity').debug("authenticate("+id.getLogin()+") modified email to "+mail) |
| 101 | return self.user |
| 102 | else: |
| 103 | return None |
| 104 | return None |
| 105 | def getAuthenticatorTag(self): |
| 106 | return LdapAuthenticator.getId() |
| 107 | |
| 108 | def objectAttributes(result_data,attributeNames): |
| 109 | """adds selected attributes""" |
| 110 | object = {'dn':result_data[0][0]} |
| 111 | mapa = result_data[0][1] |
| 112 | for name in attributeNames: |
| 113 | addAttribute(object,mapa,name) |
| 114 | return object |
| 115 | |
| 116 | def addAttribute(object,attrMap,attrName): |
| 117 | """safely adds attribute""" |
| 118 | if attrName in attrMap: |
| 119 | attr = attrMap[attrName] |
| 120 | if len(attr)==1: |
| 121 | object[attrName]=attr[0] |
| 122 | else: |
| 123 | object[attrName]=attr |
| 124 | |
| 125 | # handles communication with the LDAP server specified in indico.conf |
| 126 | # |
| 127 | # defualt values as specified in Configuration.py are |
| 128 | # LDAPhost="ldap.example.com" |
| 129 | # LDAPpeopleDN="ou=people,dc=example,dc=com" |
| 130 | # LDAPgroupsDN="ou=groups,dc=example,dc=com" |
| 131 | # |
| 132 | # the code expects the users to be (in the LDAP) objects of type inetOrgPerson |
| 133 | # identified by thier "uid" attribute, and the groups to be objects of type groupOfNames |
| 134 | # with the "member" multivalued attribute containing complete DNs of users |
| 135 | # which seems to be the standard LDAP setup |
| 136 | # |
| 137 | class LdapLDAP: |
| 138 | l = None |
| 139 | def __init__(self): |
| 140 | conf = Configuration.Config.getInstance() |
| 141 | self.ldapHost = conf.getLDAPhost() |
| 142 | self.ldapPeopleDN = conf.getLDAPpeopleDN() |
| 143 | self.ldapGroupsDN = conf.getLDAPgroupsDN() |
| 144 | # opens an anonymous LDAP connection |
| 145 | def open(self): |
| 146 | self.l = ldap.open(self.ldapHost) |
| 147 | self.l.protocol_version = ldap.VERSION3 |
| 148 | return self.l |
| 149 | # verifies username and password by binding to the LDAP server |
| 150 | def openAsUser(self,userName,password): |
| 151 | self.open() |
| 152 | self.l.simple_bind_s("uid="+userName+","+self.ldapPeopleDN, password) |
| 153 | # closes LDAP connection |
| 154 | def close(self): |
| 155 | self.l.unbind_s() |
| 156 | l = None |
| 157 | # finds a user in LDAP |
| 158 | # returns a map containing dn,cn,uid,mail,o,ou and postalAddress as keys and strings as values |
| 159 | # returns None if a user is not found |
| 160 | def lookupUser(self,uid): |
| 161 | ldap_result_id = self.l.search("uid="+uid+","+self.ldapPeopleDN, ldap.SCOPE_BASE, "objectClass=inetOrgPerson") |
| 162 | while 1: |
| 163 | result_type, result_data = self.l.result(ldap_result_id, 0) |
| 164 | if (result_data == []): |
| 165 | break |
| 166 | else: |
| 167 | if result_type == ldap.RES_SEARCH_ENTRY: |
| 168 | return objectAttributes(result_data,['uid','cn','mail','o','ou','postalAddress']) |
| 169 | return None |
| 170 | # finds users according to a specified filter |
| 171 | def findUsers(self,filter): |
| 172 | dict = {} |
| 173 | ldap_result_id = self.l.search(self.ldapPeopleDN, ldap.SCOPE_SUBTREE, filter) |
| 174 | while 1: |
| 175 | result_type, result_data = self.l.result(ldap_result_id, 0) |
| 176 | if (result_data == []): |
| 177 | break |
| 178 | else: |
| 179 | if result_type == ldap.RES_SEARCH_ENTRY: |
| 180 | ret = objectAttributes(result_data,['uid','cn','mail','o','ou','postalAddress']) |
| 181 | av= dictToAv(ret) |
| 182 | dict[ret['mail']] = av |
| 183 | return dict |
| 184 | |
| 185 | # finds a group in LDAP |
| 186 | # returns an array of groups matching the group name, each group is represented |
| 187 | # by a map with keys cn and description |
| 188 | def findGroups(self,name,exact): |
| 189 | if exact==0: |
| 190 | star='*' |
| 191 | else: |
| 192 | star='' |
| 193 | name = name.strip() |
| 194 | if len(name)>0: |
| 195 | filter='(&(objectClass=groupOfNames)(cn='+star+name+star+'))' |
| 196 | else: |
| 197 | return [] |
| 198 | ldap_result_id = self.l.search(self.ldapGroupsDN, ldap.SCOPE_SUBTREE, filter) |
| 199 | groupDicts = [] |
| 200 | while 1: |
| 201 | result_type, result_data = self.l.result(ldap_result_id, 0) |
| 202 | if (result_data == []): |
| 203 | break |
| 204 | else: |
| 205 | if result_type == ldap.RES_SEARCH_ENTRY: |
| 206 | groupDicts.append( objectAttributes(result_data,['cn','description'])) |
| 207 | return groupDicts |
| 208 | # finds uids of users referenced by the member attribute of the group LDAP object |
| 209 | def findGroupMemberUids(self,name): |
| 210 | ldap_result_id = self.l.search("cn="+name+","+self.ldapGroupsDN, ldap.SCOPE_BASE, "objectClass=groupOfNames") |
| 211 | members = None |
| 212 | while 1: |
| 213 | result_type, result_data = self.l.result(ldap_result_id, 0) |
| 214 | if (result_data == []): |
| 215 | break |
| 216 | else: |
| 217 | if result_type == ldap.RES_SEARCH_ENTRY: |
| 218 | members = result_data[0][1]['member'] |
| 219 | if not members: |
| 220 | return [] |
| 221 | memberUids = [] |
| 222 | for memberDN in members: |
| 223 | m = re.search('uid=([^,]*),',memberDN) |
| 224 | if m: |
| 225 | uid = m.group(1) |
| 226 | memberUids.append( uid ) |
| 227 | return memberUids |
| 228 | |
| 229 | class LdapChecker: |
| 230 | def check(self, userName, password): |
| 231 | try: |
| 232 | ret = {} |
| 233 | ldapLdap = LdapLDAP() |
| 234 | ldapLdap.openAsUser(userName,password) |
| 235 | ret = ldapLdap.lookupUser(userName) |
| 236 | ldapLdap.close() |
| 237 | Logger.get('LdapChecker').debug("Username: %s checked: %s"%(userName,ret)) |
| 238 | return ret |
| 239 | except ldap.INVALID_CREDENTIALS, e: |
| 240 | Logger.get('LdapChecker').warn("Username: %s - invalid credentials"%userName) |
| 241 | return None |
| 242 | |
| 243 | #conversion for inetOrgPerson.postalAddress attribute that can contain newlines encoded following the RFC 2252 |
| 244 | def fromLDAPmultiline(s): |
| 245 | if s: |
| 246 | return s.replace('$',"\r\n").replace('\\24','$').replace('\\5c','\\') |
| 247 | else: |
| 248 | return s |
| 249 | |
| 250 | class LdapUserCreator: |
| 251 | def create(self, li): |
| 252 | Logger.get('LdapUserCreator').info("create("+li.getLogin()+",password=****)") |
| 253 | # first, check if authentication is OK |
| 254 | data = LdapChecker().check(li.getLogin(), li.getPassword()) |
| 255 | if not data: |
| 256 | return None |
| 257 | |
| 258 | # Search if user already exist, using email address |
| 259 | import MaKaC.user as user |
| 260 | ah = user.AvatarHolder() |
| 261 | userList = ah.match({"email":data["mail"]}, forceWithoutExtAuth=True) |
| 262 | if len(userList) == 0: |
| 263 | # User doesn't exist, create it |
| 264 | try: |
| 265 | av = user.Avatar() |
| 266 | name = data.get('cn'); |
| 267 | av.setName(name.split()[0]) |
| 268 | av.setSurName(name.split()[-1]) |
| 269 | av.setOrganisation(data.get('o', "")) |
| 270 | av.setEmail(data['mail']) |
| 271 | if 'postalAddress' in data: |
| 272 | av.setAddress(fromLDAPmultiline(data.get('postalAddress'))) |
| 273 | #av.setTelephone(data.get('telephonenumber',"")) |
| 274 | ah.add(av) |
| 275 | av.activateAccount() |
| 276 | except KeyError, e: |
| 277 | raise MaKaCError("LDAP account does not contain the mandatory data to create \ |
| 278 | an Indico account. You can create an Indico \ |
| 279 | account manually in order to use your LDAP login"%(urlHandlers.UHUserRegistration.getURL())) |
| 280 | else: |
| 281 | # user founded |
| 282 | av = userList[0] |
| 283 | #now create the nice identity for the user |
| 284 | na = LdapAuthenticator() |
| 285 | id = na.createIdentity( li, av) |
| 286 | na.add(id) |
| 287 | return av |
| 288 | |
| 289 | |
| 290 | # for MaKaC.externUsers |
| 291 | def dictToAv(ret): |
| 292 | av= {} |
| 293 | av["email"] = [ret['mail']] |
| 294 | av["name"]= ret['cn'].split()[0] |
| 295 | av["surName"]= ret['cn'].split()[-1] |
| 296 | if 'o' in ret: |
| 297 | av["organisation"] = [ret['o']] |
| 298 | else: |
| 299 | av["organisation"] = [''] |
| 300 | if 'postalAddress' in ret: |
| 301 | av['address'] = [ fromLDAPmultiline(ret['postalAddress']) ] |
| 302 | av["id"] = 'LDAP:'+ret['uid'] |
| 303 | av["status"] = "NotCreated" |
| 304 | av["login"] = ret['uid'] |
| 305 | return av |
| 306 | |
| 307 | def is_empty(dict,key): |
| 308 | if key not in dict: |
| 309 | return False |
| 310 | if dict[key]: |
| 311 | return True |
| 312 | else: |
| 313 | return False |
| 314 | |
| 315 | class LdapUser: |
| 316 | |
| 317 | def match(self, criteria, exact=0): |
| 318 | Logger.get('LdapUser').debug("match(criteria"+str(criteria)+",exact="+str(exact)+")") |
| 319 | allEmpty=True |
| 320 | filter='objectClass=inetOrgPerson' |
| 321 | if exact==0: |
| 322 | star='*' |
| 323 | else: |
| 324 | star='' |
| 325 | for k, v in criteria.items(): |
| 326 | if k=='email' and len(v.strip())>0: |
| 327 | filter='&('+filter+')(mail='+star+v+star+')' |
| 328 | allEmpty=False |
| 329 | elif k=='name' and len(v.strip())>0: |
| 330 | filter='&('+filter+')(cn='+star+v+star+')' |
| 331 | allEmpty=False |
| 332 | elif k=='surName' and len(v.strip())>0: |
| 333 | filter='&('+filter+')(sn='+star+v+star+')' |
| 334 | allEmpty=False |
| 335 | elif k=='organisation' and len(v.strip())>0: |
| 336 | filter='&('+filter+')(|(o='+star+v+star+')(ou='+star+v+star+'))' |
| 337 | allEmpty=False |
| 338 | elif k=='login' and len(v.strip())>0: |
| 339 | filter='&('+filter+')(uid='+star+v+star+')' |
| 340 | allEmpty=False |
| 341 | if allEmpty: |
| 342 | return {} |
| 343 | filter='('+filter+')' |
| 344 | ldapLdap = LdapLDAP() |
| 345 | ldapLdap.open() |
| 346 | dict = ldapLdap.findUsers(filter) |
| 347 | ldapLdap.close() |
| 348 | return dict |
| 349 | |
| 350 | def getById(self, id): |
| 351 | Logger.get('LdapUser').debug("getById('"+str(id)+"')") |
| 352 | ldapLdap = LdapLDAP() |
| 353 | l = ldapLdap.open() |
| 354 | ret = ldapLdap.lookupUser(id) |
| 355 | ldapLdap.close() |
| 356 | if(ret==None): |
| 357 | return None |
| 358 | av = dictToAv(ret) |
| 359 | av["id"] = id |
| 360 | av["identity"] = LdapIdentity |
| 361 | av["authenticator"] = LdapAuthenticator() |
| 362 | return av |
| 363 | |
| 364 | def ldapFindGroups(name,exact): |
| 365 | ldapLdap = LdapLDAP() |
| 366 | ldapLdap.open() |
| 367 | ret = ldapLdap.findGroups(name,exact) |
| 368 | ldapLdap.close() |
| 369 | return ret |
| 370 | |
| 371 | def ldapFindGroupMemberUids(name): |
| 372 | ldapLdap = LdapLDAP() |
| 373 | ldapLdap.open() |
| 374 | ret = ldapLdap.findGroupMemberUids(name) |
| 375 | ldapLdap.close() |
| 376 | return ret |