fix speaks for auth
[sfa.git] / sfa / trust / abac_credential.py
diff --git a/sfa/trust/abac_credential.py b/sfa/trust/abac_credential.py
new file mode 100644 (file)
index 0000000..8f957ec
--- /dev/null
@@ -0,0 +1,278 @@
+#----------------------------------------------------------------------\r
+# Copyright (c) 2014 Raytheon BBN Technologies\r
+#\r
+# Permission is hereby granted, free of charge, to any person obtaining\r
+# a copy of this software and/or hardware specification (the "Work") to\r
+# deal in the Work without restriction, including without limitation the\r
+# rights to use, copy, modify, merge, publish, distribute, sublicense,\r
+# and/or sell copies of the Work, and to permit persons to whom the Work\r
+# is furnished to do so, subject to the following conditions:\r
+#\r
+# The above copyright notice and this permission notice shall be\r
+# included in all copies or substantial portions of the Work.\r
+#\r
+# THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS\r
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\r
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\r
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\r
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r
+# OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS\r
+# IN THE WORK.\r
+#----------------------------------------------------------------------\r
+\r
+from sfa.trust.credential import Credential, append_sub\r
+from sfa.util.sfalogging import logger\r
+\r
+from StringIO import StringIO\r
+from xml.dom.minidom import Document, parseString\r
+\r
+HAVELXML = False\r
+try:\r
+    from lxml import etree\r
+    HAVELXML = True\r
+except:\r
+    pass\r
+\r
+# This module defines a subtype of sfa.trust,credential.Credential\r
+# called an ABACCredential. An ABAC credential is a signed statement\r
+# asserting a role representing the relationship between a subject and target\r
+# or between a subject and a class of targets (all those satisfying a role).\r
+#\r
+# An ABAC credential is like a normal SFA credential in that it has\r
+# a validated signature block and is checked for expiration. \r
+# It does not, however, have 'privileges'. Rather it contains a 'head' and\r
+# list of 'tails' of elements, each of which represents a principal and\r
+# role.\r
+\r
+# A special case of an ABAC credential is a speaks_for credential. Such\r
+# a credential is simply an ABAC credential in form, but has a single \r
+# tail and fixed role 'speaks_for'. In ABAC notation, it asserts\r
+# AGENT.speaks_for(AGENT)<-CLIENT, or "AGENT asserts that CLIENT may speak\r
+# for AGENT". The AGENT in this case is the head and the CLIENT is the\r
+# tail and 'speaks_for_AGENT' is the role on the head. These speaks-for\r
+# Credentials are used to allow a tool to 'speak as' itself but be recognized\r
+# as speaking for an individual and be authorized to the rights of that\r
+# individual and not to the rights of the tool itself.\r
+\r
+# For more detail on the semantics and syntax and expected usage patterns\r
+# of ABAC credentials, see http://groups.geni.net/geni/wiki/TIEDABACCredential.\r
+\r
+\r
+# An ABAC element contains a principal (keyid and optional mnemonic)\r
+# and optional role and linking_role element\r
+class ABACElement:\r
+    def __init__(self, principal_keyid, principal_mnemonic=None, \\r
+                     role=None, linking_role=None):\r
+        self._principal_keyid = principal_keyid\r
+        self._principal_mnemonic = principal_mnemonic\r
+        self._role = role\r
+        self._linking_role = linking_role\r
+\r
+    def get_principal_keyid(self): return self._principal_keyid\r
+    def get_principal_mnemonic(self): return self._principal_mnemonic\r
+    def get_role(self): return self._role\r
+    def get_linking_role(self): return self._linking_role\r
+\r
+    def __str__(self):\r
+        ret = self._principal_keyid\r
+        if self._principal_mnemonic:\r
+            ret = "%s (%s)" % (self._principal_mnemonic, self._principal_keyid)\r
+        if self._linking_role:\r
+            ret += ".%s" % self._linking_role\r
+        if self._role:\r
+            ret += ".%s" % self._role\r
+        return ret\r
+\r
+# Subclass of Credential for handling ABAC credentials\r
+# They have a different cred_type (geni_abac vs. geni_sfa)\r
+# and they have a head and tail and role (as opposed to privileges)\r
+class ABACCredential(Credential):\r
+\r
+    ABAC_CREDENTIAL_TYPE = 'geni_abac'\r
+\r
+    def __init__(self, create=False, subject=None, \r
+                 string=None, filename=None):\r
+        self.head = None # An ABACElemenet\r
+        self.tails = [] # List of ABACElements\r
+        super(ABACCredential, self).__init__(create=create, \r
+                                             subject=subject, \r
+                                             string=string, \r
+                                             filename=filename)\r
+        self.cred_type = ABACCredential.ABAC_CREDENTIAL_TYPE\r
+\r
+    def get_head(self) : \r
+        if not self.head: \r
+            self.decode()\r
+        return self.head\r
+\r
+    def get_tails(self) : \r
+        if len(self.tails) == 0:\r
+            self.decode()\r
+        return self.tails\r
+\r
+    def decode(self):\r
+        super(ABACCredential, self).decode()\r
+        # Pull out the ABAC-specific info\r
+        doc = parseString(self.xml)\r
+        rt0s = doc.getElementsByTagName('rt0')\r
+        if len(rt0s) != 1:\r
+            raise CredentialNotVerifiable("ABAC credential had no rt0 element")\r
+        rt0_root = rt0s[0]\r
+        heads = self._get_abac_elements(rt0_root, 'head')\r
+        if len(heads) != 1:\r
+            raise CredentialNotVerifiable("ABAC credential should have exactly 1 head element, had %d" % len(heads))\r
+\r
+        self.head = heads[0]\r
+        self.tails = self._get_abac_elements(rt0_root, 'tail')\r
+\r
+    def _get_abac_elements(self, root, label):\r
+        abac_elements = []\r
+        elements = root.getElementsByTagName(label)\r
+        for elt in elements:\r
+            keyids = elt.getElementsByTagName('keyid')\r
+            if len(keyids) != 1:\r
+                raise CredentialNotVerifiable("ABAC credential element '%s' should have exactly 1 keyid, had %d." % (label, len(keyids)))\r
+            keyid_elt = keyids[0]\r
+            keyid = keyid_elt.childNodes[0].nodeValue.strip()\r
+\r
+            mnemonic = None\r
+            mnemonic_elts = elt.getElementsByTagName('mnemonic')\r
+            if len(mnemonic_elts) > 0:\r
+                mnemonic = mnemonic_elts[0].childNodes[0].nodeValue.strip()\r
+\r
+            role = None\r
+            role_elts = elt.getElementsByTagName('role')\r
+            if len(role_elts) > 0:\r
+                role = role_elts[0].childNodes[0].nodeValue.strip()\r
+\r
+            linking_role = None\r
+            linking_role_elts = elt.getElementsByTagName('linking_role')\r
+            if len(linking_role_elts) > 0:\r
+                linking_role = linking_role_elts[0].childNodes[0].nodeValue.strip()\r
+\r
+            abac_element = ABACElement(keyid, mnemonic, role, linking_role)\r
+            abac_elements.append(abac_element)\r
+\r
+        return abac_elements\r
+\r
+    def dump_string(self, dump_parents=False, show_xml=False):\r
+        result = "ABAC Credential\n"\r
+        filename=self.get_filename()\r
+        if filename: result += "Filename %s\n"%filename\r
+        if self.expiration:\r
+            result +=  "\texpiration: %s \n" % self.expiration.isoformat()\r
+\r
+        result += "\tHead: %s\n" % self.get_head() \r
+        for tail in self.get_tails():\r
+            result += "\tTail: %s\n" % tail\r
+        if self.get_signature():\r
+            result += "  gidIssuer:\n"\r
+            result += self.get_signature().get_issuer_gid().dump_string(8, dump_parents)\r
+        if show_xml and HAVELXML:\r
+            try:\r
+                tree = etree.parse(StringIO(self.xml))\r
+                aside = etree.tostring(tree, pretty_print=True)\r
+                result += "\nXML:\n\n"\r
+                result += aside\r
+                result += "\nEnd XML\n"\r
+            except:\r
+                import traceback\r
+                print "exc. Credential.dump_string / XML"\r
+                traceback.print_exc()\r
+        return result\r
+\r
+    # sounds like this should be __repr__ instead ??\r
+    # Produce the ABAC assertion. Something like [ABAC cred: Me.role<-You] or similar\r
+    def get_summary_tostring(self):\r
+        result = "[ABAC cred: " + str(self.get_head())\r
+        for tail in self.get_tails():\r
+            result += "<-%s" % str(tail)\r
+        result += "]"\r
+        return result\r
+\r
+    def createABACElement(self, doc, tagName, abacObj):\r
+        kid = abacObj.get_principal_keyid()\r
+        mnem = abacObj.get_principal_mnemonic() # may be None\r
+        role = abacObj.get_role() # may be None\r
+        link = abacObj.get_linking_role() # may be None\r
+        ele = doc.createElement(tagName)\r
+        prin = doc.createElement('ABACprincipal')\r
+        ele.appendChild(prin)\r
+        append_sub(doc, prin, "keyid", kid)\r
+        if mnem:\r
+            append_sub(doc, prin, "mnemonic", mnem)\r
+        if role:\r
+            append_sub(doc, ele, "role", role)\r
+        if link:\r
+            append_sub(doc, ele, "linking_role", link)\r
+        return ele\r
+\r
+    ##\r
+    # Encode the attributes of the credential into an XML string\r
+    # This should be done immediately before signing the credential.\r
+    # WARNING:\r
+    # In general, a signed credential obtained externally should\r
+    # not be changed else the signature is no longer valid.  So, once\r
+    # you have loaded an existing signed credential, do not call encode() or sign() on it.\r
+\r
+    def encode(self):\r
+        # Create the XML document\r
+        doc = Document()\r
+        signed_cred = doc.createElement("signed-credential")\r
+\r
+# Declare namespaces\r
+# Note that credential/policy.xsd are really the PG schemas\r
+# in a PL namespace.\r
+# Note that delegation of credentials between the 2 only really works\r
+# cause those schemas are identical.\r
+# Also note these PG schemas talk about PG tickets and CM policies.\r
+        signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")\r
+        signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.geni.net/resources/credential/2/credential.xsd")\r
+        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
+\r
+# PG says for those last 2:\r
+#        signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")\r
+#        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
+\r
+        doc.appendChild(signed_cred)\r
+\r
+        # Fill in the <credential> bit\r
+        cred = doc.createElement("credential")\r
+        cred.setAttribute("xml:id", self.get_refid())\r
+        signed_cred.appendChild(cred)\r
+        append_sub(doc, cred, "type", "abac")\r
+\r
+        # Stub fields\r
+        append_sub(doc, cred, "serial", "8")\r
+        append_sub(doc, cred, "owner_gid", '')\r
+        append_sub(doc, cred, "owner_urn", '')\r
+        append_sub(doc, cred, "target_gid", '')\r
+        append_sub(doc, cred, "target_urn", '')\r
+        append_sub(doc, cred, "uuid", "")\r
+\r
+        if not self.expiration:\r
+            self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))\r
+        self.expiration = self.expiration.replace(microsecond=0)\r
+        if self.expiration.tzinfo is not None and self.expiration.tzinfo.utcoffset(self.expiration) is not None:\r
+            # TZ aware. Make sure it is UTC\r
+            self.expiration = self.expiration.astimezone(tz.tzutc())\r
+        append_sub(doc, cred, "expires", self.expiration.strftime('%Y-%m-%dT%H:%M:%SZ')) # RFC3339\r
+\r
+        abac = doc.createElement("abac")\r
+        rt0 = doc.createElement("rt0")\r
+        abac.appendChild(rt0)\r
+        cred.appendChild(abac)\r
+        append_sub(doc, rt0, "version", "1.1")\r
+        head = self.createABACElement(doc, "head", self.get_head())\r
+        rt0.appendChild(head)\r
+        for tail in self.get_tails():\r
+            tailEle = self.createABACElement(doc, "tail", tail)\r
+            rt0.appendChild(tailEle)\r
+\r
+        # Create the <signatures> tag\r
+        signatures = doc.createElement("signatures")\r
+        signed_cred.appendChild(signatures)\r
+\r
+        # Get the finished product\r
+        self.xml = doc.toxml("utf-8")\r