1 | """NDG Security MSI Resource Policy module |
---|
2 | |
---|
3 | NERC Data Grid Project |
---|
4 | """ |
---|
5 | __author__ = "P J Kershaw" |
---|
6 | __date__ = "03/04/09" |
---|
7 | __copyright__ = "(C) 2009 Science and Technology Facilities Council" |
---|
8 | __contact__ = "Philip.Kershaw@stfc.ac.uk" |
---|
9 | __license__ = "BSD - see LICENSE file in top-level directory" |
---|
10 | __contact__ = "Philip.Kershaw@stfc.ac.uk" |
---|
11 | __revision__ = "$Id: $" |
---|
12 | |
---|
13 | import logging |
---|
14 | log = logging.getLogger(__name__) |
---|
15 | |
---|
16 | import traceback |
---|
17 | import warnings |
---|
18 | from elementtree import ElementTree |
---|
19 | |
---|
20 | from ndg.security.common.utils import TypedList |
---|
21 | from ndg.security.common.utils.etree import QName |
---|
22 | |
---|
23 | |
---|
24 | class PolicyParseError(Exception): |
---|
25 | """Error reading policy attributes from file""" |
---|
26 | |
---|
27 | class InvalidPolicyXmlNsError(Exception): |
---|
28 | """Invalid XML namespace for policy document""" |
---|
29 | |
---|
30 | class PolicyComponent(object): |
---|
31 | """Base class for Policy and Policy subelements""" |
---|
32 | VERSION_1_0_XMLNS = "urn:ndg:security:authz:1.0:policy" |
---|
33 | VERSION_1_1_XMLNS = "urn:ndg:security:authz:1.1:policy" |
---|
34 | XMLNS = (VERSION_1_0_XMLNS, VERSION_1_1_XMLNS) |
---|
35 | __slots__ = ('__xmlns', ) |
---|
36 | |
---|
37 | def __init__(self): |
---|
38 | self.__xmlns = None |
---|
39 | |
---|
40 | def _getXmlns(self): |
---|
41 | return self.__xmlns |
---|
42 | |
---|
43 | def _setXmlns(self, value): |
---|
44 | if not isinstance(value, basestring): |
---|
45 | raise TypeError('Expecting string type for "xmlns" ' |
---|
46 | 'attribute; got %r' % type(value)) |
---|
47 | self.__xmlns = value |
---|
48 | |
---|
49 | xmlns = property(_getXmlns, _setXmlns, |
---|
50 | doc="XML Namespace for policy the document") |
---|
51 | |
---|
52 | @property |
---|
53 | def isValidXmlns(self): |
---|
54 | return self.xmlns in PolicyComponent.XMLNS |
---|
55 | |
---|
56 | |
---|
57 | class Policy(PolicyComponent): |
---|
58 | """NDG MSI Policy.""" |
---|
59 | DESCRIPTION_LOCALNAME = "Description" |
---|
60 | TARGET_LOCALNAME = "Target" |
---|
61 | |
---|
62 | __slots__ = ( |
---|
63 | '__policyFilePath', |
---|
64 | '__description', |
---|
65 | '__targets', |
---|
66 | ) |
---|
67 | |
---|
68 | def __init__(self, policyFilePath=None): |
---|
69 | super(Policy, self).__init__() |
---|
70 | self.__policyFilePath = policyFilePath |
---|
71 | self.__description = None |
---|
72 | self.__targets = TypedList(Target) |
---|
73 | |
---|
74 | def _getPolicyFilePath(self): |
---|
75 | return self.__policyFilePath |
---|
76 | |
---|
77 | def _setPolicyFilePath(self, value): |
---|
78 | if not isinstance(value, basestring): |
---|
79 | raise TypeError('Expecting string type for "policyFilePath" ' |
---|
80 | 'attribute; got %r' % type(value)) |
---|
81 | |
---|
82 | self.__policyFilePath = value |
---|
83 | |
---|
84 | policyFilePath = property(_getPolicyFilePath, _setPolicyFilePath, |
---|
85 | doc="Policy file path") |
---|
86 | |
---|
87 | def _getTargets(self): |
---|
88 | return self.__targets |
---|
89 | |
---|
90 | def _setTargets(self, value): |
---|
91 | if (not isinstance(value, TypedList) and |
---|
92 | not issubclass(value.elementType, Target.__class__)): |
---|
93 | raise TypeError('Expecting TypedList(Target) for "targets" ' |
---|
94 | 'attribute; got %r' % type(value)) |
---|
95 | self.__targets = value |
---|
96 | |
---|
97 | targets = property(_getTargets, _setTargets, |
---|
98 | doc="list of Policy targets") |
---|
99 | |
---|
100 | def _getDescription(self): |
---|
101 | return self.__description |
---|
102 | |
---|
103 | def _setDescription(self, value): |
---|
104 | if not isinstance(value, basestring): |
---|
105 | raise TypeError('Expecting string type for "description" ' |
---|
106 | 'attribute; got %r' % type(value)) |
---|
107 | self.__description = value |
---|
108 | |
---|
109 | description = property(_getDescription, _setDescription, |
---|
110 | doc="Policy Description text") |
---|
111 | |
---|
112 | def parse(self): |
---|
113 | """Parse the policy file set in policyFilePath attribute |
---|
114 | """ |
---|
115 | elem = ElementTree.parse(self.policyFilePath) |
---|
116 | root = elem.getroot() |
---|
117 | |
---|
118 | self.xmlns = QName.getNs(root.tag) |
---|
119 | if not self.isValidXmlns: |
---|
120 | raise InvalidPolicyXmlNsError("Namespace %r is recognised; valid " |
---|
121 | "namespaces are: %r" % |
---|
122 | (self.xmlns, Policy.XMLNS)) |
---|
123 | |
---|
124 | for elem in root: |
---|
125 | localName = QName.getLocalPart(elem.tag) |
---|
126 | if localName == Policy.DESCRIPTION_LOCALNAME: |
---|
127 | self.description = elem.text.strip() |
---|
128 | |
---|
129 | elif localName == Policy.TARGET_LOCALNAME: |
---|
130 | self.targets.append(Target.Parse(elem)) |
---|
131 | |
---|
132 | else: |
---|
133 | raise PolicyParseError("Invalid policy attribute: %s" % |
---|
134 | localName) |
---|
135 | |
---|
136 | @classmethod |
---|
137 | def Parse(cls, policyFilePath): |
---|
138 | policy = cls(policyFilePath=policyFilePath) |
---|
139 | policy.parse() |
---|
140 | return policy |
---|
141 | |
---|
142 | |
---|
143 | class TargetParseError(PolicyParseError): |
---|
144 | """Error reading resource attributes from file""" |
---|
145 | |
---|
146 | import re |
---|
147 | |
---|
148 | class Target(PolicyComponent): |
---|
149 | """Define access behaviour for a resource match a given URI pattern""" |
---|
150 | URI_PATTERN_LOCALNAME = "URIPattern" |
---|
151 | ATTRIBUTES_LOCALNAME = "Attributes" |
---|
152 | ATTRIBUTE_AUTHORITY_LOCALNAME = "AttributeAuthority" |
---|
153 | |
---|
154 | __slots__ = ( |
---|
155 | '__uriPattern', |
---|
156 | '__attributes', |
---|
157 | '__regEx' |
---|
158 | ) |
---|
159 | |
---|
160 | ATTRIBUTE_AUTHORITY_LOCALNAME_DEPRECATED_MSG = """\ |
---|
161 | Use of a <%r/> child element within Target elements will be deprecated for future |
---|
162 | releases. Put the Attribute Authority setting in an Attribute |
---|
163 | <AttributeAuthorityURI/> element e.g. |
---|
164 | |
---|
165 | <Target> |
---|
166 | <uriPattern>^/.*</uriPattern> |
---|
167 | <Attributes> |
---|
168 | <Attribute> |
---|
169 | <Name>myattribute</Name> |
---|
170 | <AttributeAuthorityURI>https://myattributeauthority.ac.uk</AttributeAuthorityURI> |
---|
171 | </Attribute> |
---|
172 | </Attributes> |
---|
173 | </Target> |
---|
174 | """ % ATTRIBUTE_AUTHORITY_LOCALNAME |
---|
175 | |
---|
176 | def __init__(self): |
---|
177 | super(Target, self).__init__() |
---|
178 | self.__uriPattern = None |
---|
179 | self.__attributes = [] |
---|
180 | self.__regEx = None |
---|
181 | |
---|
182 | def getUriPattern(self): |
---|
183 | return self.__uriPattern |
---|
184 | |
---|
185 | def setUriPattern(self, value): |
---|
186 | if not isinstance(value, basestring): |
---|
187 | raise TypeError('Expecting string type for "uriPattern" ' |
---|
188 | 'attribute; got %r' % type(value)) |
---|
189 | self.__uriPattern = value |
---|
190 | |
---|
191 | uriPattern = property(getUriPattern, |
---|
192 | setUriPattern, |
---|
193 | doc="URI Pattern to match this target") |
---|
194 | |
---|
195 | def getAttributes(self): |
---|
196 | return self.__attributes |
---|
197 | |
---|
198 | def setAttributes(self, value): |
---|
199 | if (not isinstance(value, TypedList) and |
---|
200 | not issubclass(value.elementType, Attribute.__class__)): |
---|
201 | raise TypeError('Expecting TypedList(Attribute) for "attributes" ' |
---|
202 | 'attribute; got %r' % type(value)) |
---|
203 | self.__attributes = value |
---|
204 | |
---|
205 | attributes = property(getAttributes, |
---|
206 | setAttributes, |
---|
207 | doc="Attributes restricting access to this target") |
---|
208 | |
---|
209 | def getRegEx(self): |
---|
210 | return self.__regEx |
---|
211 | |
---|
212 | def setRegEx(self, value): |
---|
213 | self.__regEx = value |
---|
214 | |
---|
215 | regEx = property(getRegEx, setRegEx, doc="RegEx's Docstring") |
---|
216 | |
---|
217 | def parse(self, root): |
---|
218 | |
---|
219 | self.xmlns = QName.getNs(root.tag) |
---|
220 | version1_0attributeAuthorityURI = None |
---|
221 | |
---|
222 | for elem in root: |
---|
223 | localName = QName.getLocalPart(elem.tag) |
---|
224 | if localName == Target.URI_PATTERN_LOCALNAME: |
---|
225 | self.uriPattern = elem.text.strip() |
---|
226 | self.regEx = re.compile(self.uriPattern) |
---|
227 | |
---|
228 | elif localName == Target.ATTRIBUTES_LOCALNAME: |
---|
229 | for attrElem in elem: |
---|
230 | if self.xmlns == Target.VERSION_1_1_XMLNS: |
---|
231 | self.attributes.append(Attribute.Parse(attrElem)) |
---|
232 | else: |
---|
233 | attribute = Attribute() |
---|
234 | attribute.name = attrElem.text.strip() |
---|
235 | self.attributes.append(attribute) |
---|
236 | |
---|
237 | elif localName == Target.ATTRIBUTE_AUTHORITY_LOCALNAME: |
---|
238 | # Expecting first element to contain the URI |
---|
239 | warnings.warn( |
---|
240 | Target.ATTRIBUTE_AUTHORITY_LOCALNAME_DEPRECATED_MSG, |
---|
241 | PendingDeprecationWarning) |
---|
242 | |
---|
243 | version1_0attributeAuthorityURI = elem[-1].text.strip() |
---|
244 | else: |
---|
245 | raise TargetParseError("Invalid Target attribute: %s" % |
---|
246 | localName) |
---|
247 | |
---|
248 | if self.xmlns == Target.VERSION_1_0_XMLNS: |
---|
249 | msg = ("Setting all attributes with Attribute Authority " |
---|
250 | "URI set read using Version 1.0 schema. This will " |
---|
251 | "be deprecated in future releases") |
---|
252 | |
---|
253 | warnings.warn(msg, PendingDeprecationWarning) |
---|
254 | log.warning(msg) |
---|
255 | |
---|
256 | if version1_0attributeAuthorityURI is None: |
---|
257 | raise TargetParseError("Assuming version 1.0 schema " |
---|
258 | "for Attribute Authority URI setting " |
---|
259 | "but no URI has been set") |
---|
260 | |
---|
261 | for attribute in self.attributes: |
---|
262 | attribute.attributeAuthorityURI = \ |
---|
263 | version1_0attributeAuthorityURI |
---|
264 | |
---|
265 | @classmethod |
---|
266 | def Parse(cls, root): |
---|
267 | resource = cls() |
---|
268 | resource.parse(root) |
---|
269 | return resource |
---|
270 | |
---|
271 | def __str__(self): |
---|
272 | return str(self.uriPattern) |
---|
273 | |
---|
274 | |
---|
275 | class AttributeParseError(PolicyParseError): |
---|
276 | """Error parsing a Policy Attribute element""" |
---|
277 | |
---|
278 | |
---|
279 | class Attribute(PolicyComponent): |
---|
280 | """encapsulate a target attribute including the name and an Attribute |
---|
281 | Authority from which user attribute information may be queried |
---|
282 | """ |
---|
283 | NAME_LOCALNAME = "Name" |
---|
284 | ATTRIBUTE_AUTHORITY_URI_LOCALNAME = "AttributeAuthorityURI" |
---|
285 | |
---|
286 | __slots__ = ('__name', '__attributeAuthorityURI') |
---|
287 | |
---|
288 | def __init__(self): |
---|
289 | super(Attribute, self).__init__() |
---|
290 | self.__name = '' |
---|
291 | self.__attributeAuthorityURI = None |
---|
292 | |
---|
293 | def __str__(self): |
---|
294 | return self.__name |
---|
295 | |
---|
296 | def _getName(self): |
---|
297 | return self.__name |
---|
298 | |
---|
299 | def _setName(self, value): |
---|
300 | if not isinstance(value, basestring): |
---|
301 | raise TypeError('Expecting string type for "name"; got %r' % |
---|
302 | type(value)) |
---|
303 | self.__name = value |
---|
304 | |
---|
305 | name = property(fget=_getName, |
---|
306 | fset=_setName, |
---|
307 | doc="Attribute name") |
---|
308 | |
---|
309 | def _getAttributeAuthorityURI(self): |
---|
310 | return self.__attributeAuthorityURI |
---|
311 | |
---|
312 | def _setAttributeAuthorityURI(self, value): |
---|
313 | self.__attributeAuthorityURI = value |
---|
314 | |
---|
315 | attributeAuthorityURI = property(_getAttributeAuthorityURI, |
---|
316 | _setAttributeAuthorityURI, |
---|
317 | doc="Attribute Authority URI") |
---|
318 | |
---|
319 | def parse(self, root): |
---|
320 | """Parse from an ElementTree Element""" |
---|
321 | self.xmlns = QName.getNs(root.tag) |
---|
322 | |
---|
323 | for elem in root: |
---|
324 | localName = QName.getLocalPart(elem.tag) |
---|
325 | if localName == Attribute.ATTRIBUTE_AUTHORITY_URI_LOCALNAME: |
---|
326 | self.attributeAuthorityURI = elem.text.strip() |
---|
327 | |
---|
328 | elif localName == Attribute.NAME_LOCALNAME: |
---|
329 | self.name = elem.text.strip() |
---|
330 | else: |
---|
331 | raise AttributeParseError("Invalid Attribute element name: %s" % |
---|
332 | localName) |
---|
333 | |
---|
334 | @classmethod |
---|
335 | def Parse(cls, root): |
---|
336 | """Parse from an ElementTree Element and return a new instance""" |
---|
337 | resource = cls() |
---|
338 | resource.parse(root) |
---|
339 | return resource |
---|
340 | |
---|
341 | |
---|
342 | class _AttrDict(dict): |
---|
343 | """Utility class for holding a constrained list of attributes governed |
---|
344 | by a namespace list""" |
---|
345 | namespaces = () |
---|
346 | def __init__(self, **attributes): |
---|
347 | invalidAttributes = [attr for attr in attributes |
---|
348 | if attr not in self.__class__.namespaces] |
---|
349 | if len(invalidAttributes) > 0: |
---|
350 | raise TypeError("The following attribute namespace(s) are not " |
---|
351 | "recognised: %s" % invalidAttributes) |
---|
352 | |
---|
353 | self.update(attributes) |
---|
354 | |
---|
355 | def __setitem__(self, key, val): |
---|
356 | if key not in self.__class__.namespaces: |
---|
357 | raise KeyError('Namespace "%s" not recognised. Valid namespaces ' |
---|
358 | 'are: %s' % self.__class__.namespaces) |
---|
359 | |
---|
360 | dict.__setitem__(self, key, val) |
---|
361 | |
---|
362 | |
---|
363 | def update(self, d, **kw): |
---|
364 | for dictArg in (d, kw): |
---|
365 | for k in dictArg: |
---|
366 | if k not in self.__class__.namespaces: |
---|
367 | raise KeyError('Namespace "%s" not recognised. Valid ' |
---|
368 | 'namespaces are: %s' % |
---|
369 | self.__class__.namespaces) |
---|
370 | |
---|
371 | dict.update(self, d, **kw) |
---|
372 | |
---|
373 | |
---|
374 | class Subject(_AttrDict): |
---|
375 | '''Subject designator''' |
---|
376 | namespaces = ( |
---|
377 | "urn:ndg:security:authz:1.0:attr:subject:userId", |
---|
378 | "urn:ndg:security:authz:1.0:attr:subject:sessionId", |
---|
379 | "urn:ndg:security:authz:1.0:attr:subject:sessionManagerURI", |
---|
380 | "urn:ndg:security:authz:1.0:attr:subject:roles" |
---|
381 | ) |
---|
382 | (USERID_NS, SESSIONID_NS, SESSIONMANAGERURI_NS, ROLES_NS) = namespaces |
---|
383 | |
---|
384 | |
---|
385 | class Resource(_AttrDict): |
---|
386 | '''Resource designator''' |
---|
387 | namespaces = ( |
---|
388 | "urn:ndg:security:authz:1.0:attr:resource:uri", |
---|
389 | ) |
---|
390 | (URI_NS,) = namespaces |
---|
391 | |
---|
392 | |
---|
393 | class Request(object): |
---|
394 | '''Request to send to a PDP''' |
---|
395 | def __init__(self, subject=Subject(), resource=Resource()): |
---|
396 | self.subject = subject |
---|
397 | self.resource = resource |
---|
398 | |
---|
399 | def _getSubject(self): |
---|
400 | return self.__subject |
---|
401 | |
---|
402 | def _setSubject(self, subject): |
---|
403 | if not isinstance(subject, Subject,): |
---|
404 | raise TypeError("Expecting %s type for Request subject; got %r" % |
---|
405 | (Subject.__class__.__name__, subject)) |
---|
406 | self.__subject = subject |
---|
407 | |
---|
408 | subject = property(fget=_getSubject, |
---|
409 | fset=_setSubject, |
---|
410 | doc="Subject type object representing subject accessing " |
---|
411 | "a resource") |
---|
412 | |
---|
413 | def _getResource(self): |
---|
414 | return self.__resource |
---|
415 | |
---|
416 | def _setResource(self, resource): |
---|
417 | if not isinstance(resource, Resource): |
---|
418 | raise TypeError("Expecting %s for Request Resource; got %r" % |
---|
419 | (Resource.__class__.__name__, resource)) |
---|
420 | self.__resource = resource |
---|
421 | |
---|
422 | resource = property(fget=_getResource, |
---|
423 | fset=_setResource, |
---|
424 | doc="Resource to be protected") |
---|
425 | |
---|
426 | |
---|
427 | class Response(object): |
---|
428 | '''Response from a PDP''' |
---|
429 | decisionValues = range(4) |
---|
430 | (DECISION_PERMIT, |
---|
431 | DECISION_DENY, |
---|
432 | DECISION_INDETERMINATE, |
---|
433 | DECISION_NOT_APPLICABLE) = decisionValues |
---|
434 | |
---|
435 | # string versions of the 4 Decision types used for encoding |
---|
436 | DECISIONS = ("Permit", "Deny", "Indeterminate", "NotApplicable") |
---|
437 | |
---|
438 | decisionValue2String = dict(zip(decisionValues, DECISIONS)) |
---|
439 | |
---|
440 | def __init__(self, status, message=None): |
---|
441 | self.__status = None |
---|
442 | self.__message = None |
---|
443 | |
---|
444 | self.status = status |
---|
445 | self.message = message |
---|
446 | |
---|
447 | def _setStatus(self, status): |
---|
448 | if status not in Response.decisionValues: |
---|
449 | raise TypeError("Status %s not recognised" % status) |
---|
450 | |
---|
451 | self.__status = status |
---|
452 | |
---|
453 | def _getStatus(self): |
---|
454 | return self.__status |
---|
455 | |
---|
456 | status = property(fget=_getStatus, |
---|
457 | fset=_setStatus, |
---|
458 | doc="Integer response code; one of %r" % decisionValues) |
---|
459 | |
---|
460 | def _setMessage(self, message): |
---|
461 | if not isinstance(message, (basestring, type(None))): |
---|
462 | raise TypeError('Expecting string or None type for "message"; got ' |
---|
463 | '%r' % type(message)) |
---|
464 | |
---|
465 | self.__message = message |
---|
466 | |
---|
467 | def _getMessage(self): |
---|
468 | return self.__message |
---|
469 | |
---|
470 | message = property(fget=_getMessage, |
---|
471 | fset=_setMessage, |
---|
472 | doc="Optional message associated with response") |
---|
473 | |
---|
474 | |
---|
475 | from ndg.security.common.AttCert import (AttCertInvalidSignature, |
---|
476 | AttCertNotBeforeTimeError, AttCertExpired, AttCertError) |
---|
477 | |
---|
478 | from ndg.security.common.sessionmanager import (SessionManagerClient, |
---|
479 | SessionNotFound, SessionCertTimeError, SessionExpired, InvalidSession, |
---|
480 | AttributeRequestDenied) |
---|
481 | |
---|
482 | from ndg.security.common.attributeauthority import (AttributeAuthorityClient, |
---|
483 | NoTrustedHosts, NoMatchingRoleInTrustedHosts, |
---|
484 | InvalidAttributeAuthorityClientCtx) |
---|
485 | from ndg.security.common.attributeauthority import AttributeRequestDenied as \ |
---|
486 | AA_AttributeRequestDenied |
---|
487 | |
---|
488 | from ndg.security.common.authz.pdp import (PDPUserNotLoggedIn, |
---|
489 | PDPUserAccessDenied) |
---|
490 | |
---|
491 | |
---|
492 | class SubjectRetrievalError(Exception): |
---|
493 | """Generic exception class for errors related to information about the |
---|
494 | subject""" |
---|
495 | |
---|
496 | class InvalidAttributeCertificate(SubjectRetrievalError): |
---|
497 | "The certificate containing authorisation roles is invalid" |
---|
498 | def __init__(self, msg=None): |
---|
499 | SubjectRetrievalError.__init__(self, msg or |
---|
500 | InvalidAttributeCertificate.__doc__) |
---|
501 | |
---|
502 | class AttributeCertificateInvalidSignature(SubjectRetrievalError): |
---|
503 | ("There is a problem with the signature of the certificate containing " |
---|
504 | "authorisation roles") |
---|
505 | def __init__(self, msg=None): |
---|
506 | SubjectRetrievalError.__init__(self, msg or |
---|
507 | AttributeCertificateInvalidSignature.__doc__) |
---|
508 | |
---|
509 | class AttributeCertificateNotBeforeTimeError(SubjectRetrievalError): |
---|
510 | ("There is a time issuing error with certificate containing authorisation " |
---|
511 | "roles") |
---|
512 | def __init__(self, msg=None): |
---|
513 | SubjectRetrievalError.__init__(self, msg or |
---|
514 | AttributeCertificateNotBeforeTimeError.__doc__) |
---|
515 | |
---|
516 | class AttributeCertificateExpired(SubjectRetrievalError): |
---|
517 | "The certificate containing authorisation roles has expired" |
---|
518 | def __init__(self, msg=None): |
---|
519 | SubjectRetrievalError.__init__(self, msg or |
---|
520 | AttributeCertificateExpired.__doc__) |
---|
521 | |
---|
522 | class SessionExpiredMsg(SubjectRetrievalError): |
---|
523 | 'Session has expired. Please re-login at your home organisation' |
---|
524 | def __init__(self, msg=None): |
---|
525 | SubjectRetrievalError.__init__(self, msg or SessionExpiredMsg.__doc__) |
---|
526 | |
---|
527 | class SessionNotFoundMsg(SubjectRetrievalError): |
---|
528 | 'No session was found. Please try re-login with your home organisation' |
---|
529 | def __init__(self, msg=None): |
---|
530 | SubjectRetrievalError.__init__(self, msg or |
---|
531 | SessionNotFoundMsg.__doc__) |
---|
532 | |
---|
533 | class InvalidSessionMsg(SubjectRetrievalError): |
---|
534 | 'Session is invalid. Please try re-login with your home organisation' |
---|
535 | def __init__(self, msg=None): |
---|
536 | SubjectRetrievalError.__init__(self, msg or |
---|
537 | InvalidSessionMsg.__doc__) |
---|
538 | |
---|
539 | class InitSessionCtxError(SubjectRetrievalError): |
---|
540 | 'A problem occurred initialising a session connection' |
---|
541 | def __init__(self, msg=None): |
---|
542 | SubjectRetrievalError.__init__(self, msg or |
---|
543 | InitSessionCtxError.__doc__) |
---|
544 | |
---|
545 | class AttributeCertificateRequestError(SubjectRetrievalError): |
---|
546 | 'A problem occurred requesting a certificate containing authorisation roles' |
---|
547 | def __init__(self, msg=None): |
---|
548 | SubjectRetrievalError.__init__(self, msg or |
---|
549 | AttributeCertificateRequestError.__doc__) |
---|
550 | |
---|
551 | class PIPAttributeQuery(_AttrDict): |
---|
552 | '''Policy Information Point Query class.''' |
---|
553 | namespaces = ( |
---|
554 | "urn:ndg:security:authz:1.0:attr:subject", |
---|
555 | "urn:ndg:security:authz:1.0:attr:attributeAuthorityURI", |
---|
556 | ) |
---|
557 | (SUBJECT_NS, ATTRIBUTEAUTHORITY_NS) = namespaces |
---|
558 | |
---|
559 | class PIPAttributeResponse(dict): |
---|
560 | '''Policy Information Point Response class.''' |
---|
561 | namespaces = ( |
---|
562 | Subject.ROLES_NS, |
---|
563 | ) |
---|
564 | |
---|
565 | |
---|
566 | class PIPBase(object): |
---|
567 | """Policy Information Point base class. PIP enables PDP to get user |
---|
568 | attribute information in order to make access control decisions |
---|
569 | """ |
---|
570 | def __init__(self, prefix='', **cfg): |
---|
571 | '''Initialise settings for connection to an Attribute Authority''' |
---|
572 | raise NotImplementedError(PIPBase.__init__.__doc__) |
---|
573 | |
---|
574 | def attributeQuery(self, attributeQuery): |
---|
575 | """Query the Attribute Authority specified in the request to retrieve |
---|
576 | the attributes if any corresponding to the subject |
---|
577 | |
---|
578 | @type attributeResponse: PIPAttributeQuery |
---|
579 | @param attributeResponse: |
---|
580 | @rtype: PIPAttributeResponse |
---|
581 | @return: response containing the attributes retrieved from the |
---|
582 | Attribute Authority""" |
---|
583 | raise NotImplementedError(PIPBase.attributeQuery.__doc__) |
---|
584 | |
---|
585 | |
---|
586 | from ndg.security.common.wssecurity import WSSecurityConfig |
---|
587 | |
---|
588 | class NdgPIP(PIPBase): |
---|
589 | """Policy Information Point - this implementation enables the PDP to |
---|
590 | retrieve attributes about the Subject""" |
---|
591 | wsseSectionName = 'wssecurity' |
---|
592 | |
---|
593 | def __init__(self, prefix='', **cfg): |
---|
594 | '''Set-up WS-Security and SSL settings for connection to an |
---|
595 | Attribute Authority |
---|
596 | |
---|
597 | @type **cfg: dict |
---|
598 | @param **cfg: keywords including 'sslCACertFilePathList' used to set a |
---|
599 | list of CA certificates for an SSL connection to the Attribute |
---|
600 | Authority if used and also WS-Security settings as used by |
---|
601 | ndg.security.common.wssecurity.WSSecurityConfig |
---|
602 | ''' |
---|
603 | self.wssecurityCfg = WSSecurityConfig() |
---|
604 | wssePrefix = prefix + NdgPIP.wsseSectionName |
---|
605 | self.wssecurityCfg.update(cfg, prefix=wssePrefix) |
---|
606 | |
---|
607 | # List of CA certificates used to verify peer certificate with SSL |
---|
608 | # connections to Attribute Authority |
---|
609 | self.sslCACertFilePathList = cfg.get(prefix+'sslCACertFilePathList', []) |
---|
610 | |
---|
611 | # List of CA certificates used to verify the signatures of |
---|
612 | # Attribute Certificates retrieved |
---|
613 | self.caCertFilePathList = cfg.get(prefix + 'caCertFilePathList', []) |
---|
614 | |
---|
615 | def attributeQuery(self, attributeQuery): |
---|
616 | """Query the Attribute Authority specified in the request to retrieve |
---|
617 | the attributes if any corresponding to the subject |
---|
618 | |
---|
619 | @type attributeResponse: PIPAttributeQuery |
---|
620 | @param attributeResponse: |
---|
621 | @rtype: PIPAttributeResponse |
---|
622 | @return: response containing the attributes retrieved from the |
---|
623 | Attribute Authority""" |
---|
624 | |
---|
625 | subject = attributeQuery[PIPAttributeQuery.SUBJECT_NS] |
---|
626 | username = subject[Subject.USERID_NS] |
---|
627 | sessionId = subject[Subject.SESSIONID_NS] |
---|
628 | attributeAuthorityURI = attributeQuery[ |
---|
629 | PIPAttributeQuery.ATTRIBUTEAUTHORITY_NS] |
---|
630 | |
---|
631 | sessionId = subject[Subject.SESSIONID_NS] |
---|
632 | |
---|
633 | log.debug("PIP: received attribute query: %r", attributeQuery) |
---|
634 | |
---|
635 | attributeCertificate = self._getAttributeCertificate( |
---|
636 | attributeAuthorityURI, |
---|
637 | username=username, |
---|
638 | sessionId=sessionId, |
---|
639 | sessionManagerURI=subject[Subject.SESSIONMANAGERURI_NS]) |
---|
640 | |
---|
641 | attributeResponse = PIPAttributeResponse() |
---|
642 | attributeResponse[Subject.ROLES_NS] = attributeCertificate.roles |
---|
643 | |
---|
644 | log.debug("PIP.attributeQuery response: %r", attributeResponse) |
---|
645 | |
---|
646 | return attributeResponse |
---|
647 | |
---|
648 | def _getAttributeCertificate(self, |
---|
649 | attributeAuthorityURI, |
---|
650 | username=None, |
---|
651 | sessionId=None, |
---|
652 | sessionManagerURI=None): |
---|
653 | '''Retrieve an Attribute Certificate |
---|
654 | |
---|
655 | @type attributeAuthorityURI: basestring |
---|
656 | @param attributeAuthorityURI: URI to Attribute Authority service |
---|
657 | @type username: basestring |
---|
658 | @param username: subject user identifier - could be an OpenID |
---|
659 | @type sessionId: basestring |
---|
660 | @param sessionId: Session Manager session handle |
---|
661 | @type sessionManagerURI: basestring |
---|
662 | @param sessionManagerURI: URI to remote session manager service |
---|
663 | @rtype: ndg.security.common.AttCert.AttCert |
---|
664 | @return: Attribute Certificate containing user roles |
---|
665 | ''' |
---|
666 | |
---|
667 | if sessionId and sessionManagerURI: |
---|
668 | attrCert = self._getAttributeCertificateFromSessionManager( |
---|
669 | attributeAuthorityURI, |
---|
670 | sessionId, |
---|
671 | sessionManagerURI) |
---|
672 | else: |
---|
673 | attrCert = self._getAttributeCertificateFromAttributeAuthority( |
---|
674 | attributeAuthorityURI, |
---|
675 | username) |
---|
676 | |
---|
677 | try: |
---|
678 | attrCert.certFilePathList = self.caCertFilePathList |
---|
679 | attrCert.isValid(raiseExcep=True) |
---|
680 | |
---|
681 | except AttCertInvalidSignature, e: |
---|
682 | log.exception(e) |
---|
683 | raise AttributeCertificateInvalidSignature() |
---|
684 | |
---|
685 | except AttCertNotBeforeTimeError, e: |
---|
686 | log.exception(e) |
---|
687 | raise AttributeCertificateNotBeforeTimeError() |
---|
688 | |
---|
689 | except AttCertExpired, e: |
---|
690 | log.exception(e) |
---|
691 | raise AttributeCertificateExpired() |
---|
692 | |
---|
693 | except AttCertError, e: |
---|
694 | log.exception(e) |
---|
695 | raise InvalidAttributeCertificate() |
---|
696 | |
---|
697 | return attrCert |
---|
698 | |
---|
699 | def _getAttributeCertificateFromSessionManager(self, |
---|
700 | attributeAuthorityURI, |
---|
701 | sessionId, |
---|
702 | sessionManagerURI): |
---|
703 | '''Retrieve an Attribute Certificate using the subject's Session |
---|
704 | Manager |
---|
705 | |
---|
706 | @type sessionId: basestring |
---|
707 | @param sessionId: Session Manager session handle |
---|
708 | @type sessionManagerURI: basestring |
---|
709 | @param sessionManagerURI: URI to remote session manager service |
---|
710 | @type attributeAuthorityURI: basestring |
---|
711 | @param attributeAuthorityURI: URI to Attribute Authority service |
---|
712 | @rtype: ndg.security.common.AttCert.AttCert |
---|
713 | @return: Attribute Certificate containing user roles |
---|
714 | ''' |
---|
715 | |
---|
716 | log.debug("PIP._getAttributeCertificateFromSessionManager ...") |
---|
717 | |
---|
718 | try: |
---|
719 | # Create Session Manager client - if a file path was set, setting |
---|
720 | # are read from a separate config file section otherwise, from the |
---|
721 | # PDP config object |
---|
722 | smClnt = SessionManagerClient( |
---|
723 | uri=sessionManagerURI, |
---|
724 | sslCACertFilePathList=self.sslCACertFilePathList, |
---|
725 | cfg=self.wssecurityCfg) |
---|
726 | except Exception, e: |
---|
727 | log.error("Creating Session Manager client: %s" % e) |
---|
728 | raise InitSessionCtxError() |
---|
729 | |
---|
730 | try: |
---|
731 | # Make request for attribute certificate |
---|
732 | return smClnt.getAttCert( |
---|
733 | attributeAuthorityURI=attributeAuthorityURI, |
---|
734 | sessID=sessionId) |
---|
735 | |
---|
736 | except AttributeRequestDenied, e: |
---|
737 | log.error("Request for attribute certificate denied: %s" % e) |
---|
738 | raise PDPUserAccessDenied() |
---|
739 | |
---|
740 | except SessionNotFound, e: |
---|
741 | log.error("No session found: %s" % e) |
---|
742 | raise SessionNotFoundMsg() |
---|
743 | |
---|
744 | except SessionExpired, e: |
---|
745 | log.error("Session expired: %s" % e) |
---|
746 | raise SessionExpiredMsg() |
---|
747 | |
---|
748 | except SessionCertTimeError, e: |
---|
749 | log.error("Session cert. time error: %s" % e) |
---|
750 | raise InvalidSessionMsg() |
---|
751 | |
---|
752 | except InvalidSession, e: |
---|
753 | log.error("Invalid user session: %s" % e) |
---|
754 | raise InvalidSessionMsg() |
---|
755 | |
---|
756 | except Exception, e: |
---|
757 | log.error("Request from Session Manager [%s] to Attribute " |
---|
758 | "Authority [%s] for attribute certificate: %s: %s" % |
---|
759 | (sessionManagerURI, |
---|
760 | attributeAuthorityURI, |
---|
761 | e.__class__, e)) |
---|
762 | raise AttributeCertificateRequestError() |
---|
763 | |
---|
764 | def _getAttributeCertificateFromAttributeAuthority(self, |
---|
765 | attributeAuthorityURI, |
---|
766 | username): |
---|
767 | '''Retrieve an Attribute Certificate direct from an Attribute |
---|
768 | Authority. This method is invoked if no session ID or Session |
---|
769 | Manager endpoint where provided |
---|
770 | |
---|
771 | @type username: basestring |
---|
772 | @param username: user identifier - may be an OpenID URI |
---|
773 | @type attributeAuthorityURI: basestring |
---|
774 | @param attributeAuthorityURI: URI to Attribute Authority service |
---|
775 | @rtype: ndg.security.common.AttCert.AttCert |
---|
776 | @return: Attribute Certificate containing user roles |
---|
777 | ''' |
---|
778 | |
---|
779 | log.debug("PIP._getAttributeCertificateFromAttributeAuthority ...") |
---|
780 | |
---|
781 | try: |
---|
782 | # Create Attribute Authority client - if a file path was set, |
---|
783 | # settingare read from a separate config file section otherwise, |
---|
784 | # from the PDP config object |
---|
785 | aaClnt = AttributeAuthorityClient( |
---|
786 | uri=attributeAuthorityURI, |
---|
787 | sslCACertFilePathList=self.sslCACertFilePathList, |
---|
788 | cfg=self.wssecurityCfg) |
---|
789 | except Exception: |
---|
790 | log.error("Creating Attribute Authority client: %s", |
---|
791 | traceback.format_exc()) |
---|
792 | raise InitSessionCtxError() |
---|
793 | |
---|
794 | |
---|
795 | try: |
---|
796 | # Make request for attribute certificate |
---|
797 | return aaClnt.getAttCert(userId=username) |
---|
798 | |
---|
799 | |
---|
800 | except AA_AttributeRequestDenied: |
---|
801 | log.error("Request for attribute certificate denied: %s", |
---|
802 | traceback.format_exc()) |
---|
803 | raise PDPUserAccessDenied() |
---|
804 | |
---|
805 | # TODO: handle other specific Exception types here for more fine |
---|
806 | # grained response info |
---|
807 | |
---|
808 | except Exception, e: |
---|
809 | log.error("Request to Attribute Authority [%s] for attribute " |
---|
810 | "certificate: %s: %s", attributeAuthorityURI, |
---|
811 | e.__class__, traceback.format_exc()) |
---|
812 | raise AttributeCertificateRequestError() |
---|
813 | |
---|
814 | # Backwards compatibility |
---|
815 | PIP = NdgPIP |
---|
816 | |
---|
817 | |
---|
818 | class PDP(object): |
---|
819 | """Policy Decision Point""" |
---|
820 | |
---|
821 | def __init__(self, policy, pip): |
---|
822 | """Read in a file which determines access policy""" |
---|
823 | self.policy = policy |
---|
824 | self.pip = pip |
---|
825 | |
---|
826 | def _getPolicy(self): |
---|
827 | if self.__policy is None: |
---|
828 | raise TypeError("Policy object has not been initialised") |
---|
829 | return self.__policy |
---|
830 | |
---|
831 | def _setPolicy(self, policy): |
---|
832 | if not isinstance(policy, (Policy, None.__class__)): |
---|
833 | raise TypeError("Expecting %s or None type for PDP policy; got %r"% |
---|
834 | (Policy.__class__.__name__, policy)) |
---|
835 | self.__policy = policy |
---|
836 | |
---|
837 | policy = property(fget=_getPolicy, |
---|
838 | fset=_setPolicy, |
---|
839 | doc="Policy type object used by the PDP to determine " |
---|
840 | "access for resources") |
---|
841 | |
---|
842 | def _getPIP(self): |
---|
843 | if self.__pip is None: |
---|
844 | raise TypeError("PIP object has not been initialised") |
---|
845 | |
---|
846 | return self.__pip |
---|
847 | |
---|
848 | def _setPIP(self, pip): |
---|
849 | if not isinstance(pip, (PIPBase, None.__class__)): |
---|
850 | raise TypeError("Expecting %s or None type for PDP PIP; got %r"% |
---|
851 | (PIPBase.__class__.__name__, pip)) |
---|
852 | self.__pip = pip |
---|
853 | |
---|
854 | pip = property(fget=_getPIP, |
---|
855 | fset=_setPIP, |
---|
856 | doc="Policy Information Point - PIP type object used by " |
---|
857 | "the PDP to retrieve user attributes") |
---|
858 | |
---|
859 | def evaluate(self, request): |
---|
860 | '''Make access control decision''' |
---|
861 | |
---|
862 | if not isinstance(request, Request): |
---|
863 | raise TypeError("Expecting %s type for request; got %r" % |
---|
864 | (Request.__class__.__name__, request)) |
---|
865 | |
---|
866 | # Look for matching targets to the given resource |
---|
867 | resourceURI = request.resource[Resource.URI_NS] |
---|
868 | matchingTargets = [target for target in self.policy.targets |
---|
869 | if target.regEx.match(resourceURI) is not None] |
---|
870 | numMatchingTargets = len(matchingTargets) |
---|
871 | if numMatchingTargets == 0: |
---|
872 | log.debug("PDP.evaluate: granting access - no targets matched " |
---|
873 | "the resource URI path [%s]", |
---|
874 | resourceURI) |
---|
875 | return Response(status=Response.DECISION_PERMIT) |
---|
876 | |
---|
877 | # Iterate through matching targets checking for user access |
---|
878 | request.subject[Subject.ROLES_NS] = [] |
---|
879 | permitForAllTargets = [Response.DECISION_PERMIT]*numMatchingTargets |
---|
880 | |
---|
881 | # Keep a look-up of the decisions for each target |
---|
882 | status = [] |
---|
883 | |
---|
884 | # Make a query object for querying the Policy Information Point |
---|
885 | attributeQuery = PIPAttributeQuery() |
---|
886 | attributeQuery[PIPAttributeQuery.SUBJECT_NS] = request.subject |
---|
887 | |
---|
888 | # Keep a cache of queried Attribute Authorities to avoid calling them |
---|
889 | # multiple times |
---|
890 | queriedAttributeAuthorityURIs = [] |
---|
891 | |
---|
892 | # Iterate through the targets gathering user attributes from the |
---|
893 | # relevant attribute authorities |
---|
894 | for matchingTarget in matchingTargets: |
---|
895 | |
---|
896 | # Make call to the Policy Information Point to pull user |
---|
897 | # attributes applicable to this resource |
---|
898 | for attribute in matchingTarget.attributes: |
---|
899 | if (attribute.attributeAuthorityURI in |
---|
900 | queriedAttributeAuthorityURIs): |
---|
901 | continue |
---|
902 | |
---|
903 | attributeQuery[ |
---|
904 | PIPAttributeQuery.ATTRIBUTEAUTHORITY_NS |
---|
905 | ] = attribute.attributeAuthorityURI |
---|
906 | |
---|
907 | # Exit from function returning indeterminate status if a |
---|
908 | # problem occurs here |
---|
909 | try: |
---|
910 | attributeResponse = self.pip.attributeQuery(attributeQuery) |
---|
911 | |
---|
912 | except SubjectRetrievalError, e: |
---|
913 | # i.e. a defined exception within the scope of this |
---|
914 | # module |
---|
915 | log.error("SAML Attribute Query %s: %s", |
---|
916 | type(e), traceback.format_exc()) |
---|
917 | return Response(Response.DECISION_INDETERMINATE, |
---|
918 | message=traceback.format_exc()) |
---|
919 | |
---|
920 | except Exception, e: |
---|
921 | log.error("SAML Attribute Query %s: %s", |
---|
922 | type(e), traceback.format_exc()) |
---|
923 | return Response(Response.DECISION_INDETERMINATE, |
---|
924 | message="An internal error occurred") |
---|
925 | |
---|
926 | # Accumulate attributes retrieved from multiple attribute |
---|
927 | # authorities |
---|
928 | request.subject[Subject.ROLES_NS] += attributeResponse[ |
---|
929 | Subject.ROLES_NS] |
---|
930 | |
---|
931 | # Match the subject's attributes against the target |
---|
932 | # One of any rule - at least one of the subject's attributes |
---|
933 | # must match one of the attributes restricting access to the |
---|
934 | # resource. |
---|
935 | log.debug("PDP.evaluate: Matching subject attributes %r against " |
---|
936 | "resource attributes %r ...", |
---|
937 | request.subject[Subject.ROLES_NS], |
---|
938 | matchingTarget.attributes) |
---|
939 | |
---|
940 | status.append(PDP._match(matchingTarget.attributes, |
---|
941 | request.subject[Subject.ROLES_NS])) |
---|
942 | |
---|
943 | # All targets must yield permit status for access to be granted |
---|
944 | if status == permitForAllTargets: |
---|
945 | return Response(Response.DECISION_PERMIT) |
---|
946 | else: |
---|
947 | return Response(Response.DECISION_DENY, |
---|
948 | message="Insufficient privileges to access the " |
---|
949 | "resource") |
---|
950 | |
---|
951 | @staticmethod |
---|
952 | def _match(resourceAttr, subjectAttr): |
---|
953 | """Helper method to iterate over user and resource attributes |
---|
954 | If one at least one match is found, a permit response is returned |
---|
955 | """ |
---|
956 | for attr in resourceAttr: |
---|
957 | if attr.name in subjectAttr: |
---|
958 | return Response.DECISION_PERMIT |
---|
959 | |
---|
960 | return Response.DECISION_DENY |
---|
961 | |
---|
962 | |
---|