python3 - 2to3 + miscell obvious tweaks
[sfa.git] / sfa / trust / abac_credential.py
1 #----------------------------------------------------------------------
2 # Copyright (c) 2014 Raytheon BBN Technologies
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
25
26 from sfa.trust.credential import Credential, append_sub, DEFAULT_CREDENTIAL_LIFETIME
27 from sfa.util.sfalogging import logger
28 from sfa.util.sfatime import SFATIME_FORMAT
29
30 from xml.dom.minidom import Document, parseString
31
32 from sfa.util.py23 import StringIO
33
34 HAVELXML = False
35 try:
36     from lxml import etree
37     HAVELXML = True
38 except:
39     pass
40
41 # This module defines a subtype of sfa.trust,credential.Credential
42 # called an ABACCredential. An ABAC credential is a signed statement
43 # asserting a role representing the relationship between a subject and target
44 # or between a subject and a class of targets (all those satisfying a role).
45 #
46 # An ABAC credential is like a normal SFA credential in that it has
47 # a validated signature block and is checked for expiration.
48 # It does not, however, have 'privileges'. Rather it contains a 'head' and
49 # list of 'tails' of elements, each of which represents a principal and
50 # role.
51
52 # A special case of an ABAC credential is a speaks_for credential. Such
53 # a credential is simply an ABAC credential in form, but has a single
54 # tail and fixed role 'speaks_for'. In ABAC notation, it asserts
55 # AGENT.speaks_for(AGENT)<-CLIENT, or "AGENT asserts that CLIENT may speak
56 # for AGENT". The AGENT in this case is the head and the CLIENT is the
57 # tail and 'speaks_for_AGENT' is the role on the head. These speaks-for
58 # Credentials are used to allow a tool to 'speak as' itself but be recognized
59 # as speaking for an individual and be authorized to the rights of that
60 # individual and not to the rights of the tool itself.
61
62 # For more detail on the semantics and syntax and expected usage patterns
63 # of ABAC credentials, see http://groups.geni.net/geni/wiki/TIEDABACCredential.
64
65
66 # An ABAC element contains a principal (keyid and optional mnemonic)
67 # and optional role and linking_role element
68 class ABACElement:
69
70     def __init__(self, principal_keyid, principal_mnemonic=None,
71                  role=None, linking_role=None):
72         self._principal_keyid = principal_keyid
73         self._principal_mnemonic = principal_mnemonic
74         self._role = role
75         self._linking_role = linking_role
76
77     def get_principal_keyid(self): return self._principal_keyid
78
79     def get_principal_mnemonic(self): return self._principal_mnemonic
80
81     def get_role(self): return self._role
82
83     def get_linking_role(self): return self._linking_role
84
85     def __str__(self):
86         ret = self._principal_keyid
87         if self._principal_mnemonic:
88             ret = "%s (%s)" % (self._principal_mnemonic, self._principal_keyid)
89         if self._linking_role:
90             ret += ".%s" % self._linking_role
91         if self._role:
92             ret += ".%s" % self._role
93         return ret
94
95 # Subclass of Credential for handling ABAC credentials
96 # They have a different cred_type (geni_abac vs. geni_sfa)
97 # and they have a head and tail and role (as opposed to privileges)
98
99
100 class ABACCredential(Credential):
101
102     ABAC_CREDENTIAL_TYPE = 'geni_abac'
103
104     def __init__(self, create=False, subject=None,
105                  string=None, filename=None):
106         self.head = None  # An ABACElemenet
107         self.tails = []  # List of ABACElements
108         super(ABACCredential, self).__init__(create=create,
109                                              subject=subject,
110                                              string=string,
111                                              filename=filename)
112         self.cred_type = ABACCredential.ABAC_CREDENTIAL_TYPE
113
114     def get_head(self):
115         if not self.head:
116             self.decode()
117         return self.head
118
119     def get_tails(self):
120         if len(self.tails) == 0:
121             self.decode()
122         return self.tails
123
124     def decode(self):
125         super(ABACCredential, self).decode()
126         # Pull out the ABAC-specific info
127         doc = parseString(self.xml)
128         rt0s = doc.getElementsByTagName('rt0')
129         if len(rt0s) != 1:
130             raise CredentialNotVerifiable("ABAC credential had no rt0 element")
131         rt0_root = rt0s[0]
132         heads = self._get_abac_elements(rt0_root, 'head')
133         if len(heads) != 1:
134             raise CredentialNotVerifiable(
135                 "ABAC credential should have exactly 1 head element, had %d" % len(heads))
136
137         self.head = heads[0]
138         self.tails = self._get_abac_elements(rt0_root, 'tail')
139
140     def _get_abac_elements(self, root, label):
141         abac_elements = []
142         elements = root.getElementsByTagName(label)
143         for elt in elements:
144             keyids = elt.getElementsByTagName('keyid')
145             if len(keyids) != 1:
146                 raise CredentialNotVerifiable(
147                     "ABAC credential element '%s' should have exactly 1 keyid, had %d." % (label, len(keyids)))
148             keyid_elt = keyids[0]
149             keyid = keyid_elt.childNodes[0].nodeValue.strip()
150
151             mnemonic = None
152             mnemonic_elts = elt.getElementsByTagName('mnemonic')
153             if len(mnemonic_elts) > 0:
154                 mnemonic = mnemonic_elts[0].childNodes[0].nodeValue.strip()
155
156             role = None
157             role_elts = elt.getElementsByTagName('role')
158             if len(role_elts) > 0:
159                 role = role_elts[0].childNodes[0].nodeValue.strip()
160
161             linking_role = None
162             linking_role_elts = elt.getElementsByTagName('linking_role')
163             if len(linking_role_elts) > 0:
164                 linking_role = linking_role_elts[
165                     0].childNodes[0].nodeValue.strip()
166
167             abac_element = ABACElement(keyid, mnemonic, role, linking_role)
168             abac_elements.append(abac_element)
169
170         return abac_elements
171
172     def dump_string(self, dump_parents=False, show_xml=False):
173         result = "ABAC Credential\n"
174         filename = self.get_filename()
175         if filename:
176             result += "Filename %s\n" % filename
177         if self.expiration:
178             result += "\texpiration: %s \n" % self.expiration.strftime(
179                 SFATIME_FORMAT)
180
181         result += "\tHead: %s\n" % self.get_head()
182         for tail in self.get_tails():
183             result += "\tTail: %s\n" % tail
184         if self.get_signature():
185             result += "  gidIssuer:\n"
186             result += self.get_signature().get_issuer_gid().dump_string(8, dump_parents)
187         if show_xml and HAVELXML:
188             try:
189                 tree = etree.parse(StringIO(self.xml))
190                 aside = etree.tostring(tree, pretty_print=True)
191                 result += "\nXML:\n\n"
192                 result += aside
193                 result += "\nEnd XML\n"
194             except:
195                 import traceback
196                 print("exc. Credential.dump_string / XML")
197                 traceback.print_exc()
198         return result
199
200     # sounds like this should be __repr__ instead ??
201     # Produce the ABAC assertion. Something like [ABAC cred: Me.role<-You] or
202     # similar
203     def pretty_cred(self):
204         result = "[ABAC cred: " + str(self.get_head())
205         for tail in self.get_tails():
206             result += "<-%s" % str(tail)
207         result += "]"
208         return result
209
210     def createABACElement(self, doc, tagName, abacObj):
211         kid = abacObj.get_principal_keyid()
212         mnem = abacObj.get_principal_mnemonic()  # may be None
213         role = abacObj.get_role()  # may be None
214         link = abacObj.get_linking_role()  # may be None
215         ele = doc.createElement(tagName)
216         prin = doc.createElement('ABACprincipal')
217         ele.appendChild(prin)
218         append_sub(doc, prin, "keyid", kid)
219         if mnem:
220             append_sub(doc, prin, "mnemonic", mnem)
221         if role:
222             append_sub(doc, ele, "role", role)
223         if link:
224             append_sub(doc, ele, "linking_role", link)
225         return ele
226
227     ##
228     # Encode the attributes of the credential into an XML string
229     # This should be done immediately before signing the credential.
230     # WARNING:
231     # In general, a signed credential obtained externally should
232     # not be changed else the signature is no longer valid.  So, once
233     # you have loaded an existing signed credential, do not call encode() or
234     # sign() on it.
235
236     def encode(self):
237         # Create the XML document
238         doc = Document()
239         signed_cred = doc.createElement("signed-credential")
240
241 # Declare namespaces
242 # Note that credential/policy.xsd are really the PG schemas
243 # in a PL namespace.
244 # Note that delegation of credentials between the 2 only really works
245 # cause those schemas are identical.
246 # Also note these PG schemas talk about PG tickets and CM policies.
247         signed_cred.setAttribute(
248             "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
249         signed_cred.setAttribute("xsi:noNamespaceSchemaLocation",
250                                  "http://www.geni.net/resources/credential/2/credential.xsd")
251         signed_cred.setAttribute(
252             "xsi:schemaLocation", "http://www.planet-lab.org/resources/sfa/ext/policy/1 http://www.planet-lab.org/resources/sfa/ext/policy/1/policy.xsd")
253
254 # PG says for those last 2:
255 #        signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
256 #        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")
257
258         doc.appendChild(signed_cred)
259
260         # Fill in the <credential> bit
261         cred = doc.createElement("credential")
262         cred.setAttribute("xml:id", self.get_refid())
263         signed_cred.appendChild(cred)
264         append_sub(doc, cred, "type", "abac")
265
266         # Stub fields
267         append_sub(doc, cred, "serial", "8")
268         append_sub(doc, cred, "owner_gid", '')
269         append_sub(doc, cred, "owner_urn", '')
270         append_sub(doc, cred, "target_gid", '')
271         append_sub(doc, cred, "target_urn", '')
272         append_sub(doc, cred, "uuid", "")
273
274         if not self.expiration:
275             self.set_expiration(datetime.datetime.utcnow(
276             ) + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
277         self.expiration = self.expiration.replace(microsecond=0)
278         if self.expiration.tzinfo is not None and self.expiration.tzinfo.utcoffset(self.expiration) is not None:
279             # TZ aware. Make sure it is UTC
280             self.expiration = self.expiration.astimezone(tz.tzutc())
281         append_sub(doc, cred, "expires", self.expiration.strftime(
282             SFATIME_FORMAT))  # RFC3339
283
284         abac = doc.createElement("abac")
285         rt0 = doc.createElement("rt0")
286         abac.appendChild(rt0)
287         cred.appendChild(abac)
288         append_sub(doc, rt0, "version", "1.1")
289         head = self.createABACElement(doc, "head", self.get_head())
290         rt0.appendChild(head)
291         for tail in self.get_tails():
292             tailEle = self.createABACElement(doc, "tail", tail)
293             rt0.appendChild(tailEle)
294
295         # Create the <signatures> tag
296         signatures = doc.createElement("signatures")
297         signed_cred.appendChild(signatures)
298
299         # Get the finished product
300         self.xml = doc.toxml("utf-8")