source: TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/xacml/ctx_handler/saml_ctx_handler.py @ 7705

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/xacml/ctx_handler/saml_ctx_handler.py
Revision 7705, 21.3 KB checked in by pjkersha, 9 years ago (diff)

Working e2e tests with ESGF Group/Role? AttributeValue? integrated into SAML Attribute Queries and XACML Policy - ndg.security.test.wsgi.authz.test_authz unit test.

  • Property svn:keywords set to Id
Line 
1"""XACML Context handler translates to and from SAML Authorisation Decision
2Query / Response
3
4"""
5__author__ = "P J Kershaw"
6__date__ = "14/05/10"
7__copyright__ = "(C) 2010 Science and Technology Facilities Council"
8__license__ = "BSD - see LICENSE file in top-level directory"
9__contact__ = "Philip.Kershaw@stfc.ac.uk"
10__revision__ = '$Id$'
11import logging
12log = logging.getLogger(__name__)
13
14from os import path
15from ConfigParser import SafeConfigParser, ConfigParser
16from datetime import datetime, timedelta
17from uuid import uuid4
18
19from ndg.saml.saml2 import core as _saml
20from ndg.saml.common import SAMLVersion
21
22
23from ndg.xacml.core import Identifiers
24from ndg.xacml.core.context.pdp import PDP
25from ndg.xacml.core import context as _xacmlContext
26from ndg.xacml.core.attribute import Attribute as XacmlAttribute
27from ndg.xacml.core.attributevalue import (
28    AttributeValueClassFactory as XacmlAttributeValueClassFactory, 
29    AttributeValue as XacmlAttributeValue)
30from ndg.xacml.parsers.etree.factory import ReaderFactory as \
31    XacmlPolicyReaderFactory
32
33from ndg.security.server.xacml.pip.saml_pip import PIP
34from ndg.security.common.utils.factory import importModuleObject
35
36
37class SamlPEPRequest(object):
38    """Helper class for SamlCtxHandler.handlePEPRequest"""
39    __slots__ = ('__authzDecisionQuery', '__response', '__policyFilePath')
40   
41    def __init__(self):
42        self.__authzDecisionQuery = None
43        self.__response = None
44   
45    def _getAuthzDecisionQuery(self):
46        return self.__authzDecisionQuery
47
48    def _setAuthzDecisionQuery(self, value):
49        if not isinstance(value, _saml.AuthzDecisionQuery):
50            raise TypeError('Expecting %r type for "response" attribute, got %r'
51                            % (_saml.Response, type(value)))
52        self.__authzDecisionQuery = value
53       
54    authzDecisionQuery = property(_getAuthzDecisionQuery, 
55                                  _setAuthzDecisionQuery, 
56                                  doc="SAML Authorisation Decision Query")
57
58    def _getResponse(self):
59        return self.__response
60
61    def _setResponse(self, value):
62        if not isinstance(value, _saml.Response):
63            raise TypeError('Expecting %r type for "response" attribute, got %r'
64                            % (_saml.Response, type(value)))
65        self.__response = value
66
67    response = property(_getResponse, _setResponse, doc="SAML Response")
68   
69       
70class SamlCtxHandler(_xacmlContext.handler.CtxHandlerBase):
71    """XACML Context handler for accepting SAML 2.0 based authorisation
72    decision queries and interfacing to a PEP with SAML based Attribute Query
73    Interface
74    """
75    DEFAULT_OPT_PREFIX = 'saml_ctx_handler.'
76    PIP_OPT_PREFIX = 'pip.'
77   
78    __slots__ = (
79        '__policyFilePath',
80        '__issuerProxy', 
81        '__assertionLifetime',
82        '__xacmlExtFunc'
83    )
84   
85    def __init__(self):
86        super(SamlCtxHandler, self).__init__()
87       
88        # Proxy object for SAML AuthzDecisionQueryResponse Issuer attributes. 
89        # By generating a proxy the Response objects inherent attribute
90        # validation can be applied to Issuer related config parameters before
91        # they're assigned to the response issuer object generated in the
92        # authorisation decision query response
93        self.__issuerProxy = _saml.Issuer()
94        self.__assertionLifetime = 0.
95        self.__policyFilePath = None
96        self.__xacmlExtFunc = None
97
98    def _getXacmlExtFunc(self):
99        """Get XACML extensions function"""
100        return self.__xacmlExtFunc
101
102    def _setXacmlExtFunc(self, value):
103        """Set XACML extensions function"""
104        if isinstance(value, basestring):
105            self.__xacmlExtFunc = importModuleObject(value)
106           
107        elif callable(value):
108            self.__xacmlExtFunc = value
109           
110        else:
111            raise TypeError('Expecting module object import path string or '
112                            'callable; got %r' % type(value))
113           
114    xacmlExtFunc = property(_getXacmlExtFunc, _setXacmlExtFunc, 
115                            doc="Function or other callable which will be "
116                                "called to set any XACML specific "
117                                "extensions such as new custom attribute value "
118                                "types.  The function should accept no input "
119                                "arguments and any return value is ignored")   
120   
121    def load(self):
122        """Load Policy file, mapping file and extensions function.  In each case
123        load only if they're set
124        """ 
125        # This must be called first before the policy is loaded so that any
126        # new custom types are added before a parse is attempted.         
127        if self.xacmlExtFunc:
128            self.xacmlExtFunc()
129           
130        if self.policyFilePath:
131            self.pdp = PDP.fromPolicySource(self.policyFilePath, 
132                                            XacmlPolicyReaderFactory)
133       
134        if self.pip.mappingFilePath:
135            self.pip.readMappingFile()
136       
137    @classmethod
138    def fromConfig(cls, cfg, **kw):
139        '''Alternative constructor makes object from config file settings
140        @type cfg: basestring /ConfigParser derived type
141        @param cfg: configuration file path or ConfigParser type object
142        @rtype: ndg.security.server.xacml.ctx_handler.saml_ctx_handler
143        @return: new instance of this class
144        '''
145        obj = cls()
146        obj.parseConfig(cfg, **kw)
147       
148        # Post initialisation steps - load policy and PIP mapping file
149        obj.load()
150           
151        return obj
152
153    def parseConfig(self, cfg, prefix=DEFAULT_OPT_PREFIX, section='DEFAULT'):
154        '''Read config settings from a file, config parser object or dict
155       
156        @type cfg: basestring / ConfigParser derived type / dict
157        @param cfg: configuration file path or ConfigParser type object
158        @type prefix: basestring
159        @param prefix: prefix for option names e.g. "attributeQuery."
160        @type section: basetring
161        @param section: configuration file section from which to extract
162        parameters.
163        ''' 
164        if isinstance(cfg, basestring):
165            cfgFilePath = path.expandvars(cfg)
166           
167            # Add a 'here' helper option for setting dir paths in the config
168            # file
169            hereDir = path.abspath(path.dirname(cfgFilePath))
170            _cfg = SafeConfigParser(defaults={'here': hereDir})
171           
172            # Make option name reading case sensitive
173            _cfg.optionxform = str
174            _cfg.read(cfgFilePath)
175            items = _cfg.items(section)
176           
177        elif isinstance(cfg, ConfigParser):
178            items = cfg.items(section)
179         
180        elif isinstance(cfg, dict):
181            items = cfg.items()     
182        else:
183            raise AttributeError('Expecting basestring, ConfigParser or dict '
184                                 'type for "cfg" attribute; got %r type' % 
185                                 type(cfg))
186       
187        self.__parseFromItems(items, prefix=prefix)
188       
189    def __parseFromItems(self, items, prefix=DEFAULT_OPT_PREFIX): 
190        """Update from list of tuple name, value pairs - for internal use
191        by parseKeywords and parseConfig
192        """
193        prefixLen = len(prefix) 
194        pipPrefix = self.__class__.PIP_OPT_PREFIX
195        pipPrefixLen = len(pipPrefix)
196       
197        def _setAttr(__optName):
198            """Convenience function to check for PIP attribute related items
199            """
200            if __optName.startswith(pipPrefix):
201                if self.pip is None:   
202                    # Create Policy Information Point so that settings can be
203                    # assigned
204                    self.pip = PIP()
205                   
206                setattr(self.pip, __optName[pipPrefixLen:], val)
207            else:
208                setattr(self, __optName, val)
209               
210        for optName, val in items:
211            if prefix:
212                # Filter attributes based on prefix
213                if optName.startswith(prefix):
214                    _optName = optName[prefixLen:]
215                    _setAttr(_optName)
216            else:
217                # No prefix set - attempt to set all attributes   
218                _setAttr(optName)
219       
220    def parseKeywords(self, prefix=DEFAULT_OPT_PREFIX, **kw):
221        """Update object from input keywords
222       
223        @type prefix: basestring
224        @param prefix: if a prefix is given, only update self from kw items
225        where keyword starts with this prefix
226        @type kw: dict
227        @param kw: items corresponding to class instance variables to
228        update.  Keyword names must match their equivalent class instance
229        variable names.  However, they may prefixed with <prefix>
230        """
231        self.__parseFromItems(kw.items(), prefix=prefix)
232               
233    @classmethod
234    def fromKeywords(cls, prefix=DEFAULT_OPT_PREFIX, **kw):
235        """Create a new instance initialising instance variables from the
236        keyword inputs
237        @type prefix: basestring
238        @param prefix: if a prefix is given, only update self from kw items
239        where keyword starts with this prefix
240        @type kw: dict
241        @param kw: items corresponding to class instance variables to
242        update.  Keyword names must match their equivalent class instance
243        variable names.  However, they may prefixed with <prefix>
244        @return: new instance of this class
245        @rtype: ndg.saml.saml2.binding.soap.client.SOAPBinding or derived type
246        """
247        obj = cls()
248        obj.parseKeywords(prefix=prefix, **kw)
249       
250        # Post initialisation steps - load policy and PIP mapping file
251        obj.load()
252                       
253        return obj
254                                       
255    def _getPolicyFilePath(self):
256        return self.__policyFilePath
257
258    def _setPolicyFilePath(self, value):
259        if not isinstance(value, basestring):
260            raise TypeError('Expecting string type for "policyFilePath"; got '
261                            '%r' % type(value))
262        self.__policyFilePath = path.expandvars(value)
263
264    policyFilePath = property(_getPolicyFilePath, 
265                              _setPolicyFilePath, 
266                              doc="Policy file path for policy used by the PDP")
267       
268    def _getIssuerFormat(self):
269        if self.__issuerProxy is None:
270            return None
271        else:
272            return self.__issuerProxy.format
273
274    def _setIssuerFormat(self, value):
275        if self.__issuerProxy is None:
276            self.__issuerProxy = _saml.Issuer()
277           
278        self.__issuerProxy.format = value
279
280    issuerFormat = property(_getIssuerFormat, _setIssuerFormat, 
281                            doc="Issuer format of SAML Authorisation Query "
282                                "Response")
283
284    def _getIssuerName(self):
285        if self.__issuerProxy is None:
286            return None
287        else:
288            return self.__issuerProxy.value
289
290    def _setIssuerName(self, value):
291        if self.__issuerProxy is None:
292            self.__issuerProxy = _saml.Issuer()
293           
294        self.__issuerProxy.value = value
295
296    issuerName = property(_getIssuerName, _setIssuerName, 
297                          doc="Name of issuer of SAML Authorisation Query "
298                              "Response")
299   
300    _getAssertionLifetime = lambda self: self.__assertionLifetime
301   
302    def _setAssertionLifetime(self, value):
303        if isinstance(value, (int, float, long, basestring)):
304            self.__assertionLifetime = float(value)
305        else:
306            raise TypeError('Expecting int, long, float or string type for '
307                            '"assertionLifetime" attribute; got %s instead' % 
308                            type(value))
309
310    assertionLifetime = property(fget=_getAssertionLifetime,
311                                 fset=_setAssertionLifetime,
312                                 doc="lifetime of assertion in seconds used to "
313                                     "set assertion conditions notOnOrAfter "
314                                     "time")
315 
316    def handlePEPRequest(self, pepRequest):
317        """Handle request from Policy Enforcement Point
318       
319        @param pepRequest: request containing a SAML authorisation decision
320        query and optionally an initialised SAML response object
321        @type pepRequest: ndg.security.server.xacml.saml_ctx_handler.SamlPEPRequest
322        @return: SAML authorisation decision response
323        @rtype: ndg.saml.saml2.core.Response
324        """
325        samlAuthzDecisionQuery = pepRequest.authzDecisionQuery
326       
327        xacmlRequest = self._createXacmlRequestCtx(samlAuthzDecisionQuery)
328       
329        # Add a reference to this context so that the PDP can invoke queries
330        # back to the PIP
331        xacmlRequest.ctxHandler = self
332       
333        # Call the PDP
334        xacmlResponse = self.pdp.evaluate(xacmlRequest)
335       
336        # Create the SAML Response
337        samlResponse = self._createSAMLResponseAssertion(samlAuthzDecisionQuery,
338                                                         pepRequest.response)
339       
340        # Assume only a single assertion authorisation decision statements
341        samlAuthzDecisionStatement = samlResponse.assertions[0
342                                                ].authzDecisionStatements[0]
343       
344        # Convert the decision status
345        if (xacmlResponse.results[0].decision == 
346            _xacmlContext.result.Decision.PERMIT):
347            log.info("PDP granted access for URI path [%s]", 
348                     samlAuthzDecisionQuery.resource)
349           
350            samlAuthzDecisionStatement.decision = _saml.DecisionType.PERMIT
351       
352        # Nb. Mapping XACML NotApplicable => SAML INDETERMINATE
353        elif (xacmlResponse.results[0].decision in 
354              (_xacmlContext.result.Decision.INDETERMINATE,
355               _xacmlContext.result.Decision.NOT_APPLICABLE)):
356            log.info("PDP returned a status of [%s] for URI path [%s]; "
357                     "mapping to SAML response [%s] ...", 
358                     xacmlResponse.results[0].decision,
359                     samlAuthzDecisionQuery.resource,
360                     _saml.DecisionType.INDETERMINATE) 
361           
362            samlAuthzDecisionStatement.decision = \
363                                                _saml.DecisionType.INDETERMINATE
364        else:
365            log.info("PDP returned a status of [%s] denying access for URI "
366                     "path [%s]", _xacmlContext.result.Decision.DENY,
367                     samlAuthzDecisionQuery.resource) 
368           
369            samlAuthzDecisionStatement.decision = _saml.DecisionType.DENY
370
371        return samlResponse
372       
373    def pipQuery(self, request, designator):
374        """Implements interface method:
375       
376        Query a Policy Information Point to retrieve the attribute values
377        corresponding to the specified input designator.  Optionally, update the
378        request context.  This could be a subject, environment or resource. 
379        Matching attributes values are returned
380       
381        @param request: request context
382        @type request: ndg.xacml.core.context.request.Request
383        @param designator: designator requiring additional subject attribute
384        information
385        @type designator: ndg.xacml.core.expression.Expression derived type
386        @return: list of attribute values for subject corresponding to given
387        policy designator.  Return None if none can be found or if no PIP has
388        been assigned to this handler
389        @rtype: ndg.xacml.utils.TypedList(<designator attribute type>) / None
390        type
391        """
392        if self.pip is None:
393            return None
394        else:
395            return self.pip.attributeQuery(request, designator)
396   
397    def _createXacmlRequestCtx(self, samlAuthzDecisionQuery):
398        """Translate SAML authorisation decision query into a XACML request
399        context
400        """
401        xacmlRequest = _xacmlContext.request.Request()
402        xacmlSubject = _xacmlContext.subject.Subject()
403       
404        xacmlAttributeValueFactory = XacmlAttributeValueClassFactory()
405       
406        openidSubjectAttribute = XacmlAttribute()
407        roleAttribute = XacmlAttribute()
408       
409        openidSubjectAttribute.attributeId = \
410                                samlAuthzDecisionQuery.subject.nameID.format
411                                       
412        XacmlAnyUriAttributeValue = xacmlAttributeValueFactory(
413                                            XacmlAttributeValue.ANY_TYPE_URI)
414       
415        openidSubjectAttribute.dataType = XacmlAnyUriAttributeValue.IDENTIFIER
416       
417        openidSubjectAttribute.attributeValues.append(
418                                                    XacmlAnyUriAttributeValue())
419        openidSubjectAttribute.attributeValues[-1].value = \
420                                samlAuthzDecisionQuery.subject.nameID.value
421       
422        xacmlSubject.attributes.append(openidSubjectAttribute)
423
424        XacmlStringAttributeValue = xacmlAttributeValueFactory(
425                                            XacmlAttributeValue.STRING_TYPE_URI)
426                                 
427        xacmlRequest.subjects.append(xacmlSubject)
428       
429        resource = _xacmlContext.resource.Resource()
430        resourceAttribute = XacmlAttribute()
431        resource.attributes.append(resourceAttribute)
432       
433        resourceAttribute.attributeId = Identifiers.Resource.RESOURCE_ID
434                           
435        resourceAttribute.dataType = XacmlAnyUriAttributeValue.IDENTIFIER
436        resourceAttribute.attributeValues.append(XacmlAnyUriAttributeValue())
437        resourceAttribute.attributeValues[-1].value = \
438                                                samlAuthzDecisionQuery.resource
439
440        xacmlRequest.resources.append(resource)
441       
442        xacmlRequest.action = _xacmlContext.action.Action()
443       
444        for action in samlAuthzDecisionQuery.actions:
445            xacmlActionAttribute = XacmlAttribute()
446            xacmlRequest.action.attributes.append(xacmlActionAttribute)
447           
448            xacmlActionAttribute.attributeId = Identifiers.Action.ACTION_ID
449            xacmlActionAttribute.dataType = XacmlStringAttributeValue.IDENTIFIER
450            xacmlActionAttribute.attributeValues.append(
451                                                    XacmlStringAttributeValue())
452            xacmlActionAttribute.attributeValues[-1].value = action.value
453       
454        return xacmlRequest
455   
456    def _createSAMLResponseAssertion(self, authzDecisionQuery, response):
457        """Helper method to add an assertion containing an Authorisation
458        Decision Statement to the SAML response
459       
460        @param authzDecisionQuery: SAML Authorisation Decision Query
461        @type authzDecisionQuery: ndg.saml.saml2.core.AuthzDecisionQuery
462        @param response: SAML response
463        @type response: ndg.saml.saml2.core.Response
464        """
465       
466        # Check for a response set, if none present create one.
467        if response is None:
468            response = _saml.Response()
469           
470            now = datetime.utcnow()
471            response.issueInstant = now
472           
473            # Make up a request ID that this response is responding to
474            response.inResponseTo = authzDecisionQuery.id
475            response.id = str(uuid4())
476            response.version = SAMLVersion(SAMLVersion.VERSION_20)
477               
478            response.issuer = _saml.Issuer()
479            response.issuer.format = self.issuerFormat
480            response.issuer.value = self.issuerName
481   
482            response.status = _saml.Status()
483            response.status.statusCode = _saml.StatusCode()
484            response.status.statusMessage = _saml.StatusMessage()       
485           
486            response.status.statusCode.value = _saml.StatusCode.SUCCESS_URI
487            response.status.statusMessage.value = ("Response created "
488                                                   "successfully")
489       
490        assertion = _saml.Assertion()
491        response.assertions.append(assertion)
492           
493        assertion.version = SAMLVersion(SAMLVersion.VERSION_20)
494        assertion.id = str(uuid4())
495       
496        assertion.issuer = _saml.Issuer()
497        assertion.issuer.value = self.issuerName
498        assertion.issuer.format = self.issuerFormat
499       
500        now = datetime.utcnow()
501        assertion.issueInstant = now
502       
503        # Add a conditions statement for a validity of 8 hours
504        assertion.conditions = _saml.Conditions()
505        assertion.conditions.notBefore = now
506        assertion.conditions.notOnOrAfter = now + timedelta(
507                                                seconds=self.assertionLifetime)
508               
509        assertion.subject = _saml.Subject()
510        assertion.subject.nameID = _saml.NameID()
511        assertion.subject.nameID.format = \
512            authzDecisionQuery.subject.nameID.format
513        assertion.subject.nameID.value = \
514            authzDecisionQuery.subject.nameID.value
515       
516        authzDecisionStatement = _saml.AuthzDecisionStatement()
517        assertion.authzDecisionStatements.append(authzDecisionStatement)
518                   
519        authzDecisionStatement.resource = authzDecisionQuery.resource
520       
521        for action in authzDecisionQuery.actions:
522            authzDecisionStatement.actions.append(_saml.Action())
523            authzDecisionStatement.actions[-1].namespace = action.namespace
524            authzDecisionStatement.actions[-1].value = action.value
525
526        return response
527
Note: See TracBrowser for help on using the repository browser.