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