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

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

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