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