2to3 -f raise
[sfa.git] / sfa / client / sfaclientlib.py
1 # Thierry Parmentelat -- INRIA
2 """
3 a minimal library for writing "lightweight" SFA clients
4 """
5
6 from __future__ import print_function
7
8 # xxx todo
9 # this library should probably check for the expiration date of the various
10 # certificates and automatically retrieve fresh ones when expired
11
12 import sys
13 import os,os.path
14 import subprocess
15 from datetime import datetime
16 from sfa.util.xrn import Xrn
17
18 import sfa.util.sfalogging
19 # importing sfa.utils.faults does pull a lot of stuff 
20 # OTOH it's imported from Certificate anyways, so..
21 from sfa.util.faults import RecordNotFound
22
23 from sfa.client.sfaserverproxy import SfaServerProxy
24
25 # see optimizing dependencies below
26 from sfa.trust.certificate import Keypair, Certificate
27 from sfa.trust.credential import Credential
28 from sfa.trust.gid import GID
29 ########## 
30 # a helper class to implement the bootstrapping of cryptoa. material
31 # assuming we are starting from scratch on the client side 
32 # what's needed to complete a full slice creation cycle
33 # (**) prerequisites: 
34 #  (*) a local private key 
35 #  (*) the corresp. public key in the registry 
36 # (**) step1: a self-signed certificate
37 #      default filename is <hrn>.sscert
38 # (**) step2: a user credential
39 #      obtained at the registry with GetSelfCredential
40 #      using the self-signed certificate as the SSL cert
41 #      default filename is <hrn>.user.cred
42 # (**) step3: a registry-provided certificate (i.e. a GID)
43 #      obtained at the registry using Resolve
44 #      using the step2 credential as credential
45 #      default filename is <hrn>.user.gid
46 #
47 # From that point on, the GID is used as the SSL certificate
48 # and the following can be done
49 #
50 # (**) retrieve a slice (or authority) credential 
51 #      obtained at the registry with GetCredential
52 #      using the (step2) user-credential as credential
53 #      default filename is <hrn>.<type>.cred
54 # (**) retrieve a slice (or authority) GID 
55 #      obtained at the registry with Resolve
56 #      using the (step2) user-credential as credential
57 #      default filename is <hrn>.<type>.cred
58 #
59 # (**) additionnally, it might make sense to upgrade a GID file 
60 # into a pkcs12 certificate usable in a browser
61 # this bundled format allows for embedding the private key
62
63
64 ########## Implementation notes
65 #
66 # (*) decorators
67 #
68 # this implementation is designed as a guideline for 
69 # porting to other languages
70 #
71 # the decision to go for decorators aims at focusing 
72 # on the core of what needs to be done when everything
73 # works fine, and to take caching and error management 
74 # out of the way
75
76 # for non-pythonic developers, it should be enough to 
77 # implement the bulk of this code, namely the _produce methods
78 # and to add caching and error management by whichever means 
79 # is available, including inline 
80 #
81 # (*) self-signed certificates
82
83 # still with other languages in mind, we've tried to keep the
84 # dependencies to the rest of the code as low as possible
85
86 # however this still relies on the sfa.trust.certificate module
87 # for the initial generation of a self-signed-certificate that
88 # is associated to the user's ssh-key
89 # (for user-friendliness, and for smooth operations with planetlab, 
90 # the usage model is to reuse an existing keypair)
91
92 # there might be a more portable, i.e. less language-dependant way, to
93 # implement this step by exec'ing the openssl command.
94 # a known successful attempt at this approach that worked 
95 # for Java is documented below
96 # http://nam.ece.upatras.gr/fstoolkit/trac/wiki/JavaSFAClient
97 #
98 # (*) pkcs12
99
100 # the implementation of the pkcs12 wrapping, which is a late addition,
101 # is done through direct calls to openssl
102 #
103 ####################
104
105 class SfaClientException (Exception): pass
106
107 class SfaClientBootstrap:
108
109     # dir is mandatory but defaults to '.'
110     def __init__ (self, user_hrn, registry_url, dir=None, 
111                   verbose=False, timeout=None, logger=None):
112         self.hrn=user_hrn
113         self.registry_url=registry_url
114         if dir is None: dir="."
115         self.dir=dir
116         self.verbose=verbose
117         self.timeout=timeout
118         # default for the logger is to use the global sfa logger
119         if logger is None: 
120             logger = sfa.util.sfalogging.logger
121         self.logger=logger
122
123     ######################################## *_produce methods
124     ### step1
125     # unconditionnally create a self-signed certificate
126     def self_signed_cert_produce (self, output):
127         self.assert_private_key()
128         private_key_filename = self.private_key_filename()
129         keypair=Keypair(filename=private_key_filename)
130         self_signed = Certificate (subject = self.hrn)
131         self_signed.set_pubkey (keypair)
132         self_signed.set_issuer (keypair, self.hrn)
133         self_signed.sign ()
134         self_signed.save_to_file (output)
135         self.logger.debug("SfaClientBootstrap: Created self-signed certificate for %s in %s"%\
136                               (self.hrn, output))
137         return output
138
139     ### step2 
140     # unconditionnally retrieve my credential (GetSelfCredential)
141     # we always use the self-signed-cert as the SSL cert
142     def my_credential_produce (self, output):
143         self.assert_self_signed_cert()
144         certificate_filename = self.self_signed_cert_filename()
145         certificate_string = self.plain_read (certificate_filename)
146         self.assert_private_key()
147         registry_proxy = SfaServerProxy (self.registry_url,
148                                          self.private_key_filename(),
149                                          certificate_filename)
150         try:
151             credential_string=registry_proxy.GetSelfCredential (certificate_string, self.hrn, "user")
152         except:
153             # some urns hrns may replace non hierarchy delimiters '.' with an '_' instead of escaping the '.'
154             hrn = Xrn(self.hrn).get_hrn().replace('\.', '_') 
155             credential_string=registry_proxy.GetSelfCredential (certificate_string, hrn, "user")
156         self.plain_write (output, credential_string)
157         self.logger.debug("SfaClientBootstrap: Wrote result of GetSelfCredential in %s"%output)
158         return output
159
160     ### step3
161     # unconditionnally retrieve my GID - use the general form 
162     def my_gid_produce (self,output):
163         return self.gid_produce (output, self.hrn, "user")
164
165     ### retrieve any credential (GetCredential) unconditionnal form
166     # we always use the GID as the SSL cert
167     def credential_produce (self, output, hrn, type):
168         self.assert_my_gid()
169         certificate_filename = self.my_gid_filename()
170         self.assert_private_key()
171         registry_proxy = SfaServerProxy (self.registry_url, self.private_key_filename(),
172                                          certificate_filename)
173         self.assert_my_credential()
174         my_credential_string = self.my_credential_string()
175         credential_string=registry_proxy.GetCredential (my_credential_string, hrn, type)
176         self.plain_write (output, credential_string)
177         self.logger.debug("SfaClientBootstrap: Wrote result of GetCredential in %s"%output)
178         return output
179
180     def slice_credential_produce (self, output, hrn):
181         return self.credential_produce (output, hrn, "slice")
182
183     def authority_credential_produce (self, output, hrn):
184         return self.credential_produce (output, hrn, "authority")
185
186     ### retrieve any gid (Resolve) - unconditionnal form
187     # use my GID when available as the SSL cert, otherwise the self-signed
188     def gid_produce (self, output, hrn, type ):
189         try:
190             self.assert_my_gid()
191             certificate_filename = self.my_gid_filename()
192         except:
193             self.assert_self_signed_cert()
194             certificate_filename = self.self_signed_cert_filename()
195             
196         self.assert_private_key()
197         registry_proxy = SfaServerProxy (self.registry_url, self.private_key_filename(),
198                                          certificate_filename)
199         credential_string=self.plain_read (self.my_credential())
200         records = registry_proxy.Resolve (hrn, credential_string)
201         records=[record for record in records if record['type']==type]
202         if not records:
203             raise RecordNotFound("hrn %s (%s) unknown to registry %s"%(hrn,type,self.registry_url))
204         record=records[0]
205         self.plain_write (output, record['gid'])
206         self.logger.debug("SfaClientBootstrap: Wrote GID for %s (%s) in %s"% (hrn,type,output))
207         return output
208
209
210 # http://trac.myslice.info/wiki/MySlice/Developer/SFALogin
211 ### produce a pkcs12 bundled certificate from GID and private key
212 # xxx for now we put a hard-wired password that's just, well, 'password'
213 # when leaving this empty on the mac, result can't seem to be loaded in keychain..
214     def my_pkcs12_produce (self, filename):
215         password=raw_input("Enter password for p12 certificate: ")
216         openssl_command=['openssl', 'pkcs12', "-export"]
217         openssl_command += [ "-password", "pass:%s"%password ]
218         openssl_command += [ "-inkey", self.private_key_filename()]
219         openssl_command += [ "-in",    self.my_gid_filename()]
220         openssl_command += [ "-out",   filename ]
221         if subprocess.call(openssl_command) ==0:
222             print("Successfully created %s"%filename)
223         else:
224             print("Failed to create %s"%filename)
225
226     # Returns True if credential file is valid. Otherwise return false.
227     def validate_credential(self, filename):
228         valid = True
229         cred = Credential(filename=filename)
230         # check if credential is expires
231         if cred.get_expiration() < datetime.utcnow():
232             valid = False
233         return valid
234     
235
236     #################### public interface
237     
238     # return my_gid, run all missing steps in the bootstrap sequence
239     def bootstrap_my_gid (self):
240         self.self_signed_cert()
241         self.my_credential()
242         return self.my_gid()
243
244     # once we've bootstrapped we can use this object to issue any other SFA call
245     # always use my gid
246     def server_proxy (self, url):
247         self.assert_my_gid()
248         return SfaServerProxy (url, self.private_key_filename(), self.my_gid_filename(),
249                                verbose=self.verbose, timeout=self.timeout)
250
251     # now in some cases the self-signed is enough
252     def server_proxy_simple (self, url):
253         self.assert_self_signed_cert()
254         return SfaServerProxy (url, self.private_key_filename(), self.self_signed_cert_filename(),
255                                verbose=self.verbose, timeout=self.timeout)
256
257     # this method can optionnally be invoked to ensure proper
258     # installation of the private key that belongs to this user
259     # installs private_key in working dir with expected name -- preserve mode
260     # typically user_private_key would be ~/.ssh/id_rsa
261     # xxx should probably check the 2 files are identical
262     def init_private_key_if_missing (self, user_private_key):
263         private_key_filename=self.private_key_filename()
264         if not os.path.isfile (private_key_filename):
265             key=self.plain_read(user_private_key)
266             self.plain_write(private_key_filename, key)
267             os.chmod(private_key_filename,os.stat(user_private_key).st_mode)
268             self.logger.debug("SfaClientBootstrap: Copied private key from %s into %s"%\
269                                   (user_private_key,private_key_filename))
270         
271     #################### private details
272     # stupid stuff
273     def fullpath (self, file): return os.path.join (self.dir,file)
274
275     # the expected filenames for the various pieces
276     def private_key_filename (self): 
277         return self.fullpath ("%s.pkey" % Xrn.unescape(self.hrn))
278     def self_signed_cert_filename (self): 
279         return self.fullpath ("%s.sscert"%self.hrn)
280     def my_credential_filename (self):
281         return self.credential_filename (self.hrn, "user")
282     # the tests use sfi -u <pi-user>; meaning that the slice credential filename
283     # needs to keep track of the user too
284     def credential_filename (self, hrn, type): 
285         if type in ['user']:
286             basename="%s.%s.cred"%(hrn,type)
287         else:
288             basename="%s-%s.%s.cred"%(self.hrn,hrn,type)
289         return self.fullpath (basename)
290     def slice_credential_filename (self, hrn): 
291         return self.credential_filename(hrn,'slice')
292     def authority_credential_filename (self, hrn): 
293         return self.credential_filename(hrn,'authority')
294     def my_gid_filename (self):
295         return self.gid_filename (self.hrn, "user")
296     def gid_filename (self, hrn, type): 
297         return self.fullpath ("%s.%s.gid"%(hrn,type))
298     def my_pkcs12_filename (self):
299         return self.fullpath ("%s.p12"%self.hrn)
300
301 # optimizing dependencies
302 # originally we used classes GID or Credential or Certificate 
303 # like e.g. 
304 #        return Credential(filename=self.my_credential()).save_to_string()
305 # but in order to make it simpler to other implementations/languages..
306     def plain_read (self, filename):
307         infile=file(filename,"r")
308         result=infile.read()
309         infile.close()
310         return result
311
312     def plain_write (self, filename, contents):
313         outfile=file(filename,"w")
314         result=outfile.write(contents)
315         outfile.close()
316
317     def assert_filename (self, filename, kind):
318         if not os.path.isfile (filename):
319             raise IOError("Missing %s file %s"%(kind,filename))
320         return True
321         
322     def assert_private_key (self):
323         return self.assert_filename (self.private_key_filename(),"private key")
324     def assert_self_signed_cert (self):
325         return self.assert_filename (self.self_signed_cert_filename(),"self-signed certificate")
326     def assert_my_credential (self):
327         return self.assert_filename (self.my_credential_filename(),"user's credential")
328     def assert_my_gid (self):
329         return self.assert_filename (self.my_gid_filename(),"user's GID")
330
331
332     # decorator to make up the other methods
333     def get_or_produce (filename_method, produce_method, validate_method=None):
334         # default validator returns true
335         def wrap (f):
336             def wrapped (self, *args, **kw):
337                 filename=filename_method (self, *args, **kw)
338                 if os.path.isfile ( filename ):
339                     if not validate_method:
340                         return filename
341                     elif validate_method(self, filename): 
342                         return filename
343                     else:
344                         # remove invalid file
345                         self.logger.warning ("Removing %s - has expired"%filename)
346                         os.unlink(filename) 
347                 try:
348                     produce_method (self, filename, *args, **kw)
349                     return filename
350                 except IOError:
351                     raise 
352                 except :
353                     error = sys.exc_info()[:2]
354                     message="Could not produce/retrieve %s (%s -- %s)"%\
355                         (filename,error[0],error[1])
356                     self.logger.log_exc(message)
357                     raise Exception(message)
358             return wrapped
359         return wrap
360
361     @get_or_produce (self_signed_cert_filename, self_signed_cert_produce)
362     def self_signed_cert (self): pass
363
364     @get_or_produce (my_credential_filename, my_credential_produce, validate_credential)
365     def my_credential (self): pass
366
367     @get_or_produce (my_gid_filename, my_gid_produce)
368     def my_gid (self): pass
369
370     @get_or_produce (my_pkcs12_filename, my_pkcs12_produce)
371     def my_pkcs12 (self): pass
372
373     @get_or_produce (credential_filename, credential_produce, validate_credential)
374     def credential (self, hrn, type): pass
375
376     @get_or_produce (slice_credential_filename, slice_credential_produce, validate_credential)
377     def slice_credential (self, hrn): pass
378
379     @get_or_produce (authority_credential_filename, authority_credential_produce, validate_credential)
380     def authority_credential (self, hrn): pass
381
382     @get_or_produce (gid_filename, gid_produce)
383     def gid (self, hrn, type ): pass
384
385
386     # get the credentials as strings, for inserting as API arguments
387     def my_credential_string (self): 
388         self.my_credential()
389         return self.plain_read(self.my_credential_filename())
390     def slice_credential_string (self, hrn): 
391         self.slice_credential(hrn)
392         return self.plain_read(self.slice_credential_filename(hrn))
393     def authority_credential_string (self, hrn): 
394         self.authority_credential(hrn)
395         return self.plain_read(self.authority_credential_filename(hrn))
396
397     # for consistency
398     def private_key (self):
399         self.assert_private_key()
400         return self.private_key_filename()
401
402     def delegate_credential_string (self, original_credential, to_hrn, to_type='authority'):
403         """
404         sign a delegation credential to someone else
405
406         original_credential : typically one's user- or slice- credential to be delegated to s/b else
407         to_hrn : the hrn of the person that will be allowed to do stuff on our behalf
408         to_type : goes with to_hrn, usually 'user' or 'authority'
409
410         returns a string with the delegated credential
411
412         this internally uses self.my_gid()
413         it also retrieves the gid for to_hrn/to_type
414         and uses Credential.delegate()"""
415
416         # the gid and hrn of the object we are delegating
417         if isinstance (original_credential, str):
418             original_credential = Credential (string=original_credential)
419         original_gid = original_credential.get_gid_object()
420         original_hrn = original_gid.get_hrn()
421
422         if not original_credential.get_privileges().get_all_delegate():
423             self.logger.error("delegate_credential_string: original credential %s does not have delegate bit set"%original_hrn)
424             return
425
426         # the delegating user's gid
427         my_gid = self.my_gid()
428
429         # retrieve the GID for the entity that we're delegating to
430         to_gidfile = self.gid (to_hrn,to_type)
431 #        to_gid = GID ( to_gidfile )
432 #        to_hrn = delegee_gid.get_hrn()
433 #        print 'to_hrn',to_hrn
434         delegated_credential = original_credential.delegate(to_gidfile, self.private_key(), my_gid)
435         return delegated_credential.save_to_string(save_parents=True)