rename get_summary_tostring into pretty_cred
[sfa.git] / sfa / trust / credential.py
1 #----------------------------------------------------------------------
2 # Copyright (c) 2008 Board of Trustees, Princeton University
3 #
4 # Permission is hereby granted, free of charge, to any person obtaining
5 # a copy of this software and/or hardware specification (the "Work") to
6 # deal in the Work without restriction, including without limitation the
7 # rights to use, copy, modify, merge, publish, distribute, sublicense,
8 # and/or sell copies of the Work, and to permit persons to whom the Work
9 # is furnished to do so, subject to the following conditions:
10 #
11 # The above copyright notice and this permission notice shall be
12 # included in all copies or substantial portions of the Work.
13 #
14 # THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
15 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
16 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
17 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 
18 # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
19 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
20 # OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS 
21 # IN THE WORK.
22 #----------------------------------------------------------------------
23 ##
24 # Implements SFA Credentials
25 #
26 # Credentials are signed XML files that assign a subject gid privileges to an object gid
27 ##
28
29 import os, os.path
30 import subprocess
31 from types import StringTypes
32 import datetime
33 from StringIO import StringIO
34 from tempfile import mkstemp
35 from xml.dom.minidom import Document, parseString
36
37 HAVELXML = False
38 try:
39     from lxml import etree
40     HAVELXML = True
41 except:
42     pass
43
44 from xml.parsers.expat import ExpatError
45
46 from sfa.util.faults import CredentialNotVerifiable, ChildRightsNotSubsetOfParent
47 from sfa.util.sfalogging import logger
48 from sfa.util.sfatime import utcparse, SFATIME_FORMAT
49 from sfa.trust.rights import Right, Rights, determine_rights
50 from sfa.trust.gid import GID
51 from sfa.util.xrn import urn_to_hrn, hrn_authfor_hrn
52
53 # 31 days, in seconds 
54 DEFAULT_CREDENTIAL_LIFETIME = 86400 * 28
55
56
57 # TODO:
58 # . make privs match between PG and PL
59 # . Need to add support for other types of credentials, e.g. tickets
60 # . add namespaces to signed-credential element?
61
62 signature_template = \
63 '''
64 <Signature xml:id="Sig_%s" xmlns="http://www.w3.org/2000/09/xmldsig#">
65   <SignedInfo>
66     <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
67     <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
68     <Reference URI="#%s">
69       <Transforms>
70         <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
71       </Transforms>
72       <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
73       <DigestValue></DigestValue>
74     </Reference>
75   </SignedInfo>
76   <SignatureValue />
77   <KeyInfo>
78     <X509Data>
79       <X509SubjectName/>
80       <X509IssuerSerial/>
81       <X509Certificate/>
82     </X509Data>
83     <KeyValue />
84   </KeyInfo>
85 </Signature>
86 '''
87
88 # PG formats the template (whitespace) slightly differently.
89 # Note that they don't include the xmlns in the template, but add it later.
90 # Otherwise the two are equivalent.
91 #signature_template_as_in_pg = \
92 #'''
93 #<Signature xml:id="Sig_%s" >
94 # <SignedInfo>
95 #  <CanonicalizationMethod      Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
96 #  <SignatureMethod      Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
97 #  <Reference URI="#%s">
98 #    <Transforms>
99 #      <Transform         Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
100 #    </Transforms>
101 #    <DigestMethod        Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
102 #    <DigestValue></DigestValue>
103 #    </Reference>
104 # </SignedInfo>
105 # <SignatureValue />
106 # <KeyInfo>
107 #  <X509Data >
108 #   <X509SubjectName/>
109 #   <X509IssuerSerial/>
110 #   <X509Certificate/>
111 #  </X509Data>
112 #  <KeyValue />
113 # </KeyInfo>
114 #</Signature>
115 #'''
116
117 ##
118 # Convert a string into a bool
119 # used to convert an xsd:boolean to a Python boolean
120 def str2bool(str):
121     if str.lower() in ['true','1']:
122         return True
123     return False
124
125
126 ##
127 # Utility function to get the text of an XML element
128
129 def getTextNode(element, subele):
130     sub = element.getElementsByTagName(subele)[0]
131     if len(sub.childNodes) > 0:            
132         return sub.childNodes[0].nodeValue
133     else:
134         return None
135         
136 ##
137 # Utility function to set the text of an XML element
138 # It creates the element, adds the text to it,
139 # and then appends it to the parent.
140
141 def append_sub(doc, parent, element, text):
142     ele = doc.createElement(element)
143     ele.appendChild(doc.createTextNode(text))
144     parent.appendChild(ele)
145
146 ##
147 # Signature contains information about an xmlsec1 signature
148 # for a signed-credential
149 #
150
151 class Signature(object):
152    
153     def __init__(self, string=None):
154         self.refid = None
155         self.issuer_gid = None
156         self.xml = None
157         if string:
158             self.xml = string
159             self.decode()
160
161
162     def get_refid(self):
163         if not self.refid:
164             self.decode()
165         return self.refid
166
167     def get_xml(self):
168         if not self.xml:
169             self.encode()
170         return self.xml
171
172     def set_refid(self, id):
173         self.refid = id
174
175     def get_issuer_gid(self):
176         if not self.gid:
177             self.decode()
178         return self.gid        
179
180     def set_issuer_gid(self, gid):
181         self.gid = gid
182
183     def decode(self):
184         try:
185             doc = parseString(self.xml)
186         except ExpatError,e:
187             logger.log_exc ("Failed to parse credential, %s"%self.xml)
188             raise
189         sig = doc.getElementsByTagName("Signature")[0]
190         self.set_refid(sig.getAttribute("xml:id").strip("Sig_"))
191         keyinfo = sig.getElementsByTagName("X509Data")[0]
192         szgid = getTextNode(keyinfo, "X509Certificate")
193         szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid
194         self.set_issuer_gid(GID(string=szgid))        
195         
196     def encode(self):
197         self.xml = signature_template % (self.get_refid(), self.get_refid())
198
199 ##
200 # A credential provides a caller gid with privileges to an object gid.
201 # A signed credential is signed by the object's authority.
202 #
203 # Credentials are encoded in one of two ways. 
204 # The legacy style (now unsupported) places it in the subjectAltName of an X509 certificate.
205 # The new credentials are placed in signed XML.
206 #
207 # WARNING:
208 # In general, a signed credential obtained externally should
209 # not be changed else the signature is no longer valid.  So, once
210 # you have loaded an existing signed credential, do not call encode() or sign() on it.
211
212 def filter_creds_by_caller(creds, caller_hrn_list):
213         """
214         Returns a list of creds who's gid caller matches the
215         specified caller hrn
216         """
217         if not isinstance(creds, list): creds = [creds]
218         if not isinstance(caller_hrn_list, list): 
219             caller_hrn_list = [caller_hrn_list]
220         caller_creds = []
221         for cred in creds:
222             try:
223                 tmp_cred = Credential(string=cred)
224                 if tmp_cred.get_gid_caller().get_hrn() in caller_hrn_list:
225                     caller_creds.append(cred)
226             except: pass
227         return caller_creds
228
229 class Credential(object):
230
231     ##
232     # Create a Credential object
233     #
234     # @param create If true, create a blank x509 certificate
235     # @param subject If subject!=None, create an x509 cert with the subject name
236     # @param string If string!=None, load the credential from the string
237     # @param filename If filename!=None, load the credential from the file
238     # FIXME: create and subject are ignored!
239     def __init__(self, create=False, subject=None, string=None, filename=None, cred=None):
240         self.gidCaller = None
241         self.gidObject = None
242         self.expiration = None
243         self.privileges = None
244         self.issuer_privkey = None
245         self.issuer_gid = None
246         self.issuer_pubkey = None
247         self.parent = None
248         self.signature = None
249         self.xml = None
250         self.refid = None
251         self.type = None
252         self.version = None
253
254         if cred:
255             if isinstance(cred, StringTypes):
256                 string = cred
257                 self.type = 'geni_sfa'
258                 self.version = '1.0'
259             elif isinstance(cred, dict):
260                 string = cred['geni_value']
261                 self.type = cred['geni_type']
262                 self.version = cred['geni_version']
263                 
264
265         if string or filename:
266             if string:                
267                 str = string
268             elif filename:
269                 str = file(filename).read()
270                 
271             # if this is a legacy credential, write error and bail out
272             if isinstance (str, StringTypes) and str.strip().startswith("-----"):
273                 logger.error("Legacy credentials not supported any more - giving up with %s..."%str[:10])
274                 return
275             else:
276                 self.xml = str
277                 self.decode()
278
279         # Find an xmlsec1 path
280         self.xmlsec_path = ''
281         paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin']
282         for path in paths:
283             if os.path.isfile(path + '/' + 'xmlsec1'):
284                 self.xmlsec_path = path + '/' + 'xmlsec1'
285                 break
286
287     def get_subject(self):
288         subject = ""
289         if not self.gidObject:
290             self.decode()
291         if self.gidObject:
292             subject = self.gidObject.get_printable_subject()
293         return subject
294
295     # sounds like this should be __repr__ instead ??
296     def pretty_cred(self):
297         if not self.gidObject:
298             self.decode()
299         obj = self.gidObject.get_printable_subject()
300         caller = self.gidCaller.get_printable_subject()
301         exp = self.get_expiration()
302         # Summarize the rights too? The issuer?
303         return "[ Grant {caller} rights on {obj} until {exp} ]".format(**locals())
304
305     def get_signature(self):
306         if not self.signature:
307             self.decode()
308         return self.signature
309
310     def set_signature(self, sig):
311         self.signature = sig
312
313         
314     ##
315     # Need the issuer's private key and name
316     # @param key Keypair object containing the private key of the issuer
317     # @param gid GID of the issuing authority
318
319     def set_issuer_keys(self, privkey, gid):
320         self.issuer_privkey = privkey
321         self.issuer_gid = gid
322
323
324     ##
325     # Set this credential's parent
326     def set_parent(self, cred):
327         self.parent = cred
328         self.updateRefID()
329
330     ##
331     # set the GID of the caller
332     #
333     # @param gid GID object of the caller
334
335     def set_gid_caller(self, gid):
336         self.gidCaller = gid
337         # gid origin caller is the caller's gid by default
338         self.gidOriginCaller = gid
339
340     ##
341     # get the GID of the object
342
343     def get_gid_caller(self):
344         if not self.gidCaller:
345             self.decode()
346         return self.gidCaller
347
348     ##
349     # set the GID of the object
350     #
351     # @param gid GID object of the object
352
353     def set_gid_object(self, gid):
354         self.gidObject = gid
355
356     ##
357     # get the GID of the object
358
359     def get_gid_object(self):
360         if not self.gidObject:
361             self.decode()
362         return self.gidObject
363             
364     ##
365     # Expiration: an absolute UTC time of expiration (as either an int or string or datetime)
366     # 
367     def set_expiration(self, expiration):
368         expiration_datetime = utcparse (expiration)
369         if expiration_datetime is not None:
370             self.expiration = expiration_datetime
371         else:
372             logger.error ("unexpected input %s in Credential.set_expiration"%expiration)
373
374     ##
375     # get the lifetime of the credential (always in datetime format)
376
377     def get_expiration(self):
378         if not self.expiration:
379             self.decode()
380         # at this point self.expiration is normalized as a datetime - DON'T call utcparse again
381         return self.expiration
382
383     ##
384     # set the privileges
385     #
386     # @param privs either a comma-separated list of privileges of a Rights object
387
388     def set_privileges(self, privs):
389         if isinstance(privs, str):
390             self.privileges = Rights(string = privs)
391         else:
392             self.privileges = privs        
393
394     ##
395     # return the privileges as a Rights object
396
397     def get_privileges(self):
398         if not self.privileges:
399             self.decode()
400         return self.privileges
401
402     ##
403     # determine whether the credential allows a particular operation to be
404     # performed
405     #
406     # @param op_name string specifying name of operation ("lookup", "update", etc)
407
408     def can_perform(self, op_name):
409         rights = self.get_privileges()
410         
411         if not rights:
412             return False
413
414         return rights.can_perform(op_name)
415
416
417     ##
418     # Encode the attributes of the credential into an XML string    
419     # This should be done immediately before signing the credential.    
420     # WARNING:
421     # In general, a signed credential obtained externally should
422     # not be changed else the signature is no longer valid.  So, once
423     # you have loaded an existing signed credential, do not call encode() or sign() on it.
424
425     def encode(self):
426         # Create the XML document
427         doc = Document()
428         signed_cred = doc.createElement("signed-credential")
429
430         # Declare namespaces
431         # Note that credential/policy.xsd are really the PG schemas
432         # in a PL namespace.
433         # Note that delegation of credentials between the 2 only really works
434         # cause those schemas are identical.
435         # Also note these PG schemas talk about PG tickets and CM policies.
436         signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
437         signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd")
438         signed_cred.setAttribute("xsi:schemaLocation", "http://www.planet-lab.org/resources/sfa/ext/policy/1 http://www.planet-lab.org/resources/sfa/ext/policy/1/policy.xsd")
439
440         # PG says for those last 2:
441        #signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
442        # signed_cred.setAttribute("xsi:schemaLocation", "http://www.protogeni.net/resources/credential/ext/policy/1 http://www.protogeni.net/resources/credential/ext/policy/1/policy.xsd")
443
444         doc.appendChild(signed_cred)  
445         
446         # Fill in the <credential> bit        
447         cred = doc.createElement("credential")
448         cred.setAttribute("xml:id", self.get_refid())
449         signed_cred.appendChild(cred)
450         append_sub(doc, cred, "type", "privilege")
451         append_sub(doc, cred, "serial", "8")
452         append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
453         append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
454         append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
455         append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
456         append_sub(doc, cred, "uuid", "")
457         if not self.expiration:
458             logger.debug("Creating credential valid for %s s"%DEFAULT_CREDENTIAL_LIFETIME)
459             self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
460         self.expiration = self.expiration.replace(microsecond=0)
461         append_sub(doc, cred, "expires", self.expiration.strftime(SFATIME_FORMAT))
462         privileges = doc.createElement("privileges")
463         cred.appendChild(privileges)
464
465         if self.privileges:
466             rights = self.get_privileges()
467             for right in rights.rights:
468                 priv = doc.createElement("privilege")
469                 append_sub(doc, priv, "name", right.kind)
470                 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
471                 privileges.appendChild(priv)
472
473         # Add the parent credential if it exists
474         if self.parent:
475             sdoc = parseString(self.parent.get_xml())
476             # If the root node is a signed-credential (it should be), then
477             # get all its attributes and attach those to our signed_cred
478             # node.
479             # Specifically, PG and PLadd attributes for namespaces (which is reasonable),
480             # and we need to include those again here or else their signature
481             # no longer matches on the credential.
482             # We expect three of these, but here we copy them all:
483             #  signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
484             # and from PG (PL is equivalent, as shown above):
485             #  signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
486             #  signed_cred.setAttribute("xsi:schemaLocation", "http://www.protogeni.net/resources/credential/ext/policy/1 http://www.protogeni.net/resources/credential/ext/policy/1/policy.xsd")
487
488             # HOWEVER!
489             # PL now also declares these, with different URLs, so
490             # the code notices those attributes already existed with
491             # different values, and complains.
492             # This happens regularly on delegation now that PG and
493             # PL both declare the namespace with different URLs.
494             # If the content ever differs this is a problem,
495             # but for now it works - different URLs (values in the attributes)
496             # but the same actual schema, so using the PG schema
497             # on delegated-to-PL credentials works fine.
498
499             # Note: you could also not copy attributes
500             # which already exist. It appears that both PG and PL
501             # will actually validate a slicecred with a parent
502             # signed using PG namespaces and a child signed with PL
503             # namespaces over the whole thing. But I don't know
504             # if that is a bug in xmlsec1, an accident since
505             # the contents of the schemas are the same,
506             # or something else, but it seems odd. And this works.
507             parentRoot = sdoc.documentElement
508             if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
509                 for attrIx in range(0, parentRoot.attributes.length):
510                     attr = parentRoot.attributes.item(attrIx)
511                     # returns the old attribute of same name that was
512                     # on the credential
513                     # Below throws InUse exception if we forgot to clone the attribute first
514                     oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
515                     if oldAttr and oldAttr.value != attr.value:
516                         msg = "Delegating cred from owner %s to %s over %s:\n - Replaced attribute %s value '%s' with '%s'" % \
517                               (self.parent.gidCaller.get_urn(), self.gidCaller.get_urn(), self.gidObject.get_urn(), oldAttr.name, oldAttr.value, attr.value)
518                         logger.warn(msg)
519                         #raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)
520
521             p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
522             p = doc.createElement("parent")
523             p.appendChild(p_cred)
524             cred.appendChild(p)
525         # done handling parent credential
526
527         # Create the <signatures> tag
528         signatures = doc.createElement("signatures")
529         signed_cred.appendChild(signatures)
530
531         # Add any parent signatures
532         if self.parent:
533             for cur_cred in self.get_credential_list()[1:]:
534                 sdoc = parseString(cur_cred.get_signature().get_xml())
535                 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
536                 signatures.appendChild(ele)
537                 
538         # Get the finished product
539         self.xml = doc.toxml()
540
541
542     def save_to_random_tmp_file(self):       
543         fp, filename = mkstemp(suffix='cred', text=True)
544         fp = os.fdopen(fp, "w")
545         self.save_to_file(filename, save_parents=True, filep=fp)
546         return filename
547     
548     def save_to_file(self, filename, save_parents=True, filep=None):
549         if not self.xml:
550             self.encode()
551         if filep:
552             f = filep 
553         else:
554             f = open(filename, "w")
555         f.write(self.xml)
556         f.close()
557
558     def save_to_string(self, save_parents=True):
559         if not self.xml:
560             self.encode()
561         return self.xml
562
563     def get_refid(self):
564         if not self.refid:
565             self.refid = 'ref0'
566         return self.refid
567
568     def set_refid(self, rid):
569         self.refid = rid
570
571     ##
572     # Figure out what refids exist, and update this credential's id
573     # so that it doesn't clobber the others.  Returns the refids of
574     # the parents.
575     
576     def updateRefID(self):
577         if not self.parent:
578             self.set_refid('ref0')
579             return []
580         
581         refs = []
582
583         next_cred = self.parent
584         while next_cred:
585             refs.append(next_cred.get_refid())
586             if next_cred.parent:
587                 next_cred = next_cred.parent
588             else:
589                 next_cred = None
590
591         
592         # Find a unique refid for this credential
593         rid = self.get_refid()
594         while rid in refs:
595             val = int(rid[3:])
596             rid = "ref%d" % (val + 1)
597
598         # Set the new refid
599         self.set_refid(rid)
600
601         # Return the set of parent credential ref ids
602         return refs
603
604     def get_xml(self):
605         if not self.xml:
606             self.encode()
607         return self.xml
608
609     ##
610     # Sign the XML file created by encode()
611     #
612     # WARNING:
613     # In general, a signed credential obtained externally should
614     # not be changed else the signature is no longer valid.  So, once
615     # you have loaded an existing signed credential, do not call encode() or sign() on it.
616
617     def sign(self):
618         if not self.issuer_privkey or not self.issuer_gid:
619             return
620         doc = parseString(self.get_xml())
621         sigs = doc.getElementsByTagName("signatures")[0]
622
623         # Create the signature template to be signed
624         signature = Signature()
625         signature.set_refid(self.get_refid())
626         sdoc = parseString(signature.get_xml())        
627         sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
628         sigs.appendChild(sig_ele)
629
630         self.xml = doc.toxml()
631
632
633         # Split the issuer GID into multiple certificates if it's a chain
634         chain = GID(filename=self.issuer_gid)
635         gid_files = []
636         while chain:
637             gid_files.append(chain.save_to_random_tmp_file(False))
638             if chain.get_parent():
639                 chain = chain.get_parent()
640             else:
641                 chain = None
642
643
644         # Call out to xmlsec1 to sign it
645         ref = 'Sig_%s' % self.get_refid()
646         filename = self.save_to_random_tmp_file()
647         signed = os.popen('%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
648                  % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)).read()
649         os.remove(filename)
650
651         for gid_file in gid_files:
652             os.remove(gid_file)
653
654         self.xml = signed
655
656         # Update signatures
657         self.decode()       
658
659         
660     ##
661     # Retrieve the attributes of the credential from the XML.
662     # This is automatically called by the various get_* methods of
663     # this class and should not need to be called explicitly.
664
665     def decode(self):
666         if not self.xml:
667             return
668
669         doc = None
670         try:
671             doc = parseString(self.xml)
672         except ExpatError,e:
673             raise CredentialNotVerifiable("Malformed credential")
674         doc = parseString(self.xml)
675         sigs = []
676         signed_cred = doc.getElementsByTagName("signed-credential")
677
678         # Is this a signed-cred or just a cred?
679         if len(signed_cred) > 0:
680             creds = signed_cred[0].getElementsByTagName("credential")
681             signatures = signed_cred[0].getElementsByTagName("signatures")
682             if len(signatures) > 0:
683                 sigs = signatures[0].getElementsByTagName("Signature")
684         else:
685             creds = doc.getElementsByTagName("credential")
686         
687         if creds is None or len(creds) == 0:
688             # malformed cred file
689             raise CredentialNotVerifiable("Malformed XML: No credential tag found")
690
691         # Just take the first cred if there are more than one
692         cred = creds[0]
693
694         self.set_refid(cred.getAttribute("xml:id"))
695         self.set_expiration(utcparse(getTextNode(cred, "expires")))
696         self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
697         self.gidObject = GID(string=getTextNode(cred, "target_gid"))   
698
699
700         # Process privileges
701         privs = cred.getElementsByTagName("privileges")[0]
702         rlist = Rights()
703         for priv in privs.getElementsByTagName("privilege"):
704             kind = getTextNode(priv, "name")
705             deleg = str2bool(getTextNode(priv, "can_delegate"))
706             if kind == '*':
707                 # Convert * into the default privileges for the credential's type
708                 # Each inherits the delegatability from the * above
709                 _ , type = urn_to_hrn(self.gidObject.get_urn())
710                 rl = determine_rights(type, self.gidObject.get_urn())
711                 for r in rl.rights:
712                     r.delegate = deleg
713                     rlist.add(r)
714             else:
715                 rlist.add(Right(kind.strip(), deleg))
716         self.set_privileges(rlist)
717
718
719         # Is there a parent?
720         parent = cred.getElementsByTagName("parent")
721         if len(parent) > 0:
722             parent_doc = parent[0].getElementsByTagName("credential")[0]
723             parent_xml = parent_doc.toxml()
724             self.parent = Credential(string=parent_xml)
725             self.updateRefID()
726
727         # Assign the signatures to the credentials
728         for sig in sigs:
729             Sig = Signature(string=sig.toxml())
730
731             for cur_cred in self.get_credential_list():
732                 if cur_cred.get_refid() == Sig.get_refid():
733                     cur_cred.set_signature(Sig)
734                                     
735             
736     ##
737     # Verify
738     #   trusted_certs: A list of trusted GID filenames (not GID objects!) 
739     #                  Chaining is not supported within the GIDs by xmlsec1.
740     #
741     #   trusted_certs_required: Should usually be true. Set False means an
742     #                 empty list of trusted_certs would still let this method pass.
743     #                 It just skips xmlsec1 verification et al. Only used by some utils
744     #    
745     # Verify that:
746     # . All of the signatures are valid and that the issuers trace back
747     #   to trusted roots (performed by xmlsec1)
748     # . The XML matches the credential schema
749     # . That the issuer of the credential is the authority in the target's urn
750     #    . In the case of a delegated credential, this must be true of the root
751     # . That all of the gids presented in the credential are valid
752     #    . Including verifying GID chains, and includ the issuer
753     # . The credential is not expired
754     #
755     # -- For Delegates (credentials with parents)
756     # . The privileges must be a subset of the parent credentials
757     # . The privileges must have "can_delegate" set for each delegated privilege
758     # . The target gid must be the same between child and parents
759     # . The expiry time on the child must be no later than the parent
760     # . The signer of the child must be the owner of the parent
761     #
762     # -- Verify does *NOT*
763     # . ensure that an xmlrpc client's gid matches a credential gid, that
764     #   must be done elsewhere
765     #
766     # @param trusted_certs: The certificates of trusted CA certificates
767     def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
768         if not self.xml:
769             self.decode()
770
771         # validate against RelaxNG schema
772         if HAVELXML:
773             if schema and os.path.exists(schema):
774                 tree = etree.parse(StringIO(self.xml))
775                 schema_doc = etree.parse(schema)
776                 xmlschema = etree.XMLSchema(schema_doc)
777                 if not xmlschema.validate(tree):
778                     error = xmlschema.error_log.last_error
779                     message = "%s: %s (line %s)" % (self.pretty_cred(), error.message, error.line)
780                     raise CredentialNotVerifiable(message)
781
782         if trusted_certs_required and trusted_certs is None:
783             trusted_certs = []
784
785 #        trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
786         trusted_cert_objects = []
787         ok_trusted_certs = []
788         # If caller explicitly passed in None that means skip cert chain validation.
789         # Strange and not typical
790         if trusted_certs is not None:
791             for f in trusted_certs:
792                 try:
793                     # Failures here include unreadable files
794                     # or non PEM files
795                     trusted_cert_objects.append(GID(filename=f))
796                     ok_trusted_certs.append(f)
797                 except Exception, exc:
798                     logger.error("Failed to load trusted cert from %s: %r"%( f, exc))
799             trusted_certs = ok_trusted_certs
800
801         # make sure it is not expired
802         if self.get_expiration() < datetime.datetime.utcnow():
803             raise CredentialNotVerifiable("Credential %s expired at %s" % \
804                                           (self.pretty_cred(),
805                                            self.expiration.strftime(SFATIME_FORMAT)))
806
807         # Verify the signatures
808         filename = self.save_to_random_tmp_file()
809
810         # If caller explicitly passed in None that means skip cert chain validation.
811         # - Strange and not typical
812         if trusted_certs is not None:
813             # Verify the gids of this cred and of its parents
814             for cur_cred in self.get_credential_list():
815                 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
816                 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
817
818         refs = []
819         refs.append("Sig_%s" % self.get_refid())
820
821         parentRefs = self.updateRefID()
822         for ref in parentRefs:
823             refs.append("Sig_%s" % ref)
824
825         for ref in refs:
826             # If caller explicitly passed in None that means skip xmlsec1 validation.
827             # Strange and not typical
828             if trusted_certs is None:
829                 break
830
831             # Thierry - jan 2015
832             # up to fedora20 we used os.popen and checked that the output begins with OK
833             # turns out, with fedora21, there is extra input before this 'OK' thing
834             # looks like we're better off just using the exit code - that's what it is made for
835             #cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
836             #command = '{} --verify --node-id "{}" {} {} 2>&1'.\
837             #          format(self.xmlsec_path, ref, cert_args, filename)
838             command = [ self.xmlsec_path, '--verify', '--node-id', ref ]
839             for trusted in trusted_certs:
840                 command += ["--trusted-pem", trusted ]
841             command += [ filename ]
842             logger.debug("Running " + " ".join(command))
843             try:
844                 verified = subprocess.check_output(command, stderr=subprocess.STDOUT)
845                 logger.debug("xmlsec command returned {}".format(verified))
846                 if "OK\n" not in verified:
847                     logger.warning("WARNING: xmlsec1 seemed to return fine but without a OK in its output")
848             except subprocess.CalledProcessError as e:
849                 verified = e.output
850                 # xmlsec errors have a msg= which is the interesting bit.
851                 mstart = verified.find("msg=")
852                 msg = ""
853                 if mstart > -1 and len(verified) > 4:
854                     mstart = mstart + 4
855                     mend = verified.find('\\', mstart)
856                     msg = verified[mstart:mend]
857                 logger.warning("Credential.verify - failed - xmlsec1 returned {}".format(verified.strip()))
858                 raise CredentialNotVerifiable("xmlsec1 error verifying cred %s using Signature ID %s: %s" % \
859                                               (self.pretty_cred(), ref, msg))
860         os.remove(filename)
861
862         # Verify the parents (delegation)
863         if self.parent:
864             self.verify_parent(self.parent)
865
866         # Make sure the issuer is the target's authority, and is
867         # itself a valid GID
868         self.verify_issuer(trusted_cert_objects)
869         return True
870
871     ##
872     # Creates a list of the credential and its parents, with the root 
873     # (original delegated credential) as the last item in the list
874     def get_credential_list(self):    
875         cur_cred = self
876         list = []
877         while cur_cred:
878             list.append(cur_cred)
879             if cur_cred.parent:
880                 cur_cred = cur_cred.parent
881             else:
882                 cur_cred = None
883         return list
884     
885     ##
886     # Make sure the credential's target gid (a) was signed by or (b)
887     # is the same as the entity that signed the original credential,
888     # or (c) is an authority over the target's namespace.
889     # Also ensure that the credential issuer / signer itself has a valid
890     # GID signature chain (signed by an authority with namespace rights).
891     def verify_issuer(self, trusted_gids):
892         root_cred = self.get_credential_list()[-1]
893         root_target_gid = root_cred.get_gid_object()
894         root_cred_signer = root_cred.get_signature().get_issuer_gid()
895
896         # Case 1:
897         # Allow non authority to sign target and cred about target.
898         #
899         # Why do we need to allow non authorities to sign?
900         # If in the target gid validation step we correctly
901         # checked that the target is only signed by an authority,
902         # then this is just a special case of case 3.
903         # This short-circuit is the common case currently -
904         # and cause GID validation doesn't check 'authority',
905         # this allows users to generate valid slice credentials.
906         if root_target_gid.is_signed_by_cert(root_cred_signer):
907             # cred signer matches target signer, return success
908             return
909
910         # Case 2:
911         # Allow someone to sign credential about themeselves. Used?
912         # If not, remove this.
913         #root_target_gid_str = root_target_gid.save_to_string()
914         #root_cred_signer_str = root_cred_signer.save_to_string()
915         #if root_target_gid_str == root_cred_signer_str:
916         #    # cred signer is target, return success
917         #    return
918
919         # Case 3:
920
921         # root_cred_signer is not the target_gid
922         # So this is a different gid that we have not verified.
923         # xmlsec1 verified the cert chain on this already, but
924         # it hasn't verified that the gid meets the HRN namespace
925         # requirements.
926         # Below we'll ensure that it is an authority.
927         # But we haven't verified that it is _signed by_ an authority
928         # We also don't know if xmlsec1 requires that cert signers
929         # are marked as CAs.
930
931         # Note that if verify() gave us no trusted_gids then this
932         # call will fail. So skip it if we have no trusted_gids
933         if trusted_gids and len(trusted_gids) > 0:
934             root_cred_signer.verify_chain(trusted_gids)
935         else:
936             logger.debug("No trusted gids. Cannot verify that cred signer is signed by a trusted authority. Skipping that check.")
937
938         # See if the signer is an authority over the domain of the target.
939         # There are multiple types of authority - accept them all here
940         # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
941         root_cred_signer_type = root_cred_signer.get_type()
942         if (root_cred_signer_type.find('authority') == 0):
943             #logger.debug('Cred signer is an authority')
944             # signer is an authority, see if target is in authority's domain
945             signerhrn = root_cred_signer.get_hrn()
946             if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
947                 return
948
949         # We've required that the credential be signed by an authority
950         # for that domain. Reasonable and probably correct.
951         # A looser model would also allow the signer to be an authority
952         # in my control framework - eg My CA or CH. Even if it is not
953         # the CH that issued these, eg, user credentials.
954
955         # Give up, credential does not pass issuer verification
956
957         raise CredentialNotVerifiable("Could not verify credential owned by %s for object %s. Cred signer %s not the trusted authority for Cred target %s" % \
958                                       (self.gidCaller.get_urn(), self.gidObject.get_urn(), root_cred_signer.get_hrn(), root_target_gid.get_hrn()))
959
960
961     ##
962     # -- For Delegates (credentials with parents) verify that:
963     # . The privileges must be a subset of the parent credentials
964     # . The privileges must have "can_delegate" set for each delegated privilege
965     # . The target gid must be the same between child and parents
966     # . The expiry time on the child must be no later than the parent
967     # . The signer of the child must be the owner of the parent        
968     def verify_parent(self, parent_cred):
969         # make sure the rights given to the child are a subset of the
970         # parents rights (and check delegate bits)
971         if not parent_cred.get_privileges().is_superset(self.get_privileges()):
972             raise ChildRightsNotSubsetOfParent(("Parent cred ref %s rights " % parent_cred.get_refid()) +
973                 self.parent.get_privileges().save_to_string() + (" not superset of delegated cred %s ref %s rights " % \
974                                                                  (self.get_summary_tostring(), self.get_refid())) +
975                 self.get_privileges().save_to_string())
976
977         # make sure my target gid is the same as the parent's
978         if not parent_cred.get_gid_object().save_to_string() == \
979            self.get_gid_object().save_to_string():
980             raise CredentialNotVerifiable("Delegated cred %s: Target gid not equal between parent and child. Parent %s" % \
981                                           (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
982
983         # make sure my expiry time is <= my parent's
984         if not parent_cred.get_expiration() >= self.get_expiration():
985             raise CredentialNotVerifiable("Delegated credential %s expires after parent %s" % \
986                                           (self.pretty_cred(), parent_cred.pretty_cred()))
987
988         # make sure my signer is the parent's caller
989         if not parent_cred.get_gid_caller().save_to_string(False) == \
990            self.get_signature().get_issuer_gid().save_to_string(False):
991             raise CredentialNotVerifiable("Delegated credential %s not signed by parent %s's caller" % \
992                                           (self.pretty_cred(), parent_cred.pretty_cred()))
993                 
994         # Recurse
995         if parent_cred.parent:
996             parent_cred.verify_parent(parent_cred.parent)
997
998
999     def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
1000         """
1001         Return a delegated copy of this credential, delegated to the 
1002         specified gid's user.    
1003         """
1004         # get the gid of the object we are delegating
1005         object_gid = self.get_gid_object()
1006         object_hrn = object_gid.get_hrn()        
1007  
1008         # the hrn of the user who will be delegated to
1009         delegee_gid = GID(filename=delegee_gidfile)
1010         delegee_hrn = delegee_gid.get_hrn()
1011   
1012         #user_key = Keypair(filename=keyfile)
1013         #user_hrn = self.get_gid_caller().get_hrn()
1014         subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
1015         dcred = Credential(subject=subject_string)
1016         dcred.set_gid_caller(delegee_gid)
1017         dcred.set_gid_object(object_gid)
1018         dcred.set_parent(self)
1019         dcred.set_expiration(self.get_expiration())
1020         dcred.set_privileges(self.get_privileges())
1021         dcred.get_privileges().delegate_all_privileges(True)
1022         #dcred.set_issuer_keys(keyfile, delegee_gidfile)
1023         dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1024         dcred.encode()
1025         dcred.sign()
1026
1027         return dcred
1028
1029     # only informative
1030     def get_filename(self):
1031         return getattr(self,'filename',None)
1032     
1033     def actual_caller_hrn (self):
1034         """a helper method used by some API calls like e.g. Allocate
1035         to try and find out who really is the original caller
1036         
1037         This admittedly is a bit of a hack, please USE IN LAST RESORT
1038         
1039         This code uses a heuristic to identify a delegated credential
1040
1041         A first known restriction if for traffic that gets through a slice manager
1042         in this case the hrn reported is the one from the last SM in the call graph
1043         which is not at all what is meant here"""
1044
1045         caller_hrn = self.get_gid_caller().get_hrn()
1046         issuer_hrn = self.get_signature().get_issuer_gid().get_hrn()
1047         subject_hrn = self.get_gid_object().get_hrn()
1048         # if we find that the caller_hrn is an immediate descendant of the issuer, then
1049         # this seems to be a 'regular' credential
1050         if caller_hrn.startswith(issuer_hrn): 
1051             actual_caller_hrn=caller_hrn
1052         # else this looks like a delegated credential, and the real caller is the issuer
1053         else:
1054             actual_caller_hrn=issuer_hrn
1055         logger.info("actual_caller_hrn: caller_hrn=%s, issuer_hrn=%s, returning %s"
1056                     %(caller_hrn,issuer_hrn,actual_caller_hrn))
1057         return actual_caller_hrn
1058             
1059     ##
1060     # Dump the contents of a credential to stdout in human-readable format
1061     #
1062     # @param dump_parents If true, also dump the parent certificates
1063     def dump (self, *args, **kwargs):
1064         print self.dump_string(*args, **kwargs)
1065
1066     # show_xml is ignored
1067     def dump_string(self, dump_parents=False, show_xml=None):
1068         result=""
1069         result += "CREDENTIAL %s\n" % self.get_subject()
1070         filename=self.get_filename()
1071         if filename: result += "Filename %s\n"%filename
1072         privileges = self.get_privileges()
1073         if privileges:
1074             result += "      privs: %s\n" % privileges.save_to_string()
1075         else:
1076             result += "      privs: \n" 
1077         gidCaller = self.get_gid_caller()
1078         if gidCaller:
1079             result += "  gidCaller:\n"
1080             result += gidCaller.dump_string(8, dump_parents)
1081
1082         if self.get_signature():
1083             print "  gidIssuer:"
1084             self.get_signature().get_issuer_gid().dump(8, dump_parents)
1085
1086         if self.expiration:
1087             print "  expiration:", self.expiration.strftime(SFATIME_FORMAT)
1088
1089         gidObject = self.get_gid_object()
1090         if gidObject:
1091             result += "  gidObject:\n"
1092             result += gidObject.dump_string(8, dump_parents)
1093
1094         if self.parent and dump_parents:
1095             result += "\nPARENT"
1096             result += self.parent.dump_string(True)
1097
1098         return result