Adding oar config file.
[sfa.git] / sfa / senslab / 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 from sfa.trust.hierarchy import Hierarchy
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     def __init__(self, config_file =  '/etc/sfa/ldap_config.py'):
17         
18         try:
19             execfile(config_file, self.__dict__)
20        
21             self.config_file = config_file
22             # path to configuration data
23             self.config_path = os.path.dirname(config_file)
24         except IOError:
25             raise IOError, "Could not find or load the configuration file: %s" \
26                             % config_file
27   
28         
29 class ldap_co:
30     """ Set admin login and server configuration variables."""
31     
32     def __init__(self):
33         #Senslab PROD LDAP parameters
34         self.ldapserv = None
35         ldap_config = LdapConfig()
36         self.config = ldap_config
37         self.ldapHost = ldap_config.LDAP_IP_ADDRESS 
38         self.ldapPeopleDN = ldap_config.LDAP_PEOPLE_DN
39         self.ldapGroupDN = ldap_config.LDAP_GROUP_DN
40         self.ldapAdminDN = ldap_config.LDAP_WEB_DN
41         self.ldapAdminPassword = ldap_config.LDAP_WEB_PASSWORD
42
43
44         self.ldapPort = ldap.PORT
45         self.ldapVersion  = ldap.VERSION3
46         self.ldapSearchScope = ldap.SCOPE_SUBTREE
47
48
49     def connect(self, bind = True):
50         """Enables connection to the LDAP server.
51         Set the bind parameter to True if a bind is needed
52         (for add/modify/delete operations).
53         Set to False otherwise.
54         
55         """
56         try:
57             self.ldapserv = ldap.open(self.ldapHost)
58         except ldap.LDAPError, error:
59             return {'bool' : False, 'message' : error }
60         
61         # Bind with authentification
62         if(bind): 
63             return self.bind()
64         
65         else:     
66             return {'bool': True} 
67     
68     def bind(self):
69         """ Binding method. """
70         try:
71             # Opens a connection after a call to ldap.open in connect:
72             self.ldapserv = ldap.initialize("ldap://" + self.ldapHost)
73                 
74             # Bind/authenticate with a user with apropriate 
75             #rights to add objects
76             self.ldapserv.simple_bind_s(self.ldapAdminDN, \
77                                     self.ldapAdminPassword)
78
79         except ldap.LDAPError, error:
80             return {'bool' : False, 'message' : error }
81
82         return {'bool': True}
83     
84     def close(self):
85         """ Close the LDAP connection """
86         try:
87             self.ldapserv.unbind_s()
88         except ldap.LDAPError, error:
89             return {'bool' : False, 'message' : error }
90             
91         
92 class LDAPapi :
93     def __init__(self):
94         logger.setLevelDebug() 
95         #SFA related config
96         self.senslabauth = Hierarchy()
97         config = Config()
98         
99         self.authname = config.SFA_REGISTRY_ROOT_AUTH
100
101         self.conn =  ldap_co() 
102         self.ldapUserQuotaNFS = self.conn.config.LDAP_USER_QUOTA_NFS 
103         self.ldapUserUidNumberMin = self.conn.config.LDAP_USER_UID_NUMBER_MIN 
104         self.ldapUserGidNumber = self.conn.config.LDAP_USER_GID_NUMBER 
105         self.ldapUserHomePath = self.conn.config.LDAP_USER_HOME_PATH 
106         
107         self.lengthPassword = 8
108         self.baseDN = self.conn.ldapPeopleDN
109         #authinfo=self.senslabauth.get_auth_info(self.authname)
110         
111         
112         self.charsPassword = [ '!','$','(',')','*','+',',','-','.', \
113                                 '0','1','2','3','4','5','6','7','8','9', \
114                                 'A','B','C','D','E','F','G','H','I','J', \
115                                 'K','L','M','N','O','P','Q','R','S','T', \
116                                 'U','V','W','X','Y','Z','_','a','b','c', \
117                                 'd','e','f','g','h','i','j','k','l','m', \
118                                 'n','o','p','q','r','s','t','u','v','w', \
119                                 'x','y','z','\'']
120         
121         self.ldapShell = '/bin/bash'
122
123     
124     def generate_login(self, record):
125         """Generate login for adding a new user in LDAP Directory 
126         (four characters minimum length)
127         Record contains first name and last name.
128         
129         """ 
130         #Remove all special characters from first_name/last name
131         lower_first_name = record['first_name'].replace('-','')\
132                                         .replace('_','').replace('[','')\
133                                         .replace(']','').replace(' ','')\
134                                         .lower()
135         lower_last_name = record['last_name'].replace('-','')\
136                                         .replace('_','').replace('[','')\
137                                         .replace(']','').replace(' ','')\
138                                         .lower()  
139         length_last_name = len(lower_last_name)
140         login_max_length = 8
141         
142         #Try generating a unique login based on first name and last name
143         getAttrs = ['uid']
144         if length_last_name >= login_max_length :
145             login = lower_last_name[0:login_max_length]
146             index = 0
147             logger.debug("login : %s index : %s" %(login, index))
148         elif length_last_name >= 4 :
149             login = lower_last_name
150             index = 0
151             logger.debug("login : %s index : %s" %(login, index))
152         elif length_last_name == 3 :
153             login = lower_first_name[0:1] + lower_last_name
154             index = 1
155             logger.debug("login : %s index : %s" %(login, index))
156         elif length_last_name == 2:
157             if len ( lower_first_name) >=2:
158                 login = lower_first_name[0:2] + lower_last_name
159                 index = 2
160                 logger.debug("login : %s index : %s" %(login, index))
161             else:
162                 logger.error("LoginException : \
163                             Generation login error with \
164                             minimum four characters")
165             
166                 
167         else :
168             logger.error("LDAP generate_login failed : \
169                             impossible to generate unique login for %s %s" \
170                             %(lower_first_name,lower_last_name))
171             
172         login_filter = '(uid=' + login + ')'
173         
174         try :
175             #Check if login already in use
176             while (len(self.LdapSearch(login_filter, getAttrs)) is not 0 ):
177             
178                 index += 1
179                 if index >= 9:
180                     logger.error("LoginException : Generation login error \
181                                     with minimum four characters")
182                 else:
183                     try:
184                         login = lower_first_name[0:index] + \
185                                     lower_last_name[0:login_max_length-index]
186                         login_filter = '(uid='+ login+ ')'
187                     except KeyError:
188                         print "lower_first_name - lower_last_name too short"
189                         
190             logger.debug("LDAP.API \t generate_login login %s" %(login))
191             return login
192                     
193         except  ldap.LDAPError, error :
194             logger.log_exc("LDAP generate_login Error %s" %error)
195             return None
196
197         
198
199     def generate_password(self):
200     
201         """Generate password for adding a new user in LDAP Directory 
202         (8 characters length) return password
203         
204         """
205         password = str()
206         length = len(self.charsPassword)
207         for index in range(self.lengthPassword):
208             char_index = random.randint(0, length-1)
209             password += self.charsPassword[char_index]
210
211         return password
212
213     def encrypt_password(self, password):
214         """ Use passlib library to make a RFC2307 LDAP encrypted password
215         salt size = 8, use sha-1 algorithm. Returns encrypted password.
216         
217         """
218         #Keep consistency with Java Senslab's LDAP API 
219         #RFC2307SSHAPasswordEncryptor so set the salt size to 8 bytres
220         return lssha.encrypt(password, salt_size = 8)
221     
222
223
224     def find_max_uidNumber(self):
225             
226         """Find the LDAP max uidNumber (POSIX uid attribute) .
227         Used when adding a new user in LDAP Directory 
228         returns string  max uidNumber + 1
229         
230         """
231         #First, get all the users in the LDAP
232         getAttrs = "(uidNumber=*)"
233         login_filter = ['uidNumber']
234
235         result_data = self.LdapSearch(getAttrs, login_filter) 
236         #It there is no user in LDAP yet, First LDAP user
237         if result_data == []:
238             max_uidnumber = self.ldapUserUidNumberMin
239         #Otherwise, get the highest uidNumber
240         else:
241             
242             uidNumberList = [int(r[1]['uidNumber'][0])for r in result_data ]
243             logger.debug("LDAPapi.py \tfind_max_uidNumber  \
244                                     uidNumberList %s " %(uidNumberList))
245             max_uidnumber = max(uidNumberList) + 1
246             
247         return str(max_uidnumber)
248          
249          
250     def get_ssh_pkey(self, record):
251         """TODO ; Get ssh public key from sfa record  
252         To be filled by N. Turro ? or using GID pl way?
253         
254         """
255         return 'A REMPLIR '
256
257     def make_ldap_filters_from_record(self, record=None):
258         """TODO Handle OR filtering in the ldap query when 
259         dealing with a list of records instead of doing a for loop in GetPersons   
260         Helper function to make LDAP filter requests out of SFA records.
261         """
262         req_ldap = ''
263         req_ldapdict = {}
264         if record :
265             if 'first_name' in record  and 'last_name' in record:
266                 req_ldapdict['cn'] = str(record['first_name'])+" "\
267                                         + str(record['last_name'])
268             if 'email' in record :
269                 req_ldapdict['mail'] = record['email']
270             if 'mail' in record:
271                 req_ldapdict['mail'] = record['mail']
272             if 'enabled' in record:
273                 if record['enabled'] == True :
274                     req_ldapdict['shadowExpire'] = '-1'
275                 else:
276                     req_ldapdict['shadowExpire'] = '0'
277                 
278             #Hrn should not be part of the filter because the hrn 
279             #presented by a certificate of a SFA user not imported in 
280             #Senslab  does not include the senslab login in it 
281             #Plus, the SFA user may already have an account with senslab
282             #using another login.
283                 
284             #if 'hrn' in record :
285                 #splited_hrn = record['hrn'].split(".")
286                 #if splited_hrn[0] != self.authname :
287                     #logger.warning(" \r\n LDAP.PY \
288                         #make_ldap_filters_from_record I know nothing \
289                         #about %s my authname is %s not %s" \
290                         #%(record['hrn'], self.authname, splited_hrn[0]) )
291                         
292                 #login=splited_hrn[1]
293                 #req_ldapdict['uid'] = login
294             
295
296             logger.debug("\r\n \t LDAP.PY make_ldap_filters_from_record \
297                                 record %s req_ldapdict %s" \
298                                 %(record, req_ldapdict))
299             
300             for k in req_ldapdict:
301                 req_ldap += '('+ str(k)+ '=' + str(req_ldapdict[k]) + ')'
302             if  len(req_ldapdict.keys()) >1 :
303                 req_ldap = req_ldap[:0]+"(&"+req_ldap[0:]
304                 size = len(req_ldap)
305                 req_ldap = req_ldap[:(size-1)] +')'+ req_ldap[(size-1):]
306         else:
307             req_ldap = "(cn=*)"
308         
309         return req_ldap
310         
311     def make_ldap_attributes_from_record(self, record):
312         """When addind a new user to Senslab's LDAP, creates an attributes 
313         dictionnary from the SFA record.
314         
315         """
316
317         attrs = {}
318         attrs['objectClass'] = ["top", "person", "inetOrgPerson", \
319                                     "organizationalPerson", "posixAccount", \
320                                     "shadowAccount", "systemQuotas", \
321                                     "ldapPublicKey"]
322         
323         attrs['givenName'] = str(record['first_name']).lower().capitalize()
324         attrs['sn'] = str(record['last_name']).lower().capitalize()
325         attrs['cn'] = attrs['givenName'] + ' ' + attrs['sn']
326         attrs['gecos'] = attrs['givenName'] + ' ' + attrs['sn']
327         attrs['uid'] = self.generate_login(record)   
328                     
329         attrs['quota'] = self.ldapUserQuotaNFS 
330         attrs['homeDirectory'] = self.ldapUserHomePath + attrs['uid']
331         attrs['loginShell'] = self.ldapShell
332         attrs['gidNumber'] = self.ldapUserGidNumber
333         attrs['uidNumber'] = self.find_max_uidNumber()
334         attrs['mail'] = record['mail'].lower()
335         try:
336             attrs['sshPublicKey'] = record['pkey']
337         except KeyError:
338             attrs['sshPublicKey'] = self.get_ssh_pkey(record) 
339         
340
341         #Password is automatically generated because SFA user don't go 
342         #through the Senslab website  used to register new users, 
343         #There is no place in SFA where users can enter such information
344         #yet.
345         #If the user wants to set his own password , he must go to the Senslab 
346         #website.
347         password = self.generate_password()
348         attrs['userPassword'] = self.encrypt_password(password)
349         
350         #Account automatically validated (no mail request to admins)
351         #Set to 0 to disable the account, -1 to enable it,
352         attrs['shadowExpire'] = '-1'
353
354         #Motivation field in Senslab
355         attrs['description'] = 'SFA USER FROM OUTSIDE SENSLAB'
356
357         attrs['ou'] = 'SFA'         #Optional: organizational unit
358         #No info about those here:
359         attrs['l'] = 'To be defined'#Optional: Locality. 
360         attrs['st'] = 'To be defined' #Optional: state or province (country).
361
362         return attrs
363
364
365
366     def LdapAddUser(self, record) :
367         """Add SFA user to LDAP if it is not in LDAP  yet. """
368         
369         user_ldap_attrs = self.make_ldap_attributes_from_record(record)
370
371         
372         #Check if user already in LDAP wih email, first name and last name
373         filter_by = self.make_ldap_filters_from_record(user_ldap_attrs)
374         user_exist = self.LdapSearch(filter_by)
375         if user_exist:
376             logger.warning(" \r\n \t LDAP LdapAddUser user %s %s \
377                         already exists" %(user_ldap_attrs['sn'], \
378                         user_ldap_attrs['mail'])) 
379             return {'bool': False}
380         
381         #Bind to the server
382         result = self.conn.connect()
383         
384         if(result['bool']):
385             
386             # A dict to help build the "body" of the object
387             
388             logger.debug(" \r\n \t LDAP LdapAddUser attrs %s " %user_ldap_attrs)
389
390             # The dn of our new entry/object
391             dn = 'uid=' + user_ldap_attrs['uid'] + "," + self.baseDN 
392
393             try:
394                 ldif = modlist.addModlist(user_ldap_attrs)
395                 logger.debug("LDAPapi.py add attrs %s \r\n  ldif %s"\
396                                 %(user_ldap_attrs,ldif) )
397                 self.conn.ldapserv.add_s(dn,ldif)
398                 
399                 logger.info("Adding user %s login %s in LDAP" \
400                         %(user_ldap_attrs['cn'] ,user_ldap_attrs['uid']))
401                         
402                         
403             except ldap.LDAPError, error:
404                 logger.log_exc("LDAP Add Error %s" %error)
405                 return {'bool' : False, 'message' : error }
406         
407             self.conn.close()
408             return {'bool': True}  
409         else: 
410             return result
411
412         
413     def LdapDelete(self, person_dn):
414         """
415         Deletes a person in LDAP. Uses the dn of the user.
416         """
417         #Connect and bind   
418         result =  self.conn.connect()
419         if(result['bool']):
420             try:
421                 self.conn.ldapserv.delete_s(person_dn)
422                 self.conn.close()
423                 return {'bool': True}
424             
425             except ldap.LDAPError, error:
426                 logger.log_exc("LDAP Delete Error %s" %error)
427                 return {'bool': False}
428         
429     
430     def LdapDeleteUser(self, record_filter): 
431         """
432         Deletes a SFA person in LDAP, based on the user's hrn.
433         """
434         #Find uid of the  person 
435         person = self.LdapFindUser(record_filter,[])
436         logger.debug("LDAPapi.py \t LdapDeleteUser record %s person %s" \
437         %(record_filter, person))
438
439         if person:
440             dn = 'uid=' + person['uid'] + "," +self.baseDN 
441         else:
442             return {'bool': False}
443         
444         result = self.LdapDelete(dn)
445         return result
446         
447
448     def LdapModify(self, dn, old_attributes_dict, new_attributes_dict): 
449         """ Modifies a LDAP entry """
450          
451         ldif = modlist.modifyModlist(old_attributes_dict,new_attributes_dict)
452         # Connect and bind/authenticate    
453         result = self.conn.connect() 
454         if (result['bool']): 
455             try:
456                 self.conn.ldapserv.modify_s(dn,ldif)
457                 self.conn.close()
458                 return {'bool' : True }
459             except ldap.LDAPError, error:
460                 logger.log_exc("LDAP LdapModify Error %s" %error)
461                 return {'bool' : False }
462     
463         
464     def LdapModifyUser(self, user_record, new_attributes_dict):
465         """
466         Gets the record from one user_uid_login based on record_filter 
467         and changes the attributes according to the specified new_attributes.
468         Does not use this if we need to modify the uid. Use a ModRDN 
469         #operation instead ( modify relative DN )
470         """
471         if user_record is None:
472             logger.error("LDAP \t LdapModifyUser Need user record  ")
473             return {'bool': False} 
474         
475         #Get all the attributes of the user_uid_login 
476         #person = self.LdapFindUser(record_filter,[])
477         req_ldap = self.make_ldap_filters_from_record(user_record)
478         person_list = self.LdapSearch(req_ldap,[])
479         logger.debug("LDAPapi.py \t LdapModifyUser person_list : %s" \
480                                                         %(person_list))
481         if person_list and len(person_list) > 1 :
482             logger.error("LDAP \t LdapModifyUser Too many users returned")
483             return {'bool': False}
484         if person_list is None :
485             logger.error("LDAP \t LdapModifyUser  User %s doesn't exist "\
486                         %(user_record))
487             return {'bool': False} 
488         
489         # The dn of our existing entry/object
490         #One result only from ldapSearch
491         person = person_list[0][1]
492         dn  = 'uid=' + person['uid'][0] + "," +self.baseDN  
493        
494         if new_attributes_dict:
495             old = {}
496             for k in new_attributes_dict:
497                 if k not in person:
498                     old[k] =  ''
499                 else :
500                     old[k] = person[k]
501             logger.debug(" LDAPapi.py \t LdapModifyUser  new_attributes %s"\
502                                 %( new_attributes_dict))  
503             result = self.LdapModify(dn, old,new_attributes_dict)
504             return result
505         else:
506             logger.error("LDAP \t LdapModifyUser  No new attributes given. ")
507             return {'bool': False} 
508             
509             
510             
511             
512     def LdapMarkUserAsDeleted(self, record): 
513
514         
515         new_attrs = {}
516         #Disable account
517         new_attrs['shadowExpire'] = '0'
518         logger.debug(" LDAPapi.py \t LdapMarkUserAsDeleted ")
519         ret = self.LdapModifyUser(record, new_attrs)
520         return ret
521
522             
523     def LdapResetPassword(self,record):
524         """
525         Resets password for the user whose record is the parameter and changes
526         the corresponding entry in the LDAP.
527         
528         """
529         password = self.generate_password()
530         attrs = {}
531         attrs['userPassword'] = self.encrypt_password(password)
532         logger.debug("LDAP LdapResetPassword encrypt_password %s"\
533                     %(attrs['userPassword']))
534         result = self.LdapModifyUser(record, attrs)
535         return result
536         
537         
538     def LdapSearch (self, req_ldap = None, expected_fields = None ):
539         """
540         Used to search directly in LDAP, by using ldap filters and
541         return fields. 
542         When req_ldap is None, returns all the entries in the LDAP.
543       
544         """
545         result = self.conn.connect(bind = False)
546         if (result['bool']) :
547             
548             return_fields_list = []
549             if expected_fields == None : 
550                 return_fields_list = ['mail','givenName', 'sn', 'uid', \
551                                         'sshPublicKey', 'shadowExpire']
552             else : 
553                 return_fields_list = expected_fields
554             #No specifc request specified, get the whole LDAP    
555             if req_ldap == None:
556                 req_ldap = '(cn=*)'
557                
558             logger.debug("LDAP.PY \t LdapSearch  req_ldap %s \
559                                     return_fields_list %s" \
560                                     %(req_ldap, return_fields_list))
561
562             try:
563                 msg_id = self.conn.ldapserv.search(
564                                             self.baseDN,ldap.SCOPE_SUBTREE,\
565                                             req_ldap, return_fields_list)     
566                 #Get all the results matching the search from ldap in one 
567                 #shot (1 value)
568                 result_type, result_data = \
569                                         self.conn.ldapserv.result(msg_id,1)
570
571                 self.conn.close()
572
573                 logger.debug("LDAP.PY \t LdapSearch  result_data %s"\
574                             %(result_data))
575
576                 return result_data
577             
578             except  ldap.LDAPError,error :
579                 logger.log_exc("LDAP LdapSearch Error %s" %error)
580                 return []
581             
582             else:
583                 logger.error("LDAP.PY \t Connection Failed" )
584                 return 
585         
586     def LdapFindUser(self, record = None, is_user_enabled=None, \
587             expected_fields = None):
588         """
589         Search a SFA user with a hrn. User should be already registered 
590         in Senslab LDAP. 
591         Returns one matching entry 
592         """   
593         custom_record = {}
594         if is_user_enabled: 
595           
596             custom_record['enabled'] = is_user_enabled
597         if record:  
598             custom_record.update(record)
599
600
601         req_ldap = self.make_ldap_filters_from_record(custom_record)     
602         return_fields_list = []
603         if expected_fields == None : 
604             return_fields_list = ['mail','givenName', 'sn', 'uid', \
605                                     'sshPublicKey']
606         else : 
607             return_fields_list = expected_fields
608             
609         result_data = self.LdapSearch(req_ldap, return_fields_list )
610         logger.debug("LDAP.PY \t LdapFindUser  result_data %s" %(result_data))
611            
612         if len(result_data) is 0:
613             return None
614         #Asked for a specific user
615         if record :
616             #try:
617             ldapentry = result_data[0][1]
618             logger.debug("LDAP.PY \t LdapFindUser ldapentry %s" %(ldapentry))
619             tmpname = ldapentry['uid'][0]
620
621             tmpemail = ldapentry['mail'][0]
622             if ldapentry['mail'][0] == "unknown":
623                 tmpemail = None
624                     
625             #except IndexError: 
626                 #logger.error("LDAP ldapFindHRn : no entry for record %s found"\
627                             #%(record))
628                 #return None
629                 
630             try:
631                 hrn = record['hrn']
632                 parent_hrn = get_authority(hrn)
633                 peer_authority = None
634                 if parent_hrn is not self.authname:
635                     peer_authority = parent_hrn
636
637                 results =  {    
638                             'type': 'user',
639                             'pkey': ldapentry['sshPublicKey'][0],
640                             #'uid': ldapentry[1]['uid'][0],
641                             'uid': tmpname ,
642                             'email':tmpemail,
643                             #'email': ldapentry[1]['mail'][0],
644                             'first_name': ldapentry['givenName'][0],
645                             'last_name': ldapentry['sn'][0],
646                             #'phone': 'none',
647                             'serial': 'none',
648                             'authority': parent_hrn,
649                             'peer_authority': peer_authority,
650                             'pointer' : -1,
651                             'hrn': hrn,
652                             }
653             except KeyError,error:
654                 logger.log_exc("LDAPapi \t LdaFindUser KEyError %s" \
655                                 %error )
656                 return
657         else:
658         #Asked for all users in ldap
659             results = []
660             for ldapentry in result_data:
661                 logger.debug(" LDAP.py LdapFindUser ldapentry name : %s " \
662                                 %(ldapentry[1]['uid'][0]))
663                 tmpname = ldapentry[1]['uid'][0]
664                 hrn=self.authname+"."+ tmpname
665                 
666                 tmpemail = ldapentry[1]['mail'][0]
667                 if ldapentry[1]['mail'][0] == "unknown":
668                     tmpemail = None
669
670         
671                 parent_hrn = get_authority(hrn)
672                 parent_auth_info = self.senslabauth.get_auth_info(parent_hrn)
673                 try:
674                     results.append(  {  
675                             'type': 'user',
676                             'pkey': ldapentry[1]['sshPublicKey'][0],
677                             #'uid': ldapentry[1]['uid'][0],
678                             'uid': tmpname ,
679                             'email':tmpemail,
680                             #'email': ldapentry[1]['mail'][0],
681                             'first_name': ldapentry[1]['givenName'][0],
682                             'last_name': ldapentry[1]['sn'][0],
683                             #'phone': 'none',
684                             'serial': 'none',
685                             'authority': self.authname,
686                             'peer_authority': '',
687                             'pointer' : -1,
688                             'hrn': hrn,
689                             } ) 
690                 except KeyError,error:
691                     logger.log_exc("LDAPapi.PY \t LdapFindUser EXCEPTION %s" \
692                                                 %(error))
693                     return
694         return results   
695