source: TI12-security/trunk/NDG_XACML/ndg/xacml/core/context/pdp.py @ 6797

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/NDG_XACML/ndg/xacml/core/context/pdp.py@6797
Revision 6797, 17.3 KB checked in by pjkersha, 10 years ago (diff)

Added AttributeValueFactory? to generate <type>AttributeValue? classes.

RevLine 
[6734]1"""NDG Security Policy Decision Point type definition
[6643]2
[6734]3NERC DataGrid Project
4"""
5__author__ = "P J Kershaw"
6__date__ = "25/02/10"
7__copyright__ = "(C) 2010 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: $"
[6776]12import logging
13log = logging.getLogger(__name__)
[6734]14
[6776]15import traceback
[6771]16
[6796]17from ndg.xacml.core.context.exceptions import XacmlContextError
[6776]18from ndg.xacml.core.context.pdpinterface import PDPInterface
19from ndg.xacml.core.policy import Policy
[6783]20from ndg.xacml.core.apply import Apply
[6776]21from ndg.xacml.core.context.request import Request
22from ndg.xacml.core.context.response import Response
23from ndg.xacml.core.context.result import Result, Decision
[6783]24from ndg.xacml.core.context.result import StatusCode
[6777]25from ndg.xacml.core.functions import FunctionMap
[6797]26from ndg.xacml.core.exceptions import (UnsupportedStdFunctionError,
27                                       UnsupportedFunctionError)     
28       
[6783]29
[6796]30class PDPError(XacmlContextError):
[6783]31    """Base class for PDP class exceptions"""
[6796]32     
[6783]33   
34class UnsupportedElementError(PDPError):
35    """Element type is not supported in this implementation"""
36    def __init__(self, *arg, **kw):
37        super(UnsupportedElementError, self).__init__(*arg, **kw)
[6792]38        self.response.results[0].status.statusCode.value = \
39                                                    StatusCode.SYNTAX_ERROR
[6783]40
41
42class UnsupportedStdElementError(UnsupportedElementError):
[6790]43    """Element type is part of the XACML spec. but is not supported in this
[6783]44    implementation""" 
45   
46   
[6776]47class PDP(PDPInterface):
48    """A XACML Policy Decision Point implementation.  It supports the use of a
49    single policy but not policy sets
50    """
[6796]51    __slots__ = ('__policy', '__request')
[6776]52    TARGET_CHILD_ATTRS = ('subjects', 'resources', 'actions', 'environments')
[6771]53   
[6776]54    def __init__(self, policy=None):
55        """
56        @param policy: policy object for PDP to use to apply access control
57        decisions, may be omitted.
58        @type policy: ndg.xacml.core.policy.Policy / None
59        """
60        self.__policy = None
61        if policy is not None:
62            self.policy = policy
[6777]63           
[6790]64        self.__functionMap = FunctionMap.withLoadedMap()
[6796]65           
[6790]66        self.__request = None
[6777]67
[6771]68    @classmethod
[6777]69    def fromPolicySource(cls, source, readerFactory):
[6776]70        """Create a new PDP instance with a given policy
71        @param source: source for policy
72        @type source: type (dependent on the reader set, it could be for example
73        a file path string, file object, XML element instance)
[6777]74        @param readerFactory: reader factory returns the reader to use to read
75        this policy
76        @type readerFactory: ndg.xacml.parsers.AbstractReader derived type
77        """           
[6776]78        pdp = cls()
[6777]79        pdp.policy = Policy.fromSource(source, readerFactory)
[6776]80        return pdp
81   
82    @property
83    def policy(self):
84        """policy object for PDP to use to apply access control decisions"""
85        return self.__policy
86   
87    @policy.setter
88    def policy(self, value):
89        '''policy object for PDP to use to apply access control decisions'''
90        if not isinstance(value, Policy):
91            raise TypeError('Expecting %r derived type for "policy" input; got '
92                            '%r instead' % (Policy, type(value)))
93        self.__policy = value
[6790]94
95    @property
96    def request(self):
97        """request context"""
98        return self.__request
99   
100    @request.setter
101    def request(self, value):
102        '''request context'''
103        if not isinstance(value, Request):
104            raise TypeError('Expecting %r derived type for "request" '
105                            'input; got %r instead' % (Request, 
106                                                       type(value)))
107        self.__request = value
108                                       
[6771]109    def evaluate(self, request):
[6776]110        """Make an access control decision for the given request based on the
111        policy set
[6771]112       
[6776]113        @param request: XACML request context
[6771]114        @type request: ndg.xacml.core.context.request.Request
[6776]115        @return: XACML response instance
[6771]116        @rtype: ndg.xacml.core.context.response.Response
[6776]117        """
[6777]118        response = Response()
[6790]119        result = Result.createInitialised(decision=Decision.NOT_APPLICABLE)
[6776]120        response.results.append(result)
121       
[6790]122        try:
123            self.request = request
124           
125        except AttributeError, e:
126            log.error(str(e))
[6776]127            result.decision = Decision.INDETERMINATE
128            return response
129           
130        # Exception block around all rule processing in order to set
131        # INDETERMINATE response from any exceptions raised
132        try:
[6782]133            log.debug('Checking policy target for match with request...')
[6776]134           
[6790]135           
[6776]136            if not self.matchTarget(self.policy.target, request):
[6783]137                log.debug('No match for policy target setting %r decision',
[6776]138                          Decision.NOT_APPLICABLE_STR)
139               
140                result.decision = Decision.NOT_APPLICABLE
141                return response
142           
[6782]143            log.debug('Request matches the Policy target')
144           
[6776]145            # Check rules
[6790]146            ruleStatusValues = [False]*len(self.policy.rules)
147            for i, rule in enumerate(self.policy.rules):
[6776]148                log.debug('Checking policy rule %r for match...', rule.id)
149                if not self.matchTarget(rule.target, request):
150                    log.debug('No match to request context for target in rule '
151                              '%r', rule.id)
[6790]152                    ruleStatusValues[i] = True
[6782]153                    continue   
154               
155                # Apply the condition
[6790]156                ruleStatusValues[i] = self.evaluateCondition(rule.condition) 
[6783]157                     
158        except PDPError, e:
[6776]159            log.error('Exception raised evaluating request context, returning '
[6783]160                      '%r decision:%s', 
[6790]161                      e.response.results[0].decision, 
[6776]162                      traceback.format_exc())
[6783]163           
[6790]164            result = e.response.results[0]
[6776]165           
[6783]166        except Exception:
167            # Catch all so that nothing is handled from within the scope of this
168            # method
169            log.error('No PDPError type exception raised evaluating request '
170                      'context, returning %r decision:%s', 
171                      Decision.INDETERMINATE_STR, 
172                      traceback.format_exc()) 
173                       
174            result.decision = Decision.INDETERMINATE
[6790]175            result.status.statusCode.value = StatusCode.PROCESSING_ERROR
[6783]176           
[6776]177        return response
178           
179    def matchTarget(self, target, request):
180        """Generic method to match a <Target> element to the request context
181       
182        @param target: XACML target element
183        @type target: ndg.xacml.core.target.Target
184        @param request: XACML request context
185        @type request: ndg.xacml.core.context.request.Request
186        @return: True if request context matches the given target,
187        False otherwise
188        @rtype: bool
189        """
190        if target is None:
191            log.debug('No target set so no match with request context')
192            return False
193       
194        # From section 5.5 of the XACML 2.0 Core Spec:
195        #
196        # For the parent of the <Target> element to be applicable to the
197        # decision request, there MUST be at least one positive match between
198        # each section of the <Target> element and the corresponding section of
[6782]199        # the <xacml-context:Request> element.
200        #
201        # Also, 7.6:
202        #
203        # The target value SHALL be "Match" if the subjects, resources, actions
204        # and environments specified in the target all match values in the
205        # request context.
206        statusValues = [False]*len(self.__class__.TARGET_CHILD_ATTRS) 
207       
208        # Iterate for target subjects, resources, actions and environments
209        # elements
[6790]210        for i, attrName in enumerate(self.__class__.TARGET_CHILD_ATTRS):
[6782]211            # If any one of the <Target> children is missing then it counts as
212            # a match e.g. for <Subjects> child element - Section 5.5:
213            #
214            # <Subjects> [Optional] Matching specification for the subject
215            # attributes in the context. If this element is missing,
216            # then the target SHALL match all subjects.
[6790]217            targetElem = getattr(target, attrName)
[6782]218            if len(targetElem) == 0:
[6790]219                statusValues[i] = True
[6782]220                continue
221           
[6783]222            # Iterate over each for example, subject in the list of subjects:
223            # <Target>
224            #     <Subjects>
225            #          <Subject>
226            #              ...
227            #          </Subject>
228            #          <Subject>
229            #              ...
230            #          </Subject>
231            #     ...
[6790]232            # or resource in the list of resources and so on
[6782]233            for targetSubElem in targetElem:
234               
235                # For the given subject/resource/action/environment check for a
236                # match with the equivalent in the request
[6790]237                requestElem = getattr(request, attrName) 
[6782]238                for requestSubElem in requestElem:
239                    if self.matchTargetChild(targetSubElem, requestSubElem):
240                        # Within the list of e.g. subjects if one subject
241                        # matches then this counts as a subject match overall
242                        # for this target
[6790]243                        statusValues[i] = True
[6776]244 
[6782]245        # Target matches if all the children (i.e. subjects, resources, actions
246        # and environment sections) have at least one match.  Otherwise it
247        # doesn't count as a match
248        return all(statusValues)
[6776]249   
[6777]250    def matchTargetChild(self, targetChild, requestChild):
[6783]251        """Match a request child element (a <Subject>, <Resource>, <Action> or
252        <Environment>) with the corresponding target's <Subject>, <Resource>,
253        <Action> or <Environment>.
[6776]254       
255        @param targetChild: Target Subject, Resource, Action or Environment
256        object
257        @type targetChild: ndg.xacml.core.TargetChildBase
258        @param requestChild: Request Subject, Resource, Action or Environment
259        object
260        @type requestChild: ndg.xacml.core.context.RequestChildBase
261        @return: True if request context matches something in the target
262        @rtype: bool
[6783]263        @raise UnsupportedElementError: AttributeSelector processing is not
[6776]264        currently supported.  If an AttributeSelector is found in the policy,
265        this exception will be raised.
[6783]266        @raise UnsupportedStdFunctionError: policy references a function type
267        which is in the XACML spec. but is not supported by this implementation
268        @raise UnsupportedFunctionError: policy references a function type which
269        is not supported by this implementation
[6776]270        """
271        if targetChild is None:
272            # Default if target child is not set is to match all children
273            return True
274       
[6783]275        matchStatusValues = [True]*len(targetChild.matches)
276       
[6782]277        # Section 7.6
278        #
279        # A subject, resource, action or environment SHALL match a value in the
280        # request context if the value of all its <SubjectMatch>,
281        # <ResourceMatch>, <ActionMatch> or <EnvironmentMatch> elements,
[6783]282        # respectively, are "True".
283        #
284        # e.g. for <SubjectMatch>es in <Subject> ...
285        for childMatch, matchStatus in zip(targetChild.matches, 
286                                           matchStatusValues):
287           
[6777]288            # Get the match function from the Match ID
[6796]289            matchFunc = self.__functionMap.get(childMatch.matchId)
[6777]290            if matchFunc is NotImplemented:
[6783]291                raise UnsupportedStdFunctionError('No match function '
292                                                  'implemented for MatchId="%s"'
293                                                  % childMatch.matchId)
294            elif matchFunc is None:
295                raise UnsupportedFunctionError('Match function namespace %r is '
296                                               'not recognised' % 
297                                               childMatch.matchId)
[6777]298               
299            matchAttributeValue = childMatch.attributeValue.value
[6776]300           
301            # Create a match function based on the presence or absence of an
302            # AttributeDesignator or AttributeSelector
303            if childMatch.attributeDesignator is not None:
[6790]304                _attributeMatch = self.attributeDesignatorMatchFuncFactory(
305                                                matchFunc,
306                                                childMatch.attributeValue.value,
307                                                childMatch.attributeDesignator)
[6776]308               
309            elif childMatch.attributeSelector is not None:
310                # Nb. This will require that the request provide a reference to
311                # it's XML representation and an abstraction of the XML parser
312                # for executing XPath searches into that representation
[6783]313                raise UnsupportedElementError('This PDP implementation does '
314                                              'not support <AttributeSelector> '
315                                              'elements')
[6776]316            else:
317                _attributeMatch = lambda requestChildAttribute: (
[6780]318                    matchFunc.evaluate(matchAttributeValue, 
319                                    requestChildAttribute.attributeValue.value)
[6776]320                )
[6783]321       
322            # Iterate through each attribute in the request in turn matching it
323            # against the target using the generated _attributeMatch function
324            #
325            # Any Match element NOT matching will result in an overall status of
326            # no match.
327            #
328            # Continue iterating through the whole list even if a False status
329            # is found.  The other attributes need to be checked in case an
330            # error occurs.  In this case the top-level PDP exception handling
331            # block will catch it and set an overall decision of INDETERMINATE
332            attrMatchStatusValues = [False]*len(requestChild.attributes)
333           
334            for attribute, attrMatchStatus in zip(requestChild.attributes, 
335                                                  attrMatchStatusValues):
336                attrMatchStatus = _attributeMatch(attribute)
337                if attrMatchStatus == True:
[6782]338                    if log.getEffectiveLevel() <= logging.DEBUG:
339                        log.debug('Request attribute %r set to %r matches '
340                                  'target',
341                                  attribute.attributeId,
[6796]342                                  [a.value for a in attribute.attributeValues])
[6782]343                       
[6783]344            matchStatus = all(attrMatchStatusValues)
345       
346        # Any match => overall match     
347        return any(matchStatusValues)
[6790]348               
349    def attributeDesignatorMatchFuncFactory(self,
350                                            matchFunc,
351                                            matchAttributeValue, 
352                                            attributeDesignator):
353        """Define a match function to match a given request attribute against
354        the input attribute value and AttributeDesignator defined in a policy
355        target
356        """           
357        attributeId = attributeDesignator.attributeId
358        dataType = attributeDesignator.dataType
359       
360        # Issuer is an optional match - see core spec. 7.2.4
361        issuer = attributeDesignator.issuer
362        if issuer is not None:
363            # Issuer found - set lambda to match this against the
364            # issuer setting in the request
365            _issuerMatch = lambda requestChildIssuer: (
366                                                issuer == requestChildIssuer)
367        else:
368            # No issuer set - lambda returns True regardless
369            _issuerMatch = lambda requestChildIssuer: True
370       
371       
372        _attributeMatch = lambda attribute: (
[6792]373            any([matchFunc.evaluate(matchAttributeValue, attrVal.value) 
374                 for attrVal in attribute.attributeValues]) and
[6790]375            attribute.attributeId == attributeId and
376            attribute.dataType == dataType and
377            _issuerMatch(attribute.issuer)
378        )
379       
380        return _attributeMatch
381   
[6783]382    def evaluateCondition(self, condition):
[6790]383        """Evaluate a rule condition
384        @param condition: rule condition
385        @type condition: ndg.xacml.core.condition.Condition
386        @return: True/False status for whether the rule condition matched or
387        not
388        @rtype: bool
389        """
[6782]390       
[6783]391        # Spec:
392        #
393        # "The condition value SHALL be "True" if the <Condition> element is
394        # absent"
395        if condition is None:
396            return True
397       
398        applyElem = condition.expression
399        if not isinstance(applyElem, Apply):
400            raise UnsupportedElementError('%r type <Condition> sub-element '
401                                          'processing is not supported in this '
402                                          'implementation' % applyElem)
403         
[6796]404        result = applyElem.evaluate(self.request)
[6783]405
[6790]406
407
Note: See TracBrowser for help on using the repository browser.