| | 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 |