source: TI12-security/trunk/NDGSecurity/python/ndg_security_common/ndg/security/common/authz/msi.py @ 6597

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/NDGSecurity/python/ndg_security_common/ndg/security/common/authz/msi.py@6597
Revision 6597, 20.6 KB checked in by pjkersha, 11 years ago (diff)

New Policy Information Point class ndg.security.common.authz.pip.esg.PIP for ESG Authorisation Service.

Line 
1"""NDG Security MSI Resource Policy module
2
3NERC 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
13import logging
14log = logging.getLogger(__name__)
15
16import traceback
17import warnings
18from elementtree import ElementTree
19
20from ndg.security.common.utils import TypedList
21from ndg.security.common.utils.etree import QName
22from ndg.security.common.authz import _AttrDict, SubjectBase
23from ndg.security.common.authz.pip import (PIPBase, PIPAttributeQuery, 
24                                           PIPAttributeResponse,
25                                           SubjectRetrievalError)
26
27
28class PolicyParseError(Exception):
29    """Error reading policy attributes from file"""
30
31
32class InvalidPolicyXmlNsError(Exception):
33    """Invalid XML namespace for policy document"""
34
35
36class PolicyComponent(object):
37    """Base class for Policy and Policy subelements"""
38    VERSION_1_0_XMLNS = "urn:ndg:security:authz:1.0:policy"
39    VERSION_1_1_XMLNS = "urn:ndg:security:authz:1.1:policy"
40    XMLNS = (VERSION_1_0_XMLNS, VERSION_1_1_XMLNS)
41    __slots__ = ('__xmlns', )
42
43    def __init__(self):
44        self.__xmlns = None
45       
46    def _getXmlns(self):
47        return self.__xmlns
48
49    def _setXmlns(self, value):
50        if not isinstance(value, basestring):
51            raise TypeError('Expecting string type for "xmlns" '
52                            'attribute; got %r' % type(value))
53        self.__xmlns = value
54
55    xmlns = property(_getXmlns, _setXmlns, 
56                     doc="XML Namespace for policy the document")
57   
58    @property
59    def isValidXmlns(self):
60        return self.xmlns in PolicyComponent.XMLNS
61   
62   
63class Policy(PolicyComponent):
64    """NDG MSI Policy."""   
65    DESCRIPTION_LOCALNAME = "Description"
66    TARGET_LOCALNAME = "Target"
67   
68    __slots__ = (
69        '__policyFilePath',
70        '__description',
71        '__targets',
72    )
73   
74    def __init__(self, policyFilePath=None):
75        super(Policy, self).__init__()
76        self.__policyFilePath = policyFilePath
77        self.__description = None
78        self.__targets = TypedList(Target)
79
80    def _getPolicyFilePath(self):
81        return self.__policyFilePath
82
83    def _setPolicyFilePath(self, value):
84        if not isinstance(value, basestring):
85            raise TypeError('Expecting string type for "policyFilePath" '
86                            'attribute; got %r' % type(value))
87           
88        self.__policyFilePath = value
89
90    policyFilePath = property(_getPolicyFilePath, _setPolicyFilePath, 
91                              doc="Policy file path")
92
93    def _getTargets(self):
94        return self.__targets
95
96    def _setTargets(self, value):
97        if (not isinstance(value, TypedList) and 
98            not issubclass(value.elementType, Target.__class__)):
99            raise TypeError('Expecting TypedList(Target) for "targets" '
100                            'attribute; got %r' % type(value))
101        self.__targets = value
102
103    targets = property(_getTargets, _setTargets, 
104                       doc="list of Policy targets")
105
106    def _getDescription(self):
107        return self.__description
108
109    def _setDescription(self, value):
110        if not isinstance(value, basestring):
111            raise TypeError('Expecting string type for "description" '
112                            'attribute; got %r' % type(value))
113        self.__description = value
114
115    description = property(_getDescription, _setDescription, 
116                           doc="Policy Description text")
117   
118    def parse(self):
119        """Parse the policy file set in policyFilePath attribute
120        """
121        elem = ElementTree.parse(self.policyFilePath)
122        root = elem.getroot()
123       
124        self.xmlns = QName.getNs(root.tag)
125        if not self.isValidXmlns:
126            raise InvalidPolicyXmlNsError("Namespace %r is recognised; valid "
127                                          "namespaces are: %r" %
128                                          (self.xmlns, Policy.XMLNS))
129           
130        for elem in root:
131            localName = QName.getLocalPart(elem.tag)
132            if localName == Policy.DESCRIPTION_LOCALNAME:
133                self.description = elem.text.strip()
134               
135            elif localName == Policy.TARGET_LOCALNAME:
136                self.targets.append(Target.Parse(elem))
137               
138            else:
139                raise PolicyParseError("Invalid policy attribute: %s" % 
140                                        localName)
141               
142    @classmethod
143    def Parse(cls, policyFilePath):
144        policy = cls(policyFilePath=policyFilePath)
145        policy.parse()
146        return policy
147
148
149class TargetParseError(PolicyParseError):
150    """Error reading resource attributes from file"""
151
152import re
153   
154class Target(PolicyComponent):
155    """Define access behaviour for a resource match a given URI pattern"""
156    URI_PATTERN_LOCALNAME = "URIPattern"
157    ATTRIBUTES_LOCALNAME = "Attributes"
158    ATTRIBUTE_AUTHORITY_LOCALNAME = "AttributeAuthority"
159   
160    __slots__ = (
161        '__uriPattern',
162        '__attributes',
163        '__regEx'       
164    )
165
166    ATTRIBUTE_AUTHORITY_LOCALNAME_DEPRECATED_MSG = """\
167Use of a <%r/> child element within Target elements will be deprecated for future
168releases.  Put the Attribute Authority setting in an Attribute
169<AttributeAuthorityURI/> element e.g.
170
171<Target>
172    <uriPattern>^/.*</uriPattern>
173    <Attributes>
174        <Attribute>
175            <Name>myattribute</Name>
176            <AttributeAuthorityURI>https://myattributeauthority.ac.uk</AttributeAuthorityURI>
177        </Attribute>
178    </Attributes>
179</Target>
180"""  % ATTRIBUTE_AUTHORITY_LOCALNAME 
181   
182    def __init__(self):
183        super(Target, self).__init__()
184        self.__uriPattern = None
185        self.__attributes = []
186        self.__regEx = None
187       
188    def getUriPattern(self):
189        return self.__uriPattern
190
191    def setUriPattern(self, value):
192        if not isinstance(value, basestring):
193            raise TypeError('Expecting string type for "uriPattern" '
194                            'attribute; got %r' % type(value))
195        self.__uriPattern = value
196
197    uriPattern = property(getUriPattern, 
198                          setUriPattern, 
199                          doc="URI Pattern to match this target")
200
201    def getAttributes(self):
202        return self.__attributes
203
204    def setAttributes(self, value):
205        if (not isinstance(value, TypedList) and 
206            not issubclass(value.elementType, Attribute.__class__)):
207            raise TypeError('Expecting TypedList(Attribute) for "attributes" '
208                            'attribute; got %r' % type(value))
209        self.__attributes = value
210
211    attributes = property(getAttributes, 
212                          setAttributes, 
213                          doc="Attributes restricting access to this target")
214
215    def getRegEx(self):
216        return self.__regEx
217
218    def setRegEx(self, value):
219        self.__regEx = value
220
221    regEx = property(getRegEx, setRegEx, doc="RegEx's Docstring")
222       
223    def parse(self, root):
224       
225        self.xmlns = QName.getNs(root.tag)
226        version1_0attributeAuthorityURI = None
227       
228        for elem in root:
229            localName = QName.getLocalPart(elem.tag)
230            if localName == Target.URI_PATTERN_LOCALNAME:
231                self.uriPattern = elem.text.strip()
232                self.regEx = re.compile(self.uriPattern)
233               
234            elif localName == Target.ATTRIBUTES_LOCALNAME:
235                for attrElem in elem:
236                    if self.xmlns == Target.VERSION_1_1_XMLNS:
237                        self.attributes.append(Attribute.Parse(attrElem))
238                    else:
239                        attribute = Attribute()
240                        attribute.name = attrElem.text.strip()
241                        self.attributes.append(attribute)
242                   
243            elif localName == Target.ATTRIBUTE_AUTHORITY_LOCALNAME:
244                # Expecting first element to contain the URI
245                warnings.warn(
246                        Target.ATTRIBUTE_AUTHORITY_LOCALNAME_DEPRECATED_MSG,
247                        PendingDeprecationWarning)
248               
249                version1_0attributeAuthorityURI = elem[-1].text.strip()
250            else:
251                raise TargetParseError("Invalid Target attribute: %s" % 
252                                       localName)
253               
254        if self.xmlns == Target.VERSION_1_0_XMLNS:
255            msg = ("Setting all attributes with Attribute Authority "
256                   "URI set read using Version 1.0 schema.  This will "
257                   "be deprecated in future releases")
258           
259            warnings.warn(msg, PendingDeprecationWarning)
260            log.warning(msg)
261           
262            if version1_0attributeAuthorityURI is None:
263                raise TargetParseError("Assuming version 1.0 schema "
264                                       "for Attribute Authority URI setting "
265                                       "but no URI has been set")
266               
267            for attribute in self.attributes:
268                attribute.attributeAuthorityURI = \
269                    version1_0attributeAuthorityURI
270   
271    @classmethod
272    def Parse(cls, root):
273        resource = cls()
274        resource.parse(root)
275        return resource
276   
277    def __str__(self):
278        return str(self.uriPattern)
279
280
281class AttributeParseError(PolicyParseError):
282    """Error parsing a Policy Attribute element"""
283   
284
285class Attribute(PolicyComponent):
286    """encapsulate a target attribute including the name and an Attribute
287    Authority from which user attribute information may be queried
288    """
289    NAME_LOCALNAME = "Name"
290    ATTRIBUTE_AUTHORITY_URI_LOCALNAME = "AttributeAuthorityURI"
291   
292    __slots__ = ('__name', '__attributeAuthorityURI')
293   
294    def __init__(self):
295        super(Attribute, self).__init__()
296        self.__name = ''
297        self.__attributeAuthorityURI = None
298
299    def __str__(self):
300        return self.__name
301   
302    def _getName(self):
303        return self.__name
304
305    def _setName(self, value):
306        if not isinstance(value, basestring):
307            raise TypeError('Expecting string type for "name"; got %r' %
308                            type(value))
309        self.__name = value
310
311    name = property(fget=_getName, 
312                    fset=_setName, 
313                    doc="Attribute name")
314       
315    def _getAttributeAuthorityURI(self):
316        return self.__attributeAuthorityURI
317
318    def _setAttributeAuthorityURI(self, value):
319        self.__attributeAuthorityURI = value
320
321    attributeAuthorityURI = property(_getAttributeAuthorityURI, 
322                                     _setAttributeAuthorityURI, 
323                                     doc="Attribute Authority URI")
324       
325    def parse(self, root):
326        """Parse from an ElementTree Element"""
327        self.xmlns = QName.getNs(root.tag)
328       
329        for elem in root:
330            localName = QName.getLocalPart(elem.tag)
331            if localName == Attribute.ATTRIBUTE_AUTHORITY_URI_LOCALNAME:
332                self.attributeAuthorityURI = elem.text.strip()
333               
334            elif localName == Attribute.NAME_LOCALNAME:
335                self.name = elem.text.strip()
336            else:
337                raise AttributeParseError("Invalid Attribute element name: %s" % 
338                                          localName)
339   
340    @classmethod
341    def Parse(cls, root):
342        """Parse from an ElementTree Element and return a new instance"""
343        resource = cls()
344        resource.parse(root)
345        return resource
346
347
348class Subject(SubjectBase):
349    '''MSI Subject designator'''
350
351
352class Resource(_AttrDict):
353    '''Resource designator'''
354    namespaces = (
355        "urn:ndg:security:authz:1.0:attr:resource:uri",
356    )
357    (URI_NS,) = namespaces
358
359           
360class Request(object):
361    '''Request to send to a PDP'''
362    def __init__(self, subject=Subject(), resource=Resource()):
363        self.subject = subject
364        self.resource = resource
365
366    def _getSubject(self):
367        return self.__subject
368   
369    def _setSubject(self, subject):
370        if not isinstance(subject, Subject,):
371            raise TypeError("Expecting %s type for Request subject; got %r" %
372                            (Subject.__class__.__name__, subject))
373        self.__subject = subject
374
375    subject = property(fget=_getSubject,
376                       fset=_setSubject,
377                       doc="Subject type object representing subject accessing "
378                           "a resource")
379
380    def _getResource(self):
381        return self.__resource
382   
383    def _setResource(self, resource):
384        if not isinstance(resource, Resource):
385            raise TypeError("Expecting %s for Request Resource; got %r" %
386                            (Resource.__class__.__name__, resource))
387        self.__resource = resource
388
389    resource = property(fget=_getResource,
390                        fset=_setResource,
391                        doc="Resource to be protected")
392
393
394class Response(object):
395    '''Response from a PDP'''
396    decisionValues = range(4)
397    (DECISION_PERMIT,
398     DECISION_DENY,
399     DECISION_INDETERMINATE,
400     DECISION_NOT_APPLICABLE) = decisionValues
401
402    # string versions of the 4 Decision types used for encoding
403    DECISIONS = ("Permit", "Deny", "Indeterminate", "NotApplicable")
404   
405    decisionValue2String = dict(zip(decisionValues, DECISIONS))
406   
407    def __init__(self, status, message=None):
408        self.__status = None
409        self.__message = None
410       
411        self.status = status
412        self.message = message
413
414    def _setStatus(self, status):
415        if status not in Response.decisionValues:
416            raise TypeError("Status %s not recognised" % status)
417       
418        self.__status = status
419       
420    def _getStatus(self):
421        return self.__status
422   
423    status = property(fget=_getStatus,
424                      fset=_setStatus,
425                      doc="Integer response code; one of %r" % decisionValues)
426
427    def _setMessage(self, message):
428        if not isinstance(message, (basestring, type(None))):
429            raise TypeError('Expecting string or None type for "message"; got '
430                            '%r' % type(message))
431       
432        self.__message = message
433       
434    def _getMessage(self):
435        return self.__message
436   
437    message = property(fget=_getMessage,
438                       fset=_setMessage,
439                       doc="Optional message associated with response")
440
441           
442class PDP(object):
443    """Policy Decision Point"""
444   
445    def __init__(self, policy, pip):
446        """Read in a file which determines access policy"""
447        self.policy = policy
448        self.pip = pip
449
450    def _getPolicy(self):
451        if self.__policy is None:
452            raise TypeError("Policy object has not been initialised")
453        return self.__policy
454   
455    def _setPolicy(self, policy):
456        if not isinstance(policy, (Policy, None.__class__)):
457            raise TypeError("Expecting %s or None type for PDP policy; got %r"%
458                            (Policy.__class__.__name__, policy))
459        self.__policy = policy
460
461    policy = property(fget=_getPolicy,
462                      fset=_setPolicy,
463                      doc="Policy type object used by the PDP to determine "
464                          "access for resources")
465
466    def _getPIP(self):
467        if self.__pip is None:
468            raise TypeError("PIP object has not been initialised")
469       
470        return self.__pip
471   
472    def _setPIP(self, pip):
473        if not isinstance(pip, (PIPBase, None.__class__)):
474            raise TypeError("Expecting %s or None type for PDP PIP; got %r"%
475                            (PIPBase.__class__.__name__, pip))
476        self.__pip = pip
477
478    pip = property(fget=_getPIP,
479                   fset=_setPIP,
480                   doc="Policy Information Point - PIP type object used by "
481                       "the PDP to retrieve user attributes")
482   
483    def evaluate(self, request):
484        '''Make access control decision'''
485       
486        if not isinstance(request, Request):
487            raise TypeError("Expecting %s type for request; got %r" %
488                            (Request.__class__.__name__, request))
489       
490        # Look for matching targets to the given resource
491        resourceURI = request.resource[Resource.URI_NS]
492        matchingTargets = [target for target in self.policy.targets
493                           if target.regEx.match(resourceURI) is not None]
494        numMatchingTargets = len(matchingTargets)
495        if numMatchingTargets == 0:
496            log.debug("PDP.evaluate: granting access - no targets matched "
497                      "the resource URI path [%s]", 
498                      resourceURI)
499            return Response(status=Response.DECISION_PERMIT)
500       
501        # Iterate through matching targets checking for user access
502        request.subject[Subject.ROLES_NS] = []
503        permitForAllTargets = [Response.DECISION_PERMIT]*numMatchingTargets
504       
505        # Keep a look-up of the decisions for each target
506        status = []
507       
508        # Make a query object for querying the Policy Information Point
509        attributeQuery = PIPAttributeQuery()
510        attributeQuery[PIPAttributeQuery.SUBJECT_NS] = request.subject
511       
512        # Keep a cache of queried Attribute Authorities to avoid calling them
513        # multiple times
514        queriedAttributeAuthorityURIs = []
515       
516        # Iterate through the targets gathering user attributes from the
517        # relevant attribute authorities
518        for matchingTarget in matchingTargets:
519           
520            # Make call to the Policy Information Point to pull user
521            # attributes applicable to this resource
522            for attribute in matchingTarget.attributes:
523                if (attribute.attributeAuthorityURI in 
524                    queriedAttributeAuthorityURIs): 
525                    continue
526                         
527                attributeQuery[
528                    PIPAttributeQuery.ATTRIBUTEAUTHORITY_NS
529                ] = attribute.attributeAuthorityURI
530           
531                # Exit from function returning indeterminate status if a
532                # problem occurs here
533                try:
534                    attributeResponse = self.pip.attributeQuery(attributeQuery)
535                   
536                except SubjectRetrievalError, e:
537                    # i.e. a defined exception within the scope of this
538                    # module
539                    log.error("SAML Attribute Query %s: %s", 
540                              type(e), traceback.format_exc())
541                    return Response(Response.DECISION_INDETERMINATE, 
542                                    message=traceback.format_exc())
543                               
544                except Exception, e:
545                    log.error("SAML Attribute Query %s: %s", 
546                              type(e), traceback.format_exc())
547                    return Response(Response.DECISION_INDETERMINATE,
548                                    message="An internal error occurred")
549                               
550                # Accumulate attributes retrieved from multiple attribute
551                # authorities
552                request.subject[Subject.ROLES_NS] += attributeResponse[
553                                                            Subject.ROLES_NS]
554               
555            # Match the subject's attributes against the target
556            # One of any rule - at least one of the subject's attributes
557            # must match one of the attributes restricting access to the
558            # resource.
559            log.debug("PDP.evaluate: Matching subject attributes %r against "
560                      "resource attributes %r ...", 
561                      request.subject[Subject.ROLES_NS],
562                      matchingTarget.attributes)
563           
564            status.append(PDP._match(matchingTarget.attributes, 
565                                     request.subject[Subject.ROLES_NS]))
566           
567        # All targets must yield permit status for access to be granted
568        if status == permitForAllTargets:
569            return Response(Response.DECISION_PERMIT)
570        else:   
571            return Response(Response.DECISION_DENY,
572                            message="Insufficient privileges to access the "
573                                    "resource")
574       
575    @staticmethod
576    def _match(resourceAttr, subjectAttr):
577        """Helper method to iterate over user and resource attributes
578        If one at least one match is found, a permit response is returned
579        """
580        for attr in resourceAttr:
581            if attr.name in subjectAttr:
582                return Response.DECISION_PERMIT
583           
584        return Response.DECISION_DENY
585
586       
Note: See TracBrowser for help on using the repository browser.