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