source: TI12-security/trunk/python/ndg.security.common/ndg/security/common/authz/xacml/__init__.py @ 5375

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.common/ndg/security/common/authz/xacml/__init__.py@5375
Revision 5375, 25.3 KB checked in by pjkersha, 12 years ago (diff)

Refactored XACML code:

  • fixes to Policy.getInstance parser method
  • moving Factory and Evaluatable classes from cond module into their own modules
Line 
1"""XACML Package
2
3NERC DataGrid Project
4"""
5__author__ = "P J Kershaw"
6__date__ = "13/02/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
16from elementtree import ElementTree
17
18from ndg.security.common.authz.xacml.factory import FunctionFactory, \
19    UnknownIdentifierException, FunctionTypeException
20   
21import ndg.security.common.authz.xacml.cond
22#class FunctionFactory:
23#    pass
24
25
26# For parsing: ElementTree helpers
27getNs = lambda elem: elem.tag.split('}')[0][1:]
28getLocalName = lambda elem: elem.tag.rsplit('}',1)[-1]
29
30class XacmlBase(object):
31    pass
32
33class Subject(XacmlBase):
34    '''XACML Subject designator'''
35    def __init__(self, attributes={}):
36        self.attributes = attributes
37
38class Resource(XacmlBase):
39    '''XACML Resource designator'''
40
41class Action(XacmlBase):
42    '''XACML Action designator'''
43
44class Environment(XacmlBase):
45    '''XACML Environment designator'''
46
47class PolicySet(XacmlBase):
48    def __init__(self):
49          self.policies = []
50          self.combiningAlg = None
51         
52class Policy(XacmlBase):
53
54    def __init__(self,
55                 id='',
56                 ruleCombiningAlg=None,
57                 description='',
58                 target=None,
59                 rules=[],
60                 obligations=[]):
61          self.id = id
62          self.description = description
63          self.rules = rules
64          self.ruleCombiningAlg = ruleCombiningAlg
65          self.obligations = obligations
66          self.target = target
67
68    def encode(self):
69        '''Encode the policy'''
70        raise NotImplemented()
71   
72    @classmethod
73    def getInstance(cls, root=None, source=None):
74        """
75        @type root: ElementTree.Element
76        @param root: ElementTree root element
77        @type source: basestring / file like object
78        @param source: file path or file like object source of data
79        """
80        if root is None:
81            if source is None:
82                raise AttributeError('"root" or "source" keywords must be '
83                                     'provided')
84               
85            elem = ElementTree.parse(source)
86            root = elem.getroot()
87       
88        for elem in root:
89            localName = getLocalName(elem)
90            if localName == 'Description':
91                description = elem.text.strip()
92               
93            elif localName == 'Target':
94                target = Target.getInstance(elem)
95               
96            elif localName == 'Rule':
97                pass
98           
99        policy = cls(id=root.attrib['PolicyId'], 
100                     ruleCombiningAlg=root.attrib['RuleCombiningAlgId'],
101                     description=description,
102                     target=target,
103                     rules=rules,
104                     obligations=obligations)
105        return policy
106
107class MatchResult(XacmlBase):
108    pass
109
110class Target(XacmlBase):
111    '''The target selects policies relevant to a request'''
112
113    def __init__(self, subjects=None, resources=None, actions=None):
114          self.subjects = subjects
115          self.resources = resources
116          self.actions = actions
117          self.rules = []
118
119    def Match(self, evaluationCtx):
120          return MatchResult()
121       
122    @classmethod
123    def getInstance(cls, root):
124        '''Parse a Target from a given XML ElementTree element
125        '''
126        subjects = None
127        resources = None
128        actions = None
129       
130        for elem in root:
131            localName = getLocalName(elem)
132
133            if localName == "Subjects":
134                subjects = Target._getAttributes(elem, "Subject")
135               
136            elif localName == "Resources":
137                resources = Target._getAttributes(elem, "Resource")
138               
139            elif localName == "Actions":
140                actions = Target._getAttributes(elem, "Action")
141       
142        return cls(subjects=subjects, resources=resources, actions=actions)
143   
144    @staticmethod
145    def _getAttributes(root, prefix):
146        '''Helper method to get Target children elements'''
147        matches = []
148
149        for elem in root:
150            localName = getLocalName(elem)
151
152            if localName == prefix:
153                matches += Target._getMatches(elem, prefix)
154            elif localName == "Any" + prefix:
155                return None
156
157        return matches
158   
159    @staticmethod
160    def _getMatches(root, prefix):
161
162        list = []
163
164        for elem in root:
165            localName = getLocalName(elem)
166
167            if localName == prefix + "Match":
168                list += TargetMatch.getInstance(elem, prefix)
169
170        return tuple(list)
171   
172   
173class AttributeDesignator(XacmlBase):
174    elemNames = [n+'AttributeDesignator' for n in ('Action', 'Environment',
175                                                  'Resource', 'Subject')]
176    targetCodes = range(4)
177    targetLUT = dict(zip(elemNames, targetCodes))
178   
179    (ACTION_TARGET, 
180    ENVIRONMENT_TARGET, 
181    RESOURCE_TARGET, 
182    SUBJECT_TARGET) = targetCodes
183   
184    def __init__(self, target, type, id, mustBePresent=False, issuer=None):
185        if target not in AttributeDesignator.targetCodes:
186            raise AttributeError("Target code must be one of %r; input code "
187                                 "is %r" % (AttributeDesignator.targetCodes,
188                                            target))
189        self.target = target
190        self.type = type
191        self.id = id
192        self.mustBePresent = mustBePresent
193        self.issuer = issuer
194
195    @classmethod
196    def getInstance(cls, elem, target):
197        """Create a new instance from an ElementTree element
198        @type elem: ElementTree.Element
199        @param elem: AttributeDesignator XML element
200        @type target: int
201        @param target: target code
202        @rtype: AttributeDesignator
203        @return: new AttributeDesignator instance
204        """
205        localName = getLocalName(elem)
206        if localName not in cls.elemNames:
207            raise AttributeError("Element name [%s] is not a recognised "
208                                 "AttributeDesignator name %r" % 
209                                 (localName, cls.elemNames))
210           
211       
212        if target not in cls.targetCodes:
213            raise AttributeError("Target code [%d] is not a recognised "
214                                 "AttributeDesignator target code %r" % 
215                                 (localName, cls.targetCodes))
216           
217        id = elem.attrib['AttributeId']
218        type = elem.attrib['DataType']
219        mustBePresent=elem.attrib.get('mustBePresent','false').lower()=='true'
220        issuer = elem.attrib.get('issuer')
221        return cls(target, type, id, mustBePresent=mustBePresent,issuer=issuer)
222   
223   
224class TargetMatch(XacmlBase):
225    '''Represents the SubjectMatch, ResourceMatch, or ActionMatch XML
226    types in XACML, depending on the value of the type field. This is the
227    part of the Target that actually evaluates whether the specified
228    attribute values in the Target match the corresponding attribute
229    values in the request context.
230    '''
231    types = range(3)
232    SUBJECT, RESOURCE, ACTION = types
233   
234    def __init__(self,
235                 type,
236                 function,
237                 eval,
238                 attributeValue):
239        '''Create a TargetMatch from components.
240         
241        @param type an integer indicating whether this class represents a
242        SubjectMatch, ResourceMatch, or ActionMatch
243        @param function the Function that represents the MatchId
244        @param eval the AttributeDesignator or AttributeSelector to be used to
245        select attributes from the request context
246        @param attrValue the AttributeValue to compare against
247        @raise TypeError if the input type isn't a valid value
248        '''
249        if type not in self.__class__.types:
250            raise TypeError("Type is [%d] but it must be one of %r" % 
251                            (type, self.__class__.types))
252        self.type = type
253        self.function = function
254        self.eval = eval
255        self.attrValue = attributeValue
256
257    def _getType(self):
258        return getattr(self, '_type', None)
259   
260    def _setType(self, type):
261        if type not in self.__class__.types:
262            raise TypeError('Type value "%d" not recognised, expecting one of '
263                            '%r types' % (type, self.__class__.types))
264        self._type = type
265       
266    type = property(fget=_getType, fset=_setType, 
267                    doc="the type of match for this target")
268   
269    @classmethod
270    def getInstance(cls, root, prefix):
271        '''Creates a TargetMatch by parsing a node, using the
272        input prefix to determine whether this is a SubjectMatch,
273        ResourceMatch, or ActionMatch.
274     
275        @param root the node to parse for the TargetMatch
276        @param prefix a String indicating what type of TargetMatch
277        to instantiate (Subject, Resource, or Action)
278        @param xpathVersion the XPath version to use in any selectors, or
279        null if this is unspecified (ie, not supplied in
280        the defaults section of the policy)
281
282        @return a new TargetMatch constructed by parsing
283        '''
284
285        type = ["Subject", "Resource", "Action"].index(prefix)
286        if type not in cls.types:
287            raise TypeError("Unknown TargetMatch type: %s" % prefix)
288
289        # function type
290        funcId = root.attrib["MatchId"]
291        factory = FunctionFactory.getTargetInstance()
292        try:
293            function = factory.createFunction(funcId)
294        except UnknownIdentifierException, e:
295            raise ParsingException("Unknown MatchId: %s" % e)
296       
297        except FunctionTypeException, e:
298            # try to create an abstract function
299            try:
300                function = factory.createAbstractFunction(funcId, root)
301            except Exception, e:
302                raise ParsingException("invalid abstract function: %s" % e)
303           
304        # Get the designator or selector being used, and the attribute
305        # value paired with it
306        for elem in root:
307            localName = getLocalName(elem)
308
309            if localName == prefix + "AttributeDesignator":
310                eval = AttributeDesignator.getInstance(elem, type)
311               
312            elif localName == "AttributeSelector":
313                eval = AttributeSelector.getInstance(elem)
314               
315            elif localName == "AttributeValue":
316                try:
317                    attrValue = attrFactory.createValue(elem)
318                except UnknownIdentifierException, e:
319                    raise ParsingException("Unknown Attribute Type: %s" % e)
320
321        # finally, check that the inputs are valid for this function
322        inputs = [attrValue, eval]
323        function.checkInputsNoBag(inputs)
324       
325        return cls(type, function, eval, attributeValue)
326   
327
328    def match(self, context):
329        '''determines whether this TargetMatch matches
330        the input request (whether it is applicable)
331
332        @param context the representation of the request
333
334        @return the result of trying to match the TargetMatch and the request
335        '''
336       
337        result = eval.evaluate(context)
338       
339        if result.indeterminate():
340            # in this case, we don't ask the function for anything, and we
341            # simply return INDETERMINATE
342            return MatchResult(MatchResult.INDETERMINATE, result.getStatus())
343       
344
345        bag = result.getAttributeValue()
346
347        if not bag.isEmpty():
348           
349            # we got back a set of attributes, so we need to iterate through
350            # them, seeing if at least one matches
351            it = bag.iterator()
352            atLeastOneError = False
353            firstIndeterminateStatus = None
354
355            while it.hasNext():
356                inputs = []
357
358                inputs.add(attrValue)
359                inputs.add(it.next())
360
361                # do the evaluation
362                match = evaluateMatch(inputs, context)
363               
364                # we only need one match for this whole thing to match
365                if match.getResult() == MatchResult.MATCH:
366                    return match
367
368                # if it was INDETERMINATE, we want to remember for later
369                if match.getResult() == MatchResult.INDETERMINATE:
370                    atLeastOneError = True
371
372                    # there are no rules about exactly what status data
373                    # should be returned here, so like in the combining
374                    # also, we'll just track the first error
375                    if firstIndeterminateStatus == None:
376                        firstIndeterminateStatus = match.getStatus()
377
378            # if we got here, then nothing matched, so we'll either return
379            # INDETERMINATE or NO_MATCH
380            if atLeastOneError:
381                return MatchResult(MatchResult.INDETERMINATE,
382                                       firstIndeterminateStatus)
383            else:
384                return MatchResult(MatchResult.NO_MATCH)
385
386        else:
387            # this is just an optimization, since the loop above will
388            # actually handle this case, but this is just a little
389            # quicker way to handle an empty bag
390            return MatchResult(MatchResult.NO_MATCH)
391   
392    def evaluateMatch(self, inputs, context):
393        '''Private helper that evaluates an individual match'''
394       
395        # evaluate the function
396        result = function.evaluate(inputs, context)
397
398        # if it was indeterminate, then that's what we return immediately
399        if result.indeterminate():
400            return MatchResult(MatchResult.INDETERMINATE,
401                               result.getStatus())
402
403        # otherwise, we figure out if it was a match
404        bool = result.getAttributeValue()
405
406        if bool.getValue():
407            return MatchResult(MatchResult.MATCH)
408        else:
409            return MatchResult(MatchResult.NO_MATCH)
410
411    def encode(self, output, indenter=None):
412        '''Encodes this TargetMatch into its XML representation
413        and writes this encoding to the given OutputStream with no
414        indentation.
415        @param output a stream into which the XML-encoded data is written'''
416        raise NotImplementedError()
417   
418   
419class Status(XacmlBase):
420    STATUS_MISSING_ATTRIBUTE = \
421          "urn:oasis:names:tc:xacml:1.0:status:missing-attribute"
422    STATUS_OK = "urn:oasis:names:tc:xacml:1.0:status:ok"
423    STATUS_PROCESSING_ERROR = \
424          "urn:oasis:names:tc:xacml:1.0:status:processing-error"
425    STATUS_SYNTAX_ERROR = \
426          "urn:oasis:names:tc:xacml:1.0:status:syntax-error" 
427     
428class EvaluationResult(XacmlBase):
429    def __init__(self, 
430                 attributeValue=None, 
431                 status=None, 
432                 indeterminate=False):
433        self.status = status
434        self.attributeValue = attributeValue
435        self.indeterminate = indeterminate
436     
437
438class Effect(XacmlBase):
439    def __str__(self):
440        raise NotImplementedError()
441
442             
443class DenyEffect(Effect):
444    def __str__(self):
445        return 'deny'
446         
447class PermitEffect(Effect):
448    def __str__(self):
449        return 'permit'
450
451class Rule(XacmlBase):
452    '''Consists of a condition, an effect, and a target.
453    '''
454    def __init__(self, conditions=[], effect=DenyEffect(), target=None):
455        # Conditions are statements about attributes that upon evaluation
456        # return either True, False, or Indeterminate.
457        self.conditions = conditions
458       
459        # Effect is the intended consequence of the satisfied rule. It can
460        # either take the value Permit or Deny.
461        self.effect = effect
462     
463        # Target, as in the case of a policy, helps in determining whether or
464        # not a rule is relevant for a request. The mechanism for achieving
465        # this is also similar to how it is done in the case of a target for a
466        # policy.
467        self.target = target
468       
469    @classmethod
470    def getInstance(cls, elem):
471        root = elem.getroot()
472        return Rule(conditions=conditions, effect=effect, target=target)
473         
474class Attribute(XacmlBase):
475    def __init__(self, id, type=None, issuer=None, issueInstant=None, 
476                 value=None):
477        self.id = id
478        self.type = type or value.__class__
479        self.issuer = issuer
480        self.issueInstant = issueInstant
481        self.value = value
482
483       
484class Request(XacmlBase):
485    '''XACML Request XacmlBase
486   
487    TODO: refactor from this initial placeholder'''
488    def __init__(self, subject, resource, action=None, environment={}):
489          self.subject = subject
490          self.resource = resource
491          self.action = action
492          self.environment = environment
493
494class Response(XacmlBase):
495    pass
496
497
498class PDP(XacmlBase):
499    '''Modify PDPInterface to use the four XACML request designators: subject,
500    resource, action and environment
501   
502    This is an initial iteration toward a complete XACML implementation'''
503    def __init__(self, *arg, **kw):
504          pass
505   
506    def evaluate(self, request):
507          '''Make access control decision - override this in a derived class to
508          implement the decision logic but this method may be called within
509          the derived method to check input types
510         
511          @param request: request object containing the subject, resource,
512          action and environment
513          @type request: ndg.security.common.authz.xacml.Request
514          @return reponse object
515          @rtype: ndg.security.common.authz.xacml.Response
516          '''
517          raise NotImplementedError()
518
519
520class RuleCombiningAlg(XacmlBase):
521    id = None
522
523class DenyOverrides(RuleCombiningAlg):
524   '''Deny-overrides: If any rule evaluates to Deny, then the final
525   authorization decision is also Deny.'''
526   id = 'Deny-overrides'
527   
528class OrderedDenyOverrides(RuleCombiningAlg):
529    '''Ordered-deny-overrides: Same as deny-overrides, except the order in
530    which relevant rules are evaluated is the same as the order in which they
531    are added in the policy.'''
532    id = 'Ordered-deny-overrides'
533   
534class PermitOverrides(RuleCombiningAlg):
535    '''Permit-overrides: If any rule evaluates to Permit, then the final
536    authorization decision is also Permit.'''
537   
538class OrderedPermitOverrides(RuleCombiningAlg):
539    '''Ordered-permit-overrides: Same as permit-overrides, except the order in
540    which relevant rules are evaluated is the same as the order in which they
541    are added in the policy.'''
542    id = 'Ordered-permit-overrides'
543   
544class FirstApplicable(RuleCombiningAlg):
545    '''First-applicable: The result of the first relevant rule encountered is
546    the final authorization decision as well.'''
547    id = 'First-applicable'
548
549
550class EvaluationCtx(object):
551
552    # The standard URI for listing a resource's id
553    RESOURCE_ID ="urn:oasis:names:tc:xacml:1.0:resource:resource-id"
554
555    # The standard URI for listing a resource's scope
556    RESOURCE_SCOPE = "urn:oasis:names:tc:xacml:1.0:resource:scope"
557
558    # Resource scope of Immediate (only the given resource)
559    SCOPE_IMMEDIATE = 0
560
561    # Resource scope of Children (the given resource and its direct
562    # children)
563    SCOPE_CHILDREN = 1
564
565    # Resource scope of Descendants (the given resource and all descendants
566    # at any depth or distance)
567    SCOPE_DESCENDANTS = 2
568   
569    def getRequestRoot(self):
570        '''Returns the DOM root of the original RequestType XML document, if
571        this context is backed by an XACML Request. If this context is not
572        backed by an XML representation, then an exception is thrown.'''
573        raise NotImplementedError()
574
575    def getResourceId(self):
576        '''Returns the identifier for the resource being requested.'''
577        raise NotImplementedError()
578
579    def getScope(self):
580        '''Returns the resource scope, which will be one of the three fields
581        denoting Immediate, Children, or Descendants.'''
582        raise NotImplementedError()
583
584    def setResourceId(self, resourceId):
585        '''Changes the value of the resource-id attribute in this context. This
586        is useful when you have multiple resources (ie, a scope other than
587        IMMEDIATE), and you need to keep changing only the resource-id to
588        evaluate the different effective requests.'''
589        raise NotImplementedError()
590
591    def getCurrentTime(self):
592        '''Returns the cached value for the current time. If the value has
593        never been set by a call to setCurrentTime, or if caching
594        is not enabled in this instance, then this will return null.'''
595        raise NotImplementedError()
596
597    def setCurrentTime(self, currentTime):
598        '''Sets the current time for this evaluation. If caching is not enabled
599        for this instance then the value is ignored.
600     
601        @param currentTime the dynamically resolved current time'''
602        raise NotImplementedError()
603
604    def getCurrentDate(self):
605        '''Returns the cached value for the current date. If the value has
606        never been set by a call to setCurrentDate, or if caching
607        is not enabled in this instance, then this will return null.'''
608        raise NotImplementedError()
609
610    def setCurrentDate(self, currentDate):
611        '''Sets the current date for this evaluation. If caching is not enabled
612        for this instance then the value is ignored.'''
613        raise NotImplementedError()
614
615    def getCurrentDateTime(self):
616        '''Returns the cached value for the current dateTime. If the value has
617        never been set by a call to setCurrentDateTime, or if
618        caching is not enabled in this instance, then this will return null.
619        '''
620        raise NotImplementedError()
621
622    def setCurrentDateTime(self, currentDateTime):
623        '''Sets the current dateTime for this evaluation. If caching is not
624        enabled for this instance then the value is ignored.
625     
626        @param currentDateTime the dynamically resolved current dateTime'''
627        raise NotImplementedError()
628
629    def getSubjectAttribute(self, type, id, category):
630        '''Returns available subject attribute value(s) ignoring the issuer.
631     
632        @param type the type of the attribute value(s) to find
633        @param id the id of the attribute value(s) to find
634        @param category the category the attribute value(s) must be in
635     
636        @return a result containing a bag either empty because no values were
637        found or containing at least one value, or status associated with an
638        Indeterminate result'''
639        raise NotImplementedError()
640
641    def getSubjectAttribute(self, type, id, issuer, category):
642        '''Returns available subject attribute value(s).
643     
644        @param type the type of the attribute value(s) to find
645        @param id the id of the attribute value(s) to find
646        @param issuer the issuer of the attribute value(s) to find or null
647        @param category the category the attribute value(s) must be in
648     
649        @return a result containing a bag either empty because no values were
650        found or containing at least one value, or status associated with an
651        Indeterminate result'''
652        raise NotImplementedError()
653   
654    def getResourceAttribute(self, type, id, issuer):
655        '''Returns available resource attribute value(s).
656     
657        @param type the type of the attribute value(s) to find
658        @param id the id of the attribute value(s) to find
659        @param issuer the issuer of the attribute value(s) to find or null
660     
661        @return a result containing a bag either empty because no values were
662        found or containing at least one value, or status associated with an
663        Indeterminate result'''
664        raise NotImplementedError()
665
666    def getActionAttribute(self, type, id, issuer):
667        '''Returns available action attribute value(s).
668     
669        @param type the type of the attribute value(s) to find
670        @param id the id of the attribute value(s) to find
671        @param issuer the issuer of the attribute value(s) to find or null
672     
673        @return a result containing a bag either empty because no values were
674        found or containing at least one value, or status associated with an
675        Indeterminate result'''
676        raise NotImplementedError()
677
678    def getEnvironmentAttribute(self, type, id, issuer):
679        '''Returns available environment attribute value(s).
680     
681        @param type the type of the attribute value(s) to find
682        @param id the id of the attribute value(s) to find
683        @param issuer the issuer of the attribute value(s) to find or null
684     
685        @return a result containing a bag either empty because no values were
686        found or containing at least one value, or status associated with an
687        Indeterminate result'''
688        raise NotImplementedError()
689
690    def getAttribute(self, contextPath, namespaceNode, type, xpathVersion):
691        '''Returns the attribute value(s) retrieved using the given XPath
692        expression.
693     
694        @param contextPath the XPath expression to search
695        @param namespaceNode the DOM node defining namespace mappings to use,
696                            or null if mappings come from the context root
697        @param type the type of the attribute value(s) to find
698        @param xpathVersion the version of XPath to use
699     
700        @return a result containing a bag either empty because no values were
701       
702        found or containing at least one value, or status associated with an
703        Indeterminate result'''
704        raise NotImplementedError()
Note: See TracBrowser for help on using the repository browser.