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

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@7327
Revision 7327, 19.2 KB checked in by pjkersha, 10 years ago (diff)

Incomplete - task 2: XACML-Security Integration

  • added unit tests for XACML Context handler
  • 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        if obj.policyFilePath:
107            obj.pdp = PDP.fromPolicySource(obj.policyFilePath, 
108                                           XacmlPolicyReaderFactory)
109           
110        return obj
111
112    def parseConfig(self, cfg, prefix=DEFAULT_OPT_PREFIX, section='DEFAULT'):
113        '''Read config settings from a file, config parser object or dict
114       
115        @type cfg: basestring / ConfigParser derived type / dict
116        @param cfg: configuration file path or ConfigParser type object
117        @type prefix: basestring
118        @param prefix: prefix for option names e.g. "attributeQuery."
119        @type section: basetring
120        @param section: configuration file section from which to extract
121        parameters.
122        ''' 
123        if isinstance(cfg, basestring):
124            cfgFilePath = path.expandvars(cfg)
125           
126            # Add a 'here' helper option for setting dir paths in the config
127            # file
128            hereDir = path.abspath(path.dirname(cfgFilePath))
129            _cfg = SafeConfigParser(defaults={'here': hereDir})
130           
131            # Make option name reading case sensitive
132            _cfg.optionxform = str
133            _cfg.read(cfgFilePath)
134            items = _cfg.items(section)
135           
136        elif isinstance(cfg, ConfigParser):
137            items = cfg.items(section)
138         
139        elif isinstance(cfg, dict):
140            items = cfg.items()     
141        else:
142            raise AttributeError('Expecting basestring, ConfigParser or dict '
143                                 'type for "cfg" attribute; got %r type' % 
144                                 type(cfg))
145       
146        self.__parseFromItems(items, prefix=prefix)
147       
148    def __parseFromItems(self, items, prefix=DEFAULT_OPT_PREFIX): 
149        """Update from list of tuple name, value pairs - for internal use
150        by parseKeywords and parseConfig
151        """
152        prefixLen = len(prefix) 
153        pipPrefix = self.__class__.PIP_OPT_PREFIX
154        pipPrefixLen = len(pipPrefix)
155       
156        def _setAttr(__optName):
157            # Check for PIP attribute related items
158            if __optName.startswith(pipPrefix):
159                setattr(self.pip, __optName[pipPrefixLen:], val)
160            else:
161                setattr(self, __optName, val)
162               
163        for optName, val in items:
164            if prefix:
165                # Filter attributes based on prefix
166                if optName.startswith(prefix):
167                    _optName = optName[prefixLen:]
168                    _setAttr(_optName)
169            else:
170                # No prefix set - attempt to set all attributes   
171                _setAttr(optName)
172       
173    def parseKeywords(self, prefix=DEFAULT_OPT_PREFIX, **kw):
174        """Update object from input keywords
175       
176        @type prefix: basestring
177        @param prefix: if a prefix is given, only update self from kw items
178        where keyword starts with this prefix
179        @type kw: dict
180        @param kw: items corresponding to class instance variables to
181        update.  Keyword names must match their equivalent class instance
182        variable names.  However, they may prefixed with <prefix>
183        """
184        self.__parseFromItems(kw.items(), prefix=prefix)
185               
186    @classmethod
187    def fromKeywords(cls, prefix=DEFAULT_OPT_PREFIX, **kw):
188        """Create a new instance initialising instance variables from the
189        keyword inputs
190        @type prefix: basestring
191        @param prefix: if a prefix is given, only update self from kw items
192        where keyword starts with this prefix
193        @type kw: dict
194        @param kw: items corresponding to class instance variables to
195        update.  Keyword names must match their equivalent class instance
196        variable names.  However, they may prefixed with <prefix>
197        @return: new instance of this class
198        @rtype: ndg.saml.saml2.binding.soap.client.SOAPBinding or derived type
199        """
200        obj = cls()
201        obj.parseKeywords(prefix=prefix, **kw)
202       
203        if obj.policyFilePath:
204            obj.pdp = PDP.fromPolicySource(obj.policyFilePath, 
205                                           XacmlPolicyReaderFactory)
206                   
207        return obj
208                                       
209    def _getPolicyFilePath(self):
210        return self.__policyFilePath
211
212    def _setPolicyFilePath(self, value):
213        if not isinstance(value, basestring):
214            raise TypeError('Expecting string type for "policyFilePath"; got '
215                            '%r' % type(value))
216        self.__policyFilePath = path.expandvars(value)
217
218    policyFilePath = property(_getPolicyFilePath, 
219                              _setPolicyFilePath, 
220                              doc="Policy file path for policy used by the PDP")
221       
222    def _getIssuerFormat(self):
223        if self.__issuerProxy is None:
224            return None
225        else:
226            return self.__issuerProxy.value
227
228    def _setIssuerFormat(self, value):
229        if self.__issuerProxy is None:
230            self.__issuerProxy = _saml.Issuer()
231           
232        self.__issuerProxy.format = value
233
234    issuerFormat = property(_getIssuerFormat, _setIssuerFormat, 
235                            doc="Issuer format")
236
237    def _getIssuerName(self):
238        if self.__issuerProxy is None:
239            return None
240        else:
241            return self.__issuerProxy.value
242
243    def _setIssuerName(self, value):
244        if self.__issuerProxy is None:
245            self.__issuerProxy = _saml.Issuer()
246           
247        self.__issuerProxy.value = value
248
249    issuerName = property(_getIssuerName, _setIssuerName, 
250                          doc="Name of issuer of SAML Authorisation Query "
251                              "Response")
252   
253    _getAssertionLifetime = lambda self: self.__assertionLifetime
254   
255    def _setAssertionLifetime(self, value):
256        if isinstance(value, (int, float, long, basestring)):
257            self.__assertionLifetime = float(value)
258        else:
259            raise TypeError('Expecting int, long, float or string type for '
260                            '"assertionLifetime" attribute; got %s instead' % 
261                            type(value))
262
263    assertionLifetime = property(fget=_getAssertionLifetime,
264                                 fset=_setAssertionLifetime,
265                                 doc="lifetime of assertion in seconds used to "
266                                     "set assertion conditions notOnOrAfter "
267                                     "time")
268 
269    def handlePEPRequest(self, pepRequest):
270        """Handle request from Policy Enforcement Point
271       
272        @param pepRequest: request containing a SAML authorisation decision
273        query and optionally an initialised SAML response object
274        @type pepRequest: ndg.security.server.xacml.saml_ctx_handler.SamlPEPRequest
275        @return: SAML authorisation decision response
276        @rtype: ndg.saml.saml2.core.Response
277        """
278        samlAuthzDecisionQuery = pepRequest.authzDecisionQuery
279       
280        xacmlRequest = self._createXacmlRequestCtx(samlAuthzDecisionQuery)
281       
282        # Call the PDP
283        xacmlResponse = self.pdp.evaluate(xacmlRequest)
284       
285        # Create the SAML Response
286        samlResponse = self._createSAMLResponseAssertion(samlAuthzDecisionQuery,
287                                                         pepRequest.response)
288       
289        samlAuthzDecisionStatement = samlResponse.assertions[0
290                                                ].authzDecisionStatements[0]
291       
292        # Convert the decision status
293        if (xacmlResponse.results[0].decision == 
294            _xacmlContext.result.Decision.PERMIT):
295            log.info("PDP granted access for URI path [%s]", 
296                     samlAuthzDecisionQuery.resource)
297           
298            samlAuthzDecisionStatement.decision = _saml.DecisionType.PERMIT
299       
300        elif (xacmlResponse.results[0].decision == 
301              _xacmlContext.result.Decision.INDETERMINATE):
302            log.info("PDP returned a status of [%s] denying access for URI "
303                     "path [%s]", _xacmlContext.result.Decision.INDETERMINATE,
304                     samlAuthzDecisionQuery.resource) 
305           
306            samlAuthzDecisionStatement.decision = \
307                                                _saml.DecisionType.INDETERMINATE
308        else:
309            log.info("PDP returned a status of [%s] denying access for URI "
310                     "path [%s]", _xacmlContext.result.Decision.DENY,
311                     samlAuthzDecisionQuery.resource) 
312           
313            samlAuthzDecisionStatement.decision = _saml.DecisionType.DENY
314
315        return samlResponse
316       
317    def pipQuery(self, request, designator):
318        """Implements interface method:
319       
320        Query a Policy Information Point to retrieve the attribute values
321        corresponding to the specified input designator.  Optionally, update the
322        request context.  This could be a subject, environment or resource. 
323        Matching attributes values are returned
324       
325        @param request: request context
326        @type request: ndg.xacml.core.context.request.Request
327        @param designator: designator requiring additional subject attribute
328        information
329        @type designator: ndg.xacml.core.expression.Expression derived type
330        @return: list of attribute values for subject corresponding to given
331        policy designator.  Return None if none can be found
332        @rtype: ndg.xacml.utils.TypedList(<designator attribute type>) / None
333        """
334        return self.pip.attributeQuery(request, designator)
335   
336    def _createXacmlRequestCtx(self, samlAuthzDecisionQuery):
337        """Translate SAML authorisation decision query into a XACML request
338        context
339        """
340        xacmlRequest = _xacmlContext.request.Request()
341        xacmlSubject = _xacmlContext.subject.Subject()
342       
343        xacmlAttributeValueFactory = XacmlAttributeValueClassFactory()
344       
345        openidSubjectAttribute = XacmlAttribute()
346        roleAttribute = XacmlAttribute()
347       
348        openidSubjectAttribute.attributeId = \
349                                samlAuthzDecisionQuery.subject.nameID.format
350                                       
351        XacmlAnyUriAttributeValue = xacmlAttributeValueFactory(
352                                    'http://www.w3.org/2001/XMLSchema#anyURI')
353       
354        openidSubjectAttribute.dataType = XacmlAnyUriAttributeValue.IDENTIFIER
355       
356        openidSubjectAttribute.attributeValues.append(
357                                                    XacmlAnyUriAttributeValue())
358        openidSubjectAttribute.attributeValues[-1].value = \
359                                samlAuthzDecisionQuery.subject.nameID.value
360       
361        xacmlSubject.attributes.append(openidSubjectAttribute)
362
363        XacmlStringAttributeValue = xacmlAttributeValueFactory(
364                                    'http://www.w3.org/2001/XMLSchema#string')
365
366        # TODO: get attributes - replace hard coded values
367        roleAttribute.attributeId = "urn:ndg:security:authz:1.0:attr"
368        roleAttribute.dataType = XacmlStringAttributeValue.IDENTIFIER
369       
370        roleAttribute.attributeValues.append(XacmlStringAttributeValue())
371        roleAttribute.attributeValues[-1].value = 'staff' 
372       
373        xacmlSubject.attributes.append(roleAttribute)
374                                 
375        xacmlRequest.subjects.append(xacmlSubject)
376       
377        resource = _xacmlContext.resource.Resource()
378        resourceAttribute = XacmlAttribute()
379        resource.attributes.append(resourceAttribute)
380       
381        resourceAttribute.attributeId = \
382                            "urn:oasis:names:tc:xacml:1.0:resource:resource-id"
383                           
384        resourceAttribute.dataType = XacmlAnyUriAttributeValue.IDENTIFIER
385        resourceAttribute.attributeValues.append(XacmlAnyUriAttributeValue())
386        resourceAttribute.attributeValues[-1].value = \
387                                                samlAuthzDecisionQuery.resource
388
389        xacmlRequest.resources.append(resource)
390       
391        xacmlRequest.action = _xacmlContext.action.Action()
392       
393        for action in samlAuthzDecisionQuery.actions:
394            xacmlActionAttribute = XacmlAttribute()
395            xacmlRequest.action.attributes.append(xacmlActionAttribute)
396           
397            xacmlActionAttribute.attributeId = \
398                                "urn:oasis:names:tc:xacml:1.0:action:action-id"
399            xacmlActionAttribute.dataType = XacmlStringAttributeValue.IDENTIFIER
400            xacmlActionAttribute.attributeValues.append(
401                                                    XacmlStringAttributeValue())
402            xacmlActionAttribute.attributeValues[-1].value = action.value
403       
404        return xacmlRequest
405   
406    def _createSAMLResponseAssertion(self, authzDecisionQuery, response):
407        """Helper method to add an assertion containing an Authorisation
408        Decision Statement to the SAML response
409       
410        @param authzDecisionQuery: SAML Authorisation Decision Query
411        @type authzDecisionQuery: ndg.saml.saml2.core.AuthzDecisionQuery
412        @param response: SAML response
413        @type response: ndg.saml.saml2.core.Response
414        """
415       
416        # Check for a response set, if none present create one.
417        if response is None:
418            response = _saml.Response()
419           
420            now = datetime.utcnow()
421            response.issueInstant = now
422           
423            # Make up a request ID that this response is responding to
424            response.inResponseTo = authzDecisionQuery.id
425            response.id = str(uuid4())
426            response.version = SAMLVersion(SAMLVersion.VERSION_20)
427               
428            response.issuer = _saml.Issuer()
429            response.issuer.format = self.issuerFormat
430            response.issuer.value = self.issuerName
431   
432            response.status = _saml.Status()
433            response.status.statusCode = _saml.StatusCode()
434            response.status.statusMessage = _saml.StatusMessage()       
435           
436            response.status.statusCode.value = _saml.StatusCode.SUCCESS_URI
437            response.status.statusMessage.value = ("Response created "
438                                                   "successfully")
439       
440        assertion = _saml.Assertion()
441        response.assertions.append(assertion)
442           
443        assertion.version = SAMLVersion(SAMLVersion.VERSION_20)
444        assertion.id = str(uuid4())
445       
446        now = datetime.utcnow()
447        assertion.issueInstant = now
448       
449        # Add a conditions statement for a validity of 8 hours
450        assertion.conditions = _saml.Conditions()
451        assertion.conditions.notBefore = now
452        assertion.conditions.notOnOrAfter = now + timedelta(
453                                                seconds=self.assertionLifetime)
454               
455        assertion.subject = _saml.Subject()
456        assertion.subject.nameID = _saml.NameID()
457        assertion.subject.nameID.format = \
458            authzDecisionQuery.subject.nameID.format
459        assertion.subject.nameID.value = \
460            authzDecisionQuery.subject.nameID.value
461       
462        authzDecisionStatement = _saml.AuthzDecisionStatement()
463        assertion.authzDecisionStatements.append(authzDecisionStatement)
464                   
465        authzDecisionStatement.resource = authzDecisionQuery.resource
466       
467        for action in authzDecisionQuery.actions:
468            authzDecisionStatement.actions.append(_saml.Action())
469            authzDecisionStatement.actions[-1].namespace = action.namespace
470            authzDecisionStatement.actions[-1].value = action.value
471
472        return response
Note: See TracBrowser for help on using the repository browser.