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

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@7350
Revision 7350, 20.0 KB checked in by pjkersha, 9 years ago (diff)

Incomplete - task 2: XACML-Security Integration

  • Working version integrated with the ndg.security.test.integration.full_system test. This secures a test HTTP app with the XACML based authorisation called over a SAML interface from a PEP in the app's authorisation middleware
  • Some tuning is needed to optimise performance:
    • caching of attribute queries in the PEP
    • Possible additional PDP in the authorisation filter to filter out some requests from being routed to the SAML authorisation service.
    • possible caching of authorisation decisions at the PEP as another way of avoiding the authorisation service round-trips.
  • 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
22from ndg.xacml.core.context.pdp import PDP
23from ndg.xacml.core import context as _xacmlContext
24from ndg.xacml.core.attribute import Attribute as XacmlAttribute
25from ndg.xacml.core.attributevalue import AttributeValueClassFactory as \
26    XacmlAttributeValueClassFactory
27from ndg.xacml.parsers.etree.factory import ReaderFactory as \
28    XacmlPolicyReaderFactory
29
30from ndg.security.server.xacml.pip.saml_pip import PIP
31
32
33class SamlPEPRequest(object):
34    """Helper class for SamlCtxHandler.handlePEPRequest"""
35    __slots__ = ('__authzDecisionQuery', '__response', '__policyFilePath')
36   
37    def __init__(self):
38        self.__authzDecisionQuery = None
39        self.__response = None
40   
41    def _getAuthzDecisionQuery(self):
42        return self.__authzDecisionQuery
43
44    def _setAuthzDecisionQuery(self, value):
45        if not isinstance(value, _saml.AuthzDecisionQuery):
46            raise TypeError('Expecting %r type for "response" attribute, got %r'
47                            % (_saml.Response, type(value)))
48        self.__authzDecisionQuery = value
49       
50    authzDecisionQuery = property(_getAuthzDecisionQuery, 
51                                  _setAuthzDecisionQuery, 
52                                  doc="SAML Authorisation Decision Query")
53
54    def _getResponse(self):
55        return self.__response
56
57    def _setResponse(self, value):
58        if not isinstance(value, _saml.Response):
59            raise TypeError('Expecting %r type for "response" attribute, got %r'
60                            % (_saml.Response, type(value)))
61        self.__response = value
62
63    response = property(_getResponse, _setResponse, doc="SAML Response")
64   
65       
66class SamlCtxHandler(_xacmlContext.handler.CtxHandlerBase):
67    """XACML Context handler for accepting SAML 2.0 based authorisation
68    decision queries and interfacing to a PEP with SAML based Attribute Query
69    Interface
70    """
71    DEFAULT_OPT_PREFIX = 'saml_ctx_handler.'
72    PIP_OPT_PREFIX = 'pip.'
73   
74    __slots__ = (
75        '__policyFilePath',
76        '__issuerProxy', 
77        '__assertionLifetime',
78    )
79   
80    def __init__(self):
81        super(SamlCtxHandler, self).__init__()
82       
83        # Proxy object for SAML AuthzDecisionQueryResponse Issuer attributes. 
84        # By generating a proxy the Response objects inherent attribute
85        # validation can be applied to Issuer related config parameters before
86        # they're assigned to the response issuer object generated in the
87        # authorisation decision query response
88        self.__issuerProxy = _saml.Issuer()
89        self.__assertionLifetime = 0.
90        self.__policyFilePath = None
91       
92        # Policy Information Point
93        self.pip = PIP()
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                setattr(self.pip, __optName[pipPrefixLen:], val)
164            else:
165                setattr(self, __optName, val)
166               
167        for optName, val in items:
168            if prefix:
169                # Filter attributes based on prefix
170                if optName.startswith(prefix):
171                    _optName = optName[prefixLen:]
172                    _setAttr(_optName)
173            else:
174                # No prefix set - attempt to set all attributes   
175                _setAttr(optName)
176       
177    def parseKeywords(self, prefix=DEFAULT_OPT_PREFIX, **kw):
178        """Update object from input keywords
179       
180        @type prefix: basestring
181        @param prefix: if a prefix is given, only update self from kw items
182        where keyword starts with this prefix
183        @type kw: dict
184        @param kw: items corresponding to class instance variables to
185        update.  Keyword names must match their equivalent class instance
186        variable names.  However, they may prefixed with <prefix>
187        """
188        self.__parseFromItems(kw.items(), prefix=prefix)
189               
190    @classmethod
191    def fromKeywords(cls, prefix=DEFAULT_OPT_PREFIX, **kw):
192        """Create a new instance initialising instance variables from the
193        keyword inputs
194        @type prefix: basestring
195        @param prefix: if a prefix is given, only update self from kw items
196        where keyword starts with this prefix
197        @type kw: dict
198        @param kw: items corresponding to class instance variables to
199        update.  Keyword names must match their equivalent class instance
200        variable names.  However, they may prefixed with <prefix>
201        @return: new instance of this class
202        @rtype: ndg.saml.saml2.binding.soap.client.SOAPBinding or derived type
203        """
204        obj = cls()
205        obj.parseKeywords(prefix=prefix, **kw)
206       
207        # Post initialisation steps - load policy and PIP mapping file
208        if obj.policyFilePath:
209            obj.pdp = PDP.fromPolicySource(obj.policyFilePath, 
210                                           XacmlPolicyReaderFactory)
211       
212        if obj.pip.mappingFilePath:
213            obj.pip.readMappingFile()
214                       
215        return obj
216                                       
217    def _getPolicyFilePath(self):
218        return self.__policyFilePath
219
220    def _setPolicyFilePath(self, value):
221        if not isinstance(value, basestring):
222            raise TypeError('Expecting string type for "policyFilePath"; got '
223                            '%r' % type(value))
224        self.__policyFilePath = path.expandvars(value)
225
226    policyFilePath = property(_getPolicyFilePath, 
227                              _setPolicyFilePath, 
228                              doc="Policy file path for policy used by the PDP")
229       
230    def _getIssuerFormat(self):
231        if self.__issuerProxy is None:
232            return None
233        else:
234            return self.__issuerProxy.value
235
236    def _setIssuerFormat(self, value):
237        if self.__issuerProxy is None:
238            self.__issuerProxy = _saml.Issuer()
239           
240        self.__issuerProxy.format = value
241
242    issuerFormat = property(_getIssuerFormat, _setIssuerFormat, 
243                            doc="Issuer format of SAML Authorisation Query "
244                                "Response")
245
246    def _getIssuerName(self):
247        if self.__issuerProxy is None:
248            return None
249        else:
250            return self.__issuerProxy.value
251
252    def _setIssuerName(self, value):
253        if self.__issuerProxy is None:
254            self.__issuerProxy = _saml.Issuer()
255           
256        self.__issuerProxy.value = value
257
258    issuerName = property(_getIssuerName, _setIssuerName, 
259                          doc="Name of issuer of SAML Authorisation Query "
260                              "Response")
261   
262    _getAssertionLifetime = lambda self: self.__assertionLifetime
263   
264    def _setAssertionLifetime(self, value):
265        if isinstance(value, (int, float, long, basestring)):
266            self.__assertionLifetime = float(value)
267        else:
268            raise TypeError('Expecting int, long, float or string type for '
269                            '"assertionLifetime" attribute; got %s instead' % 
270                            type(value))
271
272    assertionLifetime = property(fget=_getAssertionLifetime,
273                                 fset=_setAssertionLifetime,
274                                 doc="lifetime of assertion in seconds used to "
275                                     "set assertion conditions notOnOrAfter "
276                                     "time")
277 
278    def handlePEPRequest(self, pepRequest):
279        """Handle request from Policy Enforcement Point
280       
281        @param pepRequest: request containing a SAML authorisation decision
282        query and optionally an initialised SAML response object
283        @type pepRequest: ndg.security.server.xacml.saml_ctx_handler.SamlPEPRequest
284        @return: SAML authorisation decision response
285        @rtype: ndg.saml.saml2.core.Response
286        """
287        samlAuthzDecisionQuery = pepRequest.authzDecisionQuery
288       
289        xacmlRequest = self._createXacmlRequestCtx(samlAuthzDecisionQuery)
290       
291        # Add a reference to this context so that the PDP can invoke queries
292        # back to the PIP
293        xacmlRequest.ctxHandler = self
294       
295        # Call the PDP
296        xacmlResponse = self.pdp.evaluate(xacmlRequest)
297       
298        # Create the SAML Response
299        samlResponse = self._createSAMLResponseAssertion(samlAuthzDecisionQuery,
300                                                         pepRequest.response)
301       
302        # Assume only a single assertion authorisation decision statements
303        samlAuthzDecisionStatement = samlResponse.assertions[0
304                                                ].authzDecisionStatements[0]
305       
306        # Convert the decision status
307        if (xacmlResponse.results[0].decision == 
308            _xacmlContext.result.Decision.PERMIT):
309            log.info("PDP granted access for URI path [%s]", 
310                     samlAuthzDecisionQuery.resource)
311           
312            samlAuthzDecisionStatement.decision = _saml.DecisionType.PERMIT
313       
314        # Nb. Mapping XACML NotApplicable => SAML INDETERMINATE
315        elif (xacmlResponse.results[0].decision in 
316              (_xacmlContext.result.Decision.INDETERMINATE,
317               _xacmlContext.result.Decision.NOT_APPLICABLE)):
318            log.info("PDP returned a status of [%s] for URI path [%s]; "
319                     "mapping to SAML response [%s] ...", 
320                     xacmlResponse.results[0].decision,
321                     samlAuthzDecisionQuery.resource,
322                     _saml.DecisionType.INDETERMINATE) 
323           
324            samlAuthzDecisionStatement.decision = \
325                                                _saml.DecisionType.INDETERMINATE
326        else:
327            log.info("PDP returned a status of [%s] denying access for URI "
328                     "path [%s]", _xacmlContext.result.Decision.DENY,
329                     samlAuthzDecisionQuery.resource) 
330           
331            samlAuthzDecisionStatement.decision = _saml.DecisionType.DENY
332
333        return samlResponse
334       
335    def pipQuery(self, request, designator):
336        """Implements interface method:
337       
338        Query a Policy Information Point to retrieve the attribute values
339        corresponding to the specified input designator.  Optionally, update the
340        request context.  This could be a subject, environment or resource. 
341        Matching attributes values are returned
342       
343        @param request: request context
344        @type request: ndg.xacml.core.context.request.Request
345        @param designator: designator requiring additional subject attribute
346        information
347        @type designator: ndg.xacml.core.expression.Expression derived type
348        @return: list of attribute values for subject corresponding to given
349        policy designator.  Return None if none can be found
350        @rtype: ndg.xacml.utils.TypedList(<designator attribute type>) / None
351        """
352        return self.pip.attributeQuery(request, designator)
353   
354    def _createXacmlRequestCtx(self, samlAuthzDecisionQuery):
355        """Translate SAML authorisation decision query into a XACML request
356        context
357        """
358        xacmlRequest = _xacmlContext.request.Request()
359        xacmlSubject = _xacmlContext.subject.Subject()
360       
361        xacmlAttributeValueFactory = XacmlAttributeValueClassFactory()
362       
363        openidSubjectAttribute = XacmlAttribute()
364        roleAttribute = XacmlAttribute()
365       
366        openidSubjectAttribute.attributeId = \
367                                samlAuthzDecisionQuery.subject.nameID.format
368                                       
369        XacmlAnyUriAttributeValue = xacmlAttributeValueFactory(
370                                    'http://www.w3.org/2001/XMLSchema#anyURI')
371       
372        openidSubjectAttribute.dataType = XacmlAnyUriAttributeValue.IDENTIFIER
373       
374        openidSubjectAttribute.attributeValues.append(
375                                                    XacmlAnyUriAttributeValue())
376        openidSubjectAttribute.attributeValues[-1].value = \
377                                samlAuthzDecisionQuery.subject.nameID.value
378       
379        xacmlSubject.attributes.append(openidSubjectAttribute)
380
381        XacmlStringAttributeValue = xacmlAttributeValueFactory(
382                                    'http://www.w3.org/2001/XMLSchema#string')
383
384        # TODO: get attributes - replace hard coded values
385        roleAttribute.attributeId = "urn:ndg:security:authz:1.0:attr"
386        roleAttribute.dataType = XacmlStringAttributeValue.IDENTIFIER
387       
388        roleAttribute.attributeValues.append(XacmlStringAttributeValue())
389        roleAttribute.attributeValues[-1].value = 'staff' 
390       
391        xacmlSubject.attributes.append(roleAttribute)
392                                 
393        xacmlRequest.subjects.append(xacmlSubject)
394       
395        resource = _xacmlContext.resource.Resource()
396        resourceAttribute = XacmlAttribute()
397        resource.attributes.append(resourceAttribute)
398       
399        resourceAttribute.attributeId = \
400                            "urn:oasis:names:tc:xacml:1.0: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 = \
416                                "urn:oasis:names:tc:xacml:1.0:action:action-id"
417            xacmlActionAttribute.dataType = XacmlStringAttributeValue.IDENTIFIER
418            xacmlActionAttribute.attributeValues.append(
419                                                    XacmlStringAttributeValue())
420            xacmlActionAttribute.attributeValues[-1].value = action.value
421       
422        return xacmlRequest
423   
424    def _createSAMLResponseAssertion(self, authzDecisionQuery, response):
425        """Helper method to add an assertion containing an Authorisation
426        Decision Statement to the SAML response
427       
428        @param authzDecisionQuery: SAML Authorisation Decision Query
429        @type authzDecisionQuery: ndg.saml.saml2.core.AuthzDecisionQuery
430        @param response: SAML response
431        @type response: ndg.saml.saml2.core.Response
432        """
433       
434        # Check for a response set, if none present create one.
435        if response is None:
436            response = _saml.Response()
437           
438            now = datetime.utcnow()
439            response.issueInstant = now
440           
441            # Make up a request ID that this response is responding to
442            response.inResponseTo = authzDecisionQuery.id
443            response.id = str(uuid4())
444            response.version = SAMLVersion(SAMLVersion.VERSION_20)
445               
446            response.issuer = _saml.Issuer()
447            response.issuer.format = self.issuerFormat
448            response.issuer.value = self.issuerName
449   
450            response.status = _saml.Status()
451            response.status.statusCode = _saml.StatusCode()
452            response.status.statusMessage = _saml.StatusMessage()       
453           
454            response.status.statusCode.value = _saml.StatusCode.SUCCESS_URI
455            response.status.statusMessage.value = ("Response created "
456                                                   "successfully")
457       
458        assertion = _saml.Assertion()
459        response.assertions.append(assertion)
460           
461        assertion.version = SAMLVersion(SAMLVersion.VERSION_20)
462        assertion.id = str(uuid4())
463       
464        now = datetime.utcnow()
465        assertion.issueInstant = now
466       
467        # Add a conditions statement for a validity of 8 hours
468        assertion.conditions = _saml.Conditions()
469        assertion.conditions.notBefore = now
470        assertion.conditions.notOnOrAfter = now + timedelta(
471                                                seconds=self.assertionLifetime)
472               
473        assertion.subject = _saml.Subject()
474        assertion.subject.nameID = _saml.NameID()
475        assertion.subject.nameID.format = \
476            authzDecisionQuery.subject.nameID.format
477        assertion.subject.nameID.value = \
478            authzDecisionQuery.subject.nameID.value
479       
480        authzDecisionStatement = _saml.AuthzDecisionStatement()
481        assertion.authzDecisionStatements.append(authzDecisionStatement)
482                   
483        authzDecisionStatement.resource = authzDecisionQuery.resource
484       
485        for action in authzDecisionQuery.actions:
486            authzDecisionStatement.actions.append(_saml.Action())
487            authzDecisionStatement.actions[-1].namespace = action.namespace
488            authzDecisionStatement.actions[-1].value = action.value
489
490        return response
Note: See TracBrowser for help on using the repository browser.