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