1 #----------------------------------------------------------------------
\r
2 # Copyright (c) 2014 Raytheon BBN Technologies
\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
11 # The above copyright notice and this permission notice shall be
\r
12 # included in all copies or substantial portions of the Work.
\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
22 #----------------------------------------------------------------------
\r
24 from sfa.trust.credential import Credential, append_sub
\r
25 from sfa.util.sfalogging import logger
\r
27 from StringIO import StringIO
\r
28 from xml.dom.minidom import Document, parseString
\r
32 from lxml import etree
\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
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
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
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
62 # An ABAC element contains a principal (keyid and optional mnemonic)
\r
63 # and optional role and linking_role element
\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
70 self._linking_role = linking_role
\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
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
84 ret += ".%s" % self._role
\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
92 ABAC_CREDENTIAL_TYPE = 'geni_abac'
\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
102 self.cred_type = ABACCredential.ABAC_CREDENTIAL_TYPE
\r
104 def get_head(self) :
\r
109 def get_tails(self) :
\r
110 if len(self.tails) == 0:
\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
120 raise CredentialNotVerifiable("ABAC credential had no rt0 element")
\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
126 self.head = heads[0]
\r
127 self.tails = self._get_abac_elements(rt0_root, 'tail')
\r
129 def _get_abac_elements(self, root, label):
\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
140 mnemonic_elts = elt.getElementsByTagName('mnemonic')
\r
141 if len(mnemonic_elts) > 0:
\r
142 mnemonic = mnemonic_elts[0].childNodes[0].nodeValue.strip()
\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
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
154 abac_element = ABACElement(keyid, mnemonic, role, linking_role)
\r
155 abac_elements.append(abac_element)
\r
157 return abac_elements
\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
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
174 tree = etree.parse(StringIO(self.xml))
\r
175 aside = etree.tostring(tree, pretty_print=True)
\r
176 result += "\nXML:\n\n"
\r
178 result += "\nEnd XML\n"
\r
181 print "exc. Credential.dump_string / XML"
\r
182 traceback.print_exc()
\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
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
204 append_sub(doc, prin, "mnemonic", mnem)
\r
206 append_sub(doc, ele, "role", role)
\r
208 append_sub(doc, ele, "linking_role", link)
\r
212 # Encode the attributes of the credential into an XML string
\r
213 # This should be done immediately before signing the credential.
\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
220 # Create the XML document
\r
222 signed_cred = doc.createElement("signed-credential")
\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
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
238 doc.appendChild(signed_cred)
\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
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
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
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
273 # Create the <signatures> tag
\r
274 signatures = doc.createElement("signatures")
\r
275 signed_cred.appendChild(signatures)
\r
277 # Get the finished product
\r
278 self.xml = doc.toxml("utf-8")
\r