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