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

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

Incomplete - task 2: XACML-Security Integration

  • first working e2e test with PEP calling a SAML Authorisation service configured with PIP to make callouts to an Attribute Authority to pull user attributes. This meets the ESG requirements. Next steps:
    • integrate with ndg.security.test.integration.authz_lite browser based integration tests
    • optimise by adding caching of authz decisions to PEP and possibly caching attribute assertions in the PEP.
  • 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        elif (xacmlResponse.results[0].decision == 
315              _xacmlContext.result.Decision.INDETERMINATE):
316            log.info("PDP returned a status of [%s] denying access for URI "
317                     "path [%s]", _xacmlContext.result.Decision.INDETERMINATE,
318                     samlAuthzDecisionQuery.resource) 
319           
320            samlAuthzDecisionStatement.decision = \
321                                                _saml.DecisionType.INDETERMINATE
322        else:
323            log.info("PDP returned a status of [%s] denying access for URI "
324                     "path [%s]", _xacmlContext.result.Decision.DENY,
325                     samlAuthzDecisionQuery.resource) 
326           
327            samlAuthzDecisionStatement.decision = _saml.DecisionType.DENY
328
329        return samlResponse
330       
331    def pipQuery(self, request, designator):
332        """Implements interface method:
333       
334        Query a Policy Information Point to retrieve the attribute values
335        corresponding to the specified input designator.  Optionally, update the
336        request context.  This could be a subject, environment or resource. 
337        Matching attributes values are returned
338       
339        @param request: request context
340        @type request: ndg.xacml.core.context.request.Request
341        @param designator: designator requiring additional subject attribute
342        information
343        @type designator: ndg.xacml.core.expression.Expression derived type
344        @return: list of attribute values for subject corresponding to given
345        policy designator.  Return None if none can be found
346        @rtype: ndg.xacml.utils.TypedList(<designator attribute type>) / None
347        """
348        return self.pip.attributeQuery(request, designator)
349   
350    def _createXacmlRequestCtx(self, samlAuthzDecisionQuery):
351        """Translate SAML authorisation decision query into a XACML request
352        context
353        """
354        xacmlRequest = _xacmlContext.request.Request()
355        xacmlSubject = _xacmlContext.subject.Subject()
356       
357        xacmlAttributeValueFactory = XacmlAttributeValueClassFactory()
358       
359        openidSubjectAttribute = XacmlAttribute()
360        roleAttribute = XacmlAttribute()
361       
362        openidSubjectAttribute.attributeId = \
363                                samlAuthzDecisionQuery.subject.nameID.format
364                                       
365        XacmlAnyUriAttributeValue = xacmlAttributeValueFactory(
366                                    'http://www.w3.org/2001/XMLSchema#anyURI')
367       
368        openidSubjectAttribute.dataType = XacmlAnyUriAttributeValue.IDENTIFIER
369       
370        openidSubjectAttribute.attributeValues.append(
371                                                    XacmlAnyUriAttributeValue())
372        openidSubjectAttribute.attributeValues[-1].value = \
373                                samlAuthzDecisionQuery.subject.nameID.value
374       
375        xacmlSubject.attributes.append(openidSubjectAttribute)
376
377        XacmlStringAttributeValue = xacmlAttributeValueFactory(
378                                    'http://www.w3.org/2001/XMLSchema#string')
379
380        # TODO: get attributes - replace hard coded values
381        roleAttribute.attributeId = "urn:ndg:security:authz:1.0:attr"
382        roleAttribute.dataType = XacmlStringAttributeValue.IDENTIFIER
383       
384        roleAttribute.attributeValues.append(XacmlStringAttributeValue())
385        roleAttribute.attributeValues[-1].value = 'staff' 
386       
387        xacmlSubject.attributes.append(roleAttribute)
388                                 
389        xacmlRequest.subjects.append(xacmlSubject)
390       
391        resource = _xacmlContext.resource.Resource()
392        resourceAttribute = XacmlAttribute()
393        resource.attributes.append(resourceAttribute)
394       
395        resourceAttribute.attributeId = \
396                            "urn:oasis:names:tc:xacml:1.0:resource:resource-id"
397                           
398        resourceAttribute.dataType = XacmlAnyUriAttributeValue.IDENTIFIER
399        resourceAttribute.attributeValues.append(XacmlAnyUriAttributeValue())
400        resourceAttribute.attributeValues[-1].value = \
401                                                samlAuthzDecisionQuery.resource
402
403        xacmlRequest.resources.append(resource)
404       
405        xacmlRequest.action = _xacmlContext.action.Action()
406       
407        for action in samlAuthzDecisionQuery.actions:
408            xacmlActionAttribute = XacmlAttribute()
409            xacmlRequest.action.attributes.append(xacmlActionAttribute)
410           
411            xacmlActionAttribute.attributeId = \
412                                "urn:oasis:names:tc:xacml:1.0:action:action-id"
413            xacmlActionAttribute.dataType = XacmlStringAttributeValue.IDENTIFIER
414            xacmlActionAttribute.attributeValues.append(
415                                                    XacmlStringAttributeValue())
416            xacmlActionAttribute.attributeValues[-1].value = action.value
417       
418        return xacmlRequest
419   
420    def _createSAMLResponseAssertion(self, authzDecisionQuery, response):
421        """Helper method to add an assertion containing an Authorisation
422        Decision Statement to the SAML response
423       
424        @param authzDecisionQuery: SAML Authorisation Decision Query
425        @type authzDecisionQuery: ndg.saml.saml2.core.AuthzDecisionQuery
426        @param response: SAML response
427        @type response: ndg.saml.saml2.core.Response
428        """
429       
430        # Check for a response set, if none present create one.
431        if response is None:
432            response = _saml.Response()
433           
434            now = datetime.utcnow()
435            response.issueInstant = now
436           
437            # Make up a request ID that this response is responding to
438            response.inResponseTo = authzDecisionQuery.id
439            response.id = str(uuid4())
440            response.version = SAMLVersion(SAMLVersion.VERSION_20)
441               
442            response.issuer = _saml.Issuer()
443            response.issuer.format = self.issuerFormat
444            response.issuer.value = self.issuerName
445   
446            response.status = _saml.Status()
447            response.status.statusCode = _saml.StatusCode()
448            response.status.statusMessage = _saml.StatusMessage()       
449           
450            response.status.statusCode.value = _saml.StatusCode.SUCCESS_URI
451            response.status.statusMessage.value = ("Response created "
452                                                   "successfully")
453       
454        assertion = _saml.Assertion()
455        response.assertions.append(assertion)
456           
457        assertion.version = SAMLVersion(SAMLVersion.VERSION_20)
458        assertion.id = str(uuid4())
459       
460        now = datetime.utcnow()
461        assertion.issueInstant = now
462       
463        # Add a conditions statement for a validity of 8 hours
464        assertion.conditions = _saml.Conditions()
465        assertion.conditions.notBefore = now
466        assertion.conditions.notOnOrAfter = now + timedelta(
467                                                seconds=self.assertionLifetime)
468               
469        assertion.subject = _saml.Subject()
470        assertion.subject.nameID = _saml.NameID()
471        assertion.subject.nameID.format = \
472            authzDecisionQuery.subject.nameID.format
473        assertion.subject.nameID.value = \
474            authzDecisionQuery.subject.nameID.value
475       
476        authzDecisionStatement = _saml.AuthzDecisionStatement()
477        assertion.authzDecisionStatements.append(authzDecisionStatement)
478                   
479        authzDecisionStatement.resource = authzDecisionQuery.resource
480       
481        for action in authzDecisionQuery.actions:
482            authzDecisionStatement.actions.append(_saml.Action())
483            authzDecisionStatement.actions[-1].namespace = action.namespace
484            authzDecisionStatement.actions[-1].value = action.value
485
486        return response
Note: See TracBrowser for help on using the repository browser.