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

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

Added new access control interface and functionality to OpenID Provider to enable a custom context object to be passed between login and logout calls.

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