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