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

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

Incomplete - task 2: XACML-Security Integration

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