4fea094dc2593ac7980adcb252f5cbcdb3ea0ec7
[sfa.git] / sfa / iotlab / LDAPapi.py
1 """
2 This API is adapted for OpenLDAP. The file contains all LDAP classes and methods
3 needed to:
4 - Load the LDAP connection configuration file (login, address..) with LdapConfig
5 - Connect to LDAP with ldap_co
6 - Create a unique LDAP login and password for a user based on his email or last
7 name and first name with LoginPassword.
8 -  Manage entries in LDAP using SFA records with LDAPapi (Search, Add, Delete,
9 Modify)
10
11 """
12 import random
13 from passlib.hash import ldap_salted_sha1 as lssha
14
15 from sfa.util.xrn import get_authority
16 from sfa.util.sfalogging import logger
17 from sfa.util.config import Config
18
19 import ldap
20 import ldap.modlist as modlist
21
22 import os.path
23
24
25 class LdapConfig():
26     """
27     Ldap configuration class loads the configuration file and sets the
28     ldap IP address, password, people dn, web dn, group dn. All these settings
29     were defined in a separate file  ldap_config.py to avoid sharing them in
30     the SFA git as it contains sensible information.
31
32     """
33     def __init__(self, config_file='/etc/sfa/ldap_config.py'):
34         """Loads configuration from file /etc/sfa/ldap_config.py and set the
35         parameters for connection to LDAP.
36
37         """
38
39         try:
40             execfile(config_file, self.__dict__)
41
42             self.config_file = config_file
43             # path to configuration data
44             self.config_path = os.path.dirname(config_file)
45         except IOError:
46             raise IOError, "Could not find or load the configuration file: %s" \
47                 % config_file
48
49
50 class ldap_co:
51     """ Set admin login and server configuration variables."""
52
53     def __init__(self):
54         """Fetch LdapConfig attributes (Ldap server connection parameters and
55         defines port , version and subtree scope.
56
57         """
58         #Iotlab PROD LDAP parameters
59         self.ldapserv = None
60         ldap_config = LdapConfig()
61         self.config = ldap_config
62         self.ldapHost = ldap_config.LDAP_IP_ADDRESS
63         self.ldapPeopleDN = ldap_config.LDAP_PEOPLE_DN
64         self.ldapGroupDN = ldap_config.LDAP_GROUP_DN
65         self.ldapAdminDN = ldap_config.LDAP_WEB_DN
66         self.ldapAdminPassword = ldap_config.LDAP_WEB_PASSWORD
67         self.ldapPort = ldap.PORT
68         self.ldapVersion = ldap.VERSION3
69         self.ldapSearchScope = ldap.SCOPE_SUBTREE
70
71     def connect(self, bind=True):
72         """Enables connection to the LDAP server.
73
74         :param bind: Set the bind parameter to True if a bind is needed
75             (for add/modify/delete operations). Set to False otherwise.
76         :type bind: boolean
77         :returns: dictionary with status of the connection. True if Successful,
78             False if not and in this case the error
79             message( {'bool', 'message'} ).
80         :rtype: dict
81
82         """
83         try:
84             self.ldapserv = ldap.open(self.ldapHost)
85         except ldap.LDAPError, error:
86             return {'bool': False, 'message': error}
87
88         # Bind with authentification
89         if(bind):
90             return self.bind()
91
92         else:
93             return {'bool': True}
94
95     def bind(self):
96         """ Binding method.
97
98         :returns: dictionary with the bind status. True if Successful,
99             False if not and in this case the error message({'bool','message'})
100         :rtype: dict
101
102         """
103         try:
104             # Opens a connection after a call to ldap.open in connect:
105             self.ldapserv = ldap.initialize("ldap://" + self.ldapHost)
106
107             # Bind/authenticate with a user with apropriate
108             #rights to add objects
109             self.ldapserv.simple_bind_s(self.ldapAdminDN,
110                                         self.ldapAdminPassword)
111
112         except ldap.LDAPError, error:
113             return {'bool': False, 'message': error}
114
115         return {'bool': True}
116
117     def close(self):
118         """Close the LDAP connection.
119
120         Can throw an exception if the unbinding fails.
121
122         :returns: dictionary with the bind status if the unbinding failed and
123             in this case the dict contains an error message. The dictionary keys
124             are : ({'bool','message'})
125         :rtype: dict or None
126
127         """
128         try:
129             self.ldapserv.unbind_s()
130         except ldap.LDAPError, error:
131             return {'bool': False, 'message': error}
132
133
134 class LoginPassword():
135     """
136
137     Class to handle login and password generation, using custom login generation
138     algorithm.
139
140     """
141     def __init__(self):
142         """
143
144         Sets password  and login maximum length, and defines the characters that
145         can be found in a random generated password.
146
147         """
148         self.login_max_length = 8
149         self.length_password = 8
150         self.chars_password = ['!', '$', '(',')', '*', '+', ',', '-', '.',
151                                '0', '1', '2', '3', '4', '5', '6', '7', '8',
152                                '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
153                                'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q',
154                                'R', 'S', 'T',  'U', 'V', 'W', 'X', 'Y', 'Z',
155                                '_', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
156                                'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',
157                                'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
158                                '\'']
159
160     @staticmethod
161     def clean_user_names(record):
162         """
163
164         Removes special characters such as '-', '_' , '[', ']' and ' ' from the
165         first name and last name.
166
167         :param record: user's record
168         :type record: dict
169         :returns: lower_first_name and lower_last_name if they were found
170             in the user's record. Return None, none otherwise.
171         :rtype: string, string or None, None.
172
173         """
174         if 'first_name' in record and 'last_name' in record:
175             #Remove all special characters from first_name/last name
176             lower_first_name = record['first_name'].replace('-', '')\
177                 .replace('_', '').replace('[', '')\
178                 .replace(']', '').replace(' ', '')\
179                 .lower()
180             lower_last_name = record['last_name'].replace('-', '')\
181                 .replace('_', '').replace('[', '')\
182                 .replace(']', '').replace(' ', '')\
183                 .lower()
184             return lower_first_name, lower_last_name
185         else:
186             return None, None
187
188     @staticmethod
189     def extract_name_from_email(record):
190         """
191
192         When there is no valid first name and last name in the record,
193         the email is used to generate the login. Here, we assume the email
194         is firstname.lastname@something.smthg. The first name and last names
195         are extracted from the email, special charcaters are removed and
196         they are changed into lower case.
197
198         :param record: user's data
199         :type record: dict
200         :returns: the first name and last name taken from the user's email.
201             lower_first_name, lower_last_name.
202         :rtype: string, string
203
204         """
205
206         email = record['email']
207         email = email.split('@')[0].lower()
208         lower_first_name = None
209         lower_last_name = None
210         #Assume there is first name and last name in email
211         #if there is a  separator
212         separator_list = ['.', '_', '-']
213         for sep in separator_list:
214             if sep in email:
215                 mail = email.split(sep)
216                 lower_first_name = mail[0]
217                 lower_last_name = mail[1]
218                 break
219
220         #Otherwise just take the part before the @ as the
221         #lower_first_name  and lower_last_name
222         if lower_first_name is None:
223             lower_first_name = email
224             lower_last_name = email
225
226         return lower_first_name, lower_last_name
227
228     def get_user_firstname_lastname(self, record):
229         """
230
231         Get the user first name and last name from the information we have in
232         the record.
233
234         :param record: user's information
235         :type record: dict
236         :returns: the user's first name and last name.
237
238         .. seealso:: clean_user_names
239         .. seealso:: extract_name_from_email
240
241         """
242         lower_first_name, lower_last_name = self.clean_user_names(record)
243
244         #No first name and last name  check  email
245         if lower_first_name is None and lower_last_name is None:
246
247             lower_first_name, lower_last_name = \
248                 self.extract_name_from_email(record)
249
250         return lower_first_name, lower_last_name
251
252     # XXX JORDAN: This function writes an error in the log but returns normally :))
253     def choose_sets_chars_for_login(self, lower_first_name, lower_last_name):
254         """
255
256         Algorithm to select sets of characters from the first name and last
257         name, depending on the lenght of the last name and the maximum login
258         length which in our case is set to 8 characters.
259
260         :param lower_first_name: user's first name in lower case.
261         :param lower_last_name: usr's last name in lower case.
262         :returns: user's login
263         :rtype: string
264
265         """
266         length_last_name = len(lower_last_name)
267         self.login_max_length = 8
268
269         #Try generating a unique login based on first name and last name
270
271         if length_last_name >= self.login_max_length:
272             login = lower_last_name[0:self.login_max_length]
273             index = 0
274             logger.debug("login : %s index : %s" % (login, index))
275         elif length_last_name >= 4:
276             login = lower_last_name
277             index = 0
278             logger.debug("login : %s index : %s" % (login, index))
279         elif length_last_name == 3:
280             login = lower_first_name[0:1] + lower_last_name
281             index = 1
282             logger.debug("login : %s index : %s" % (login, index))
283         elif length_last_name == 2:
284             if len(lower_first_name) >= 2:
285                 login = lower_first_name[0:2] + lower_last_name
286                 index = 2
287                 logger.debug("login : %s index : %s" % (login, index))
288             else:
289                 logger.error("LoginException : \
290                             Generation login error with \
291                             minimum four characters")
292
293         else:
294             logger.error("LDAP LdapGenerateUniqueLogin failed : \
295                         impossible to generate unique login for %s %s"
296                          % (lower_first_name, lower_last_name))
297         logger.debug("JORDAN choose_sets_chars_for_login %d %s" % (index, login))
298         return index, login
299
300     def generate_password(self):
301         """
302
303         Generate a password upon  adding a new user in LDAP Directory
304         (8 characters length). The generated password is composed of characters
305         from the chars_password list.
306
307         :returns: the randomly generated password
308         :rtype: string
309
310         """
311         password = str()
312
313         length = len(self.chars_password)
314         for index in range(self.length_password):
315             char_index = random.randint(0, length - 1)
316             password += self.chars_password[char_index]
317
318         return password
319
320     @staticmethod
321     def encrypt_password(password):
322         """
323
324         Use passlib library to make a RFC2307 LDAP encrypted password salt size
325         is 8, use sha-1 algorithm.
326
327         :param password:  password not encrypted.
328         :type password: string
329         :returns: Returns encrypted password.
330         :rtype: string
331
332         """
333         #Keep consistency with Java Iotlab's LDAP API
334         #RFC2307SSHAPasswordEncryptor so set the salt size to 8 bytes
335         return lssha.encrypt(password, salt_size=8)
336
337
338 class LDAPapi:
339     """Defines functions to insert and search entries in the LDAP.
340
341     .. note:: class supposes the unix schema is used
342
343     """
344     def __init__(self):
345         logger.setLevelDebug()
346
347         #SFA related config
348
349         config = Config()
350         self.login_pwd = LoginPassword()
351         self.authname = config.SFA_REGISTRY_ROOT_AUTH
352         self.conn =  ldap_co()
353         self.ldapUserQuotaNFS = self.conn.config.LDAP_USER_QUOTA_NFS
354         self.ldapUserUidNumberMin = self.conn.config.LDAP_USER_UID_NUMBER_MIN
355         self.ldapUserGidNumber = self.conn.config.LDAP_USER_GID_NUMBER
356         self.ldapUserHomePath = self.conn.config.LDAP_USER_HOME_PATH
357         self.baseDN = self.conn.ldapPeopleDN
358         self.ldapShell = '/bin/bash'
359
360
361     def LdapGenerateUniqueLogin(self, record):
362         """
363
364         Generate login for adding a new user in LDAP Directory
365         (four characters minimum length). Get proper last name and
366         first name so that the user's login can be generated.
367
368         :param record: Record must contain first_name and last_name.
369         :type record: dict
370         :returns: the generated login for the user described with record if the
371             login generation is successful, None if it fails.
372         :rtype: string or None
373
374         """
375         #For compatibility with other ldap func
376         if 'mail' in record and 'email' not in record:
377             record['email'] = record['mail']
378
379         lower_first_name, lower_last_name =  \
380             self.login_pwd.get_user_firstname_lastname(record)
381
382         index, login = self.login_pwd.choose_sets_chars_for_login(
383             lower_first_name, lower_last_name)
384
385         login_filter = '(uid=' + login + ')'
386         get_attrs = ['uid']
387         try:
388             #Check if login already in use
389
390             while (len(self.LdapSearch(login_filter, get_attrs)) is not 0):
391
392                 index += 1
393                 if index >= 9:
394                     logger.error("LoginException : Generation login error \
395                                     with minimum four characters")
396                     break
397                 else:
398                     try:
399                         login = \
400                             lower_first_name[0:index] + \
401                             lower_last_name[0:
402                                             self.login_pwd.login_max_length
403                                             - index]
404                         logger.debug("JORDAN trying login: %r" % login)
405                         login_filter = '(uid=' + login + ')'
406                     except KeyError:
407                         print "lower_first_name - lower_last_name too short"
408
409             logger.debug("LDAP.API \t LdapGenerateUniqueLogin login %s"
410                          % (login))
411             return login
412
413         except ldap.LDAPError, error:
414             logger.log_exc("LDAP LdapGenerateUniqueLogin Error %s" % (error))
415             return None
416
417     def find_max_uidNumber(self):
418         """Find the LDAP max uidNumber (POSIX uid attribute).
419
420         Used when adding a new user in LDAP Directory
421
422         :returns: max uidNumber + 1
423         :rtype: string
424
425         """
426         #First, get all the users in the LDAP
427         get_attrs = "(uidNumber=*)"
428         login_filter = ['uidNumber']
429
430         result_data = self.LdapSearch(get_attrs, login_filter)
431         #It there is no user in LDAP yet, First LDAP user
432         if result_data == []:
433             max_uidnumber = self.ldapUserUidNumberMin
434         #Otherwise, get the highest uidNumber
435         else:
436             uidNumberList = [int(r[1]['uidNumber'][0])for r in result_data]
437             logger.debug("LDAPapi.py \tfind_max_uidNumber  \
438                             uidNumberList %s " % (uidNumberList))
439             max_uidnumber = max(uidNumberList) + 1
440
441         return str(max_uidnumber)
442
443
444     def get_ssh_pkey(self, record):
445         """TODO ; Get ssh public key from sfa record
446         To be filled by N. Turro ? or using GID pl way?
447
448         """
449         return 'A REMPLIR '
450
451     @staticmethod
452     #TODO Handle OR filtering in the ldap query when
453     #dealing with a list of records instead of doing a for loop in GetPersons
454     def make_ldap_filters_from_record(record=None):
455         """Helper function to make LDAP filter requests out of SFA records.
456
457         :param record: user's sfa record. Should contain first_name,last_name,
458             email or mail, and if the record is enabled or not. If the dict
459             record does not have all of these, must at least contain the user's
460             email.
461         :type record: dict
462         :returns: LDAP request
463         :rtype: string
464
465         """
466         logger.debug("JORDAN make_ldap_filters_from_record: %r" % record)
467         req_ldap = ''
468         req_ldapdict = {}
469         if record :
470             if 'first_name' in record and 'last_name' in record:
471                 if record['first_name'] != record['last_name']:
472                     req_ldapdict['cn'] = str(record['first_name'])+" "\
473                         + str(record['last_name'])
474             if 'uid' in record:
475                 req_ldapdict['uid'] = record['uid']
476             if 'email' in record:
477                 req_ldapdict['mail'] = record['email']
478             if 'mail' in record:
479                 req_ldapdict['mail'] = record['mail']
480             if 'enabled' in record:
481                 if record['enabled'] is True:
482                     req_ldapdict['shadowExpire'] = '-1'
483                 else:
484                     req_ldapdict['shadowExpire'] = '0'
485
486             #Hrn should not be part of the filter because the hrn
487             #presented by a certificate of a SFA user not imported in
488             #Iotlab  does not include the iotlab login in it
489             #Plus, the SFA user may already have an account with iotlab
490             #using another login.
491
492             logger.debug("\r\n \t LDAP.PY make_ldap_filters_from_record \
493                                 record %s req_ldapdict %s"
494                          % (record, req_ldapdict))
495
496             for k in req_ldapdict:
497                 req_ldap += '(' + str(k) + '=' + str(req_ldapdict[k]) + ')'
498             if len(req_ldapdict.keys()) >1 :
499                 req_ldap = req_ldap[:0]+"(&"+req_ldap[0:]
500                 size = len(req_ldap)
501                 req_ldap = req_ldap[:(size-1)] + ')' + req_ldap[(size-1):]
502         else:
503             req_ldap = "(cn=*)"
504
505         return req_ldap
506
507     def make_ldap_attributes_from_record(self, record):
508         """
509
510         When adding a new user to Iotlab's LDAP, creates an attributes
511         dictionnary from the SFA record understandable by LDAP. Generates the
512         user's LDAP login.User is automatically validated (account enabled)
513         and described as a SFA USER FROM OUTSIDE IOTLAB.
514
515         :param record: must contain the following keys and values:
516             first_name, last_name, mail, pkey (ssh key).
517         :type record: dict
518         :returns: dictionary of attributes using LDAP data structure model.
519         :rtype: dict
520
521         """
522         logger.debug("JORDAN make_ldap_attributes_from_record:  %r" % record)
523
524         attrs = {}
525         attrs['objectClass'] = ["top", "person", "inetOrgPerson",
526                                 "organizationalPerson", "posixAccount",
527                                 "shadowAccount", "systemQuotas",
528                                 "ldapPublicKey"]
529
530         attrs['uid'] = self.LdapGenerateUniqueLogin(record)
531         try:
532             attrs['givenName'] = str(record['first_name']).lower().capitalize()
533             attrs['sn'] = str(record['last_name']).lower().capitalize()
534             attrs['cn'] = attrs['givenName'] + ' ' + attrs['sn']
535             attrs['gecos'] = attrs['givenName'] + ' ' + attrs['sn']
536
537         except KeyError:
538             attrs['givenName'] = attrs['uid']
539             attrs['sn'] = attrs['uid']
540             attrs['cn'] = attrs['uid']
541             attrs['gecos'] = attrs['uid']
542
543         attrs['quota'] = self.ldapUserQuotaNFS
544         attrs['homeDirectory'] = self.ldapUserHomePath + attrs['uid']
545         attrs['loginShell'] = self.ldapShell
546         attrs['gidNumber'] = self.ldapUserGidNumber
547         attrs['uidNumber'] = self.find_max_uidNumber()
548         attrs['mail'] = record['mail'].lower()
549         try:
550             attrs['sshPublicKey'] = record['pkey']
551         except KeyError:
552             attrs['sshPublicKey'] = self.get_ssh_pkey(record)
553
554
555         #Password is automatically generated because SFA user don't go
556         #through the Iotlab website  used to register new users,
557         #There is no place in SFA where users can enter such information
558         #yet.
559         #If the user wants to set his own password , he must go to the Iotlab
560         #website.
561         password = self.login_pwd.generate_password()
562         attrs['userPassword'] = self.login_pwd.encrypt_password(password)
563
564         #Account automatically validated (no mail request to admins)
565         #Set to 0 to disable the account, -1 to enable it,
566         attrs['shadowExpire'] = '-1'
567
568         #Motivation field in Iotlab
569         attrs['description'] = 'SFA USER FROM OUTSIDE SENSLAB'
570
571         attrs['ou'] = 'SFA'         #Optional: organizational unit
572         #No info about those here:
573         attrs['l'] = 'To be defined'#Optional: Locality.
574         attrs['st'] = 'To be defined' #Optional: state or province (country).
575
576         return attrs
577
578
579     def LdapAddUser(self, record) :
580         """Add SFA user to LDAP if it is not in LDAP  yet.
581
582         :param record: dictionnary with the user's data.
583         :returns: a dictionary with the status (Fail= False, Success= True)
584             and the uid of the newly added user if successful, or the error
585             message it is not. Dict has keys bool and message in case of
586             failure, and bool uid in case of success.
587         :rtype: dict
588
589         .. seealso:: make_ldap_filters_from_record
590
591         """
592         filter_by = self.make_ldap_filters_from_record({'email' : record['email']})
593         user = self.LdapSearch(filter_by)
594         if user:
595             logger.debug("LDAPapi.py user ldap exist \t%s" % user)
596             # user = [('uid=saint,ou=People,dc=senslab,dc=info', {'uid': ['saint'], 'givenName': ['Fred'], ...})]
597             return {'bool': True, 'uid': user[0][1]['uid'][0]}
598         else:
599             self.conn.connect()
600             user_ldap_attrs = self.make_ldap_attributes_from_record(record)
601             logger.debug("LDAPapi.py user ldap doesn't exist \t%s" % user_ldap_attrs)
602             # The dn of our new entry/object
603             dn = 'uid=' + user_ldap_attrs['uid'] + "," + self.baseDN
604             try:
605                 ldif = modlist.addModlist(user_ldap_attrs)
606                 self.conn.ldapserv.add_s(dn, ldif)
607             except ldap.LDAPError, error:
608                 logger.log_exc("LDAP Add Error %s" % error)
609                 return {'bool': False, 'message': error}
610             self.conn.close()
611             return {'bool': True, 'uid': user_ldap_attrs['uid']}
612         
613         
614     def LdapDelete(self, person_dn):
615         """Deletes a person in LDAP. Uses the dn of the user.
616
617         :param person_dn: user's ldap dn.
618         :type person_dn: string
619         :returns: dictionary with bool True if successful, bool False
620             and the error if not.
621         :rtype: dict
622
623         """
624         #Connect and bind
625         result =  self.conn.connect()
626         if(result['bool']):
627             try:
628                 self.conn.ldapserv.delete_s(person_dn)
629                 self.conn.close()
630                 return {'bool': True}
631
632             except ldap.LDAPError, error:
633                 logger.log_exc("LDAP Delete Error %s" % error)
634                 return {'bool': False, 'message': error}
635
636     def LdapDeleteUser(self, record_filter):
637         """Deletes a SFA person in LDAP, based on the user's hrn.
638
639         :param record_filter: Filter to find the user to be deleted. Must
640             contain at least the user's email.
641         :type record_filter: dict
642         :returns: dict with bool True if successful, bool False and error
643             message otherwise.
644         :rtype: dict
645
646         .. seealso:: LdapFindUser docstring for more info on record filter.
647         .. seealso:: LdapDelete for user deletion
648
649         """
650         #Find uid of the  person
651         person = self.LdapFindUser(record_filter, [])
652         logger.debug("LDAPapi.py \t LdapDeleteUser record %s person %s"
653                      % (record_filter, person))
654
655         if person:
656             dn = 'uid=' + person['uid'] + "," + self.baseDN
657         else:
658             return {'bool': False}
659
660         result = self.LdapDelete(dn)
661         return result
662
663     def LdapModify(self, dn, old_attributes_dict, new_attributes_dict):
664         """ Modifies a LDAP entry, replaces user's old attributes with
665         the new ones given.
666
667         :param dn: user's absolute name  in the LDAP hierarchy.
668         :param old_attributes_dict: old user's attributes. Keys must match
669             the ones used in the LDAP model.
670         :param new_attributes_dict: new user's attributes. Keys must match
671             the ones used in the LDAP model.
672         :type dn: string
673         :type old_attributes_dict: dict
674         :type new_attributes_dict: dict
675         :returns: dict bool True if Successful, bool False if not.
676         :rtype: dict
677
678         """
679
680         ldif = modlist.modifyModlist(old_attributes_dict, new_attributes_dict)
681         # Connect and bind/authenticate
682         result = self.conn.connect()
683         if (result['bool']):
684             try:
685                 self.conn.ldapserv.modify_s(dn, ldif)
686                 self.conn.close()
687                 return {'bool': True}
688             except ldap.LDAPError, error:
689                 logger.log_exc("LDAP LdapModify Error %s" % error)
690                 return {'bool': False}
691
692
693     def LdapModifyUser(self, user_record, new_attributes_dict):
694         """
695
696         Gets the record from one user based on the user sfa recordand changes
697         the attributes according to the specified new_attributes. Do not use
698         this if we need to modify the uid. Use a ModRDN operation instead
699         ( modify relative DN ).
700
701         :param user_record: sfa user record.
702         :param new_attributes_dict: new user attributes, keys must be the
703             same as the LDAP model.
704         :type user_record: dict
705         :type new_attributes_dict: dict
706         :returns: bool True if successful, bool False if not.
707         :rtype: dict
708
709         .. seealso:: make_ldap_filters_from_record for info on what is mandatory
710             in the user_record.
711         .. seealso:: make_ldap_attributes_from_record for the LDAP objectclass.
712
713         """
714         if user_record is None:
715             logger.error("LDAP \t LdapModifyUser Need user record  ")
716             return {'bool': False}
717
718         #Get all the attributes of the user_uid_login
719         #person = self.LdapFindUser(record_filter,[])
720         req_ldap = self.make_ldap_filters_from_record(user_record)
721         person_list = self.LdapSearch(req_ldap, [])
722         logger.debug("LDAPapi.py \t LdapModifyUser person_list : %s"
723                      % (person_list))
724
725         if person_list and len(person_list) > 1:
726             logger.error("LDAP \t LdapModifyUser Too many users returned")
727             return {'bool': False}
728         if person_list is None:
729             logger.error("LDAP \t LdapModifyUser  User %s doesn't exist "
730                          % (user_record))
731             return {'bool': False}
732
733         # The dn of our existing entry/object
734         #One result only from ldapSearch
735         person = person_list[0][1]
736         dn = 'uid=' + person['uid'][0] + "," + self.baseDN
737
738         if new_attributes_dict:
739             old = {}
740             for k in new_attributes_dict:
741                 if k not in person:
742                     old[k] = ''
743                 else:
744                     old[k] = person[k]
745             logger.debug(" LDAPapi.py \t LdapModifyUser  new_attributes %s"
746                          % (new_attributes_dict))
747             result = self.LdapModify(dn, old, new_attributes_dict)
748             return result
749         else:
750             logger.error("LDAP \t LdapModifyUser  No new attributes given. ")
751             return {'bool': False}
752
753
754     def LdapMarkUserAsDeleted(self, record):
755         """
756
757         Sets shadowExpire to 0, disabling the user in LDAP. Calls LdapModifyUser
758         to change the shadowExpire of the user.
759
760         :param record: the record of the user who has to be disabled.
761             Should contain first_name,last_name, email or mail, and if the
762             record is enabled or not. If the dict record does not have all of
763             these, must at least contain the user's email.
764         :type record: dict
765         :returns: {bool: True} if successful or {bool: False} if not
766         :rtype: dict
767
768         .. seealso:: LdapModifyUser, make_ldap_attributes_from_record
769         """
770
771         new_attrs = {}
772         #Disable account
773         new_attrs['shadowExpire'] = '0'
774         logger.debug(" LDAPapi.py \t LdapMarkUserAsDeleted ")
775         ret = self.LdapModifyUser(record, new_attrs)
776         return ret
777
778     def LdapResetPassword(self, record):
779         """Resets password for the user whose record is the parameter and
780         changes the corresponding entry in the LDAP.
781
782         :param record: user's sfa record whose Ldap password must be reset.
783             Should contain first_name,last_name,
784             email or mail, and if the record is enabled or not. If the dict
785             record does not have all of these, must at least contain the user's
786             email.
787         :type record: dict
788         :returns: return value of LdapModifyUser. True if successful, False
789             otherwise.
790
791         .. seealso:: LdapModifyUser, make_ldap_attributes_from_record
792
793         """
794         password = self.login_pwd.generate_password()
795         attrs = {}
796         attrs['userPassword'] = self.login_pwd.encrypt_password(password)
797         logger.debug("LDAP LdapResetPassword encrypt_password %s"
798                      % (attrs['userPassword']))
799         result = self.LdapModifyUser(record, attrs)
800         return result
801
802
803     def LdapSearch(self, req_ldap=None, expected_fields=None):
804         """
805         Used to search directly in LDAP, by using ldap filters and return
806         fields. When req_ldap is None, returns all the entries in the LDAP.
807
808         :param req_ldap: ldap style request, with appropriate filters,
809              example: (cn=*).
810         :param expected_fields: Fields in the user ldap entry that has to be
811             returned. If None is provided, will return 'mail', 'givenName',
812             'sn', 'uid', 'sshPublicKey', 'shadowExpire'.
813         :type req_ldap: string
814         :type expected_fields: list
815
816         .. seealso:: make_ldap_filters_from_record for req_ldap format.
817
818         """
819         logger.debug("JORDAN LdapSearch, req_ldap=%r, expected_fields=%r" % (req_ldap, expected_fields))
820         result = self.conn.connect(bind=False)
821         if (result['bool']):
822
823             return_fields_list = []
824             if expected_fields is None:
825                 return_fields_list = ['mail', 'givenName', 'sn', 'uid',
826                                       'sshPublicKey', 'shadowExpire']
827             else:
828                 return_fields_list = expected_fields
829             #No specifc request specified, get the whole LDAP
830             if req_ldap is None:
831                 req_ldap = '(cn=*)'
832
833             logger.debug("LDAP.PY \t LdapSearch  req_ldap %s \
834                                     return_fields_list %s" \
835                                     %(req_ldap, return_fields_list))
836
837             try:
838                 msg_id = self.conn.ldapserv.search(
839                     self.baseDN, ldap.SCOPE_SUBTREE,
840                     req_ldap, return_fields_list)
841                 #Get all the results matching the search from ldap in one
842                 #shot (1 value)
843                 result_type, result_data = \
844                     self.conn.ldapserv.result(msg_id, 1)
845
846                 self.conn.close()
847
848                 logger.debug("LDAP.PY \t LdapSearch  result_data %s"
849                              % (result_data))
850
851                 return result_data
852
853             except ldap.LDAPError, error:
854                 logger.log_exc("LDAP LdapSearch Error %s" % error)
855                 return []
856
857             else:
858                 logger.error("LDAP.PY \t Connection Failed")
859                 return
860
861     def _process_ldap_info_for_all_users(self, result_data):
862         """Process the data of all enabled users in LDAP.
863
864         :param result_data: Contains information of all enabled users in LDAP
865             and is coming from LdapSearch.
866         :param result_data: list
867
868         .. seealso:: LdapSearch
869
870         """
871         results = []
872         logger.debug(" LDAP.py _process_ldap_info_for_all_users result_data %s "
873                      % (result_data))
874         for ldapentry in result_data:
875             logger.debug(" LDAP.py _process_ldap_info_for_all_users \
876                         ldapentry name : %s " % (ldapentry[1]['uid'][0]))
877             tmpname = ldapentry[1]['uid'][0]
878             hrn = self.authname + "." + tmpname
879
880             tmpemail = ldapentry[1]['mail'][0]
881             if ldapentry[1]['mail'][0] == "unknown":
882                 tmpemail = None
883
884             try:
885                 results.append({
886                     'type': 'user',
887                     'pkey': ldapentry[1]['sshPublicKey'][0],
888                     #'uid': ldapentry[1]['uid'][0],
889                     'uid': tmpname ,
890                     'email':tmpemail,
891                     #'email': ldapentry[1]['mail'][0],
892                     'first_name': ldapentry[1]['givenName'][0],
893                     'last_name': ldapentry[1]['sn'][0],
894                     #'phone': 'none',
895                     'serial': 'none',
896                     'authority': self.authname,
897                     'peer_authority': '',
898                     'pointer': -1,
899                     'hrn': hrn,
900                               })
901             except KeyError, error:
902                 logger.log_exc("LDAPapi.PY \t LdapFindUser EXCEPTION %s"
903                                % (error))
904                 return
905
906         return results
907
908     def _process_ldap_info_for_one_user(self, record, result_data):
909         """
910
911         Put the user's ldap data into shape. Only deals with one user
912         record and one user data from ldap.
913
914         :param record: user record
915         :param result_data: Raw ldap data coming from LdapSearch
916         :returns: user's data dict with 'type','pkey','uid', 'email',
917             'first_name' 'last_name''serial''authority''peer_authority'
918             'pointer''hrn'
919         :type record: dict
920         :type result_data: list
921         :rtype :dict
922
923         """
924         #One entry only in the ldap data because we used a  filter
925         #to find one user only
926         ldapentry = result_data[0][1]
927         logger.debug("LDAP.PY \t LdapFindUser ldapentry %s" % (ldapentry))
928         tmpname = ldapentry['uid'][0]
929
930         tmpemail = ldapentry['mail'][0]
931         if ldapentry['mail'][0] == "unknown":
932             tmpemail = None
933
934         parent_hrn = None
935         peer_authority = None
936         # If the user is coming from External authority (e.g. OneLab)
937         # Then hrn is None, it should be filled in by the creation of Ldap User
938         # XXX LOIC !!! What if a user email is in 2 authorities? 
939         if 'hrn' in record and record['hrn'] is not None:
940             hrn = record['hrn']
941             parent_hrn = get_authority(hrn)
942             if parent_hrn != self.authname:
943                 peer_authority = parent_hrn
944             #In case the user was not imported from Iotlab LDAP
945             #but from another federated site, has an account in
946             #iotlab but currently using his hrn from federated site
947             #then the login is different from the one found in its hrn
948             if tmpname != hrn.split('.')[1]:
949                 hrn = None
950         else:
951             hrn = None
952
953         if hrn is None:
954             results = {
955                 'type': 'user',
956                 'pkey': ldapentry['sshPublicKey'],
957                 #'uid': ldapentry[1]['uid'][0],
958                 'uid': tmpname,
959                 'email': tmpemail,
960                 #'email': ldapentry[1]['mail'][0],
961                 'first_name': ldapentry['givenName'][0],
962                 'last_name': ldapentry['sn'][0],
963                 #'phone': 'none',
964                 'serial': 'none',
965                 'authority': parent_hrn,
966                 'peer_authority': peer_authority,
967                 'pointer': -1,
968              }
969         else:
970             #hrn = None
971             results = {
972                 'type': 'user',
973                 'pkey': ldapentry['sshPublicKey'],
974                 #'uid': ldapentry[1]['uid'][0],
975                 'uid': tmpname,
976                 'email': tmpemail,
977                 #'email': ldapentry[1]['mail'][0],
978                 'first_name': ldapentry['givenName'][0],
979                 'last_name': ldapentry['sn'][0],
980                 #'phone': 'none',
981                 'serial': 'none',
982                 'authority': parent_hrn,
983                 'peer_authority': peer_authority,
984                 'pointer': -1,
985                 'hrn': hrn,
986             }
987         return results
988
989     def LdapFindUser(self, record=None, is_user_enabled=None,
990                      expected_fields=None):
991         """
992
993         Search a SFA user with a hrn. User should be already registered
994         in Iotlab LDAP.
995
996         :param record: sfa user's record. Should contain first_name,last_name,
997             email or mail. If no record is provided, returns all the users found
998             in LDAP.
999         :type record: dict
1000         :param is_user_enabled: is the user's iotlab account already valid.
1001         :type is_user_enabled: Boolean.
1002         :returns: LDAP entries from ldap matching the filter provided. Returns
1003             a single entry if one filter has been given and a list of
1004             entries otherwise.
1005         :rtype:  dict or list
1006
1007         """
1008         logger.debug("JORDAN LdapFindUser record=%r, is_user_enabled=%r, expected_fields=%r" % (record, is_user_enabled, expected_fields))
1009
1010         custom_record = {}
1011         if is_user_enabled:
1012             custom_record['enabled'] = is_user_enabled
1013         if record:
1014             custom_record.update(record)
1015
1016         req_ldap = self.make_ldap_filters_from_record(custom_record)
1017         return_fields_list = []
1018         if expected_fields is None:
1019             return_fields_list = ['mail', 'givenName', 'sn', 'uid',
1020                                   'sshPublicKey']
1021         else:
1022             return_fields_list = expected_fields
1023
1024         result_data = self.LdapSearch(req_ldap, return_fields_list)
1025         logger.debug("LDAP.PY \t LdapFindUser  result_data %s" % (result_data))
1026
1027         if len(result_data) == 0:
1028             return None
1029         #Asked for a specific user
1030         if record is not None:
1031             logger.debug("LOIC - record = %s" % record)
1032             results = self._process_ldap_info_for_one_user(record, result_data)
1033
1034         else:
1035         #Asked for all users in ldap
1036             results = self._process_ldap_info_for_all_users(result_data)
1037         return results