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

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

Incomplete - task 2: XACML-Security Integration

  • integrating XACML context handler with authorisation service.
  • 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 of SAML Authorisation Query "
236                                "Response")
237
238    def _getIssuerName(self):
239        if self.__issuerProxy is None:
240            return None
241        else:
242            return self.__issuerProxy.value
243
244    def _setIssuerName(self, value):
245        if self.__issuerProxy is None:
246            self.__issuerProxy = _saml.Issuer()
247           
248        self.__issuerProxy.value = value
249
250    issuerName = property(_getIssuerName, _setIssuerName, 
251                          doc="Name of issuer of SAML Authorisation Query "
252                              "Response")
253   
254    _getAssertionLifetime = lambda self: self.__assertionLifetime
255   
256    def _setAssertionLifetime(self, value):
257        if isinstance(value, (int, float, long, basestring)):
258            self.__assertionLifetime = float(value)
259        else:
260            raise TypeError('Expecting int, long, float or string type for '
261                            '"assertionLifetime" attribute; got %s instead' % 
262                            type(value))
263
264    assertionLifetime = property(fget=_getAssertionLifetime,
265                                 fset=_setAssertionLifetime,
266                                 doc="lifetime of assertion in seconds used to "
267                                     "set assertion conditions notOnOrAfter "
268                                     "time")
269 
270    def handlePEPRequest(self, pepRequest):
271        """Handle request from Policy Enforcement Point
272       
273        @param pepRequest: request containing a SAML authorisation decision
274        query and optionally an initialised SAML response object
275        @type pepRequest: ndg.security.server.xacml.saml_ctx_handler.SamlPEPRequest
276        @return: SAML authorisation decision response
277        @rtype: ndg.saml.saml2.core.Response
278        """
279        samlAuthzDecisionQuery = pepRequest.authzDecisionQuery
280       
281        xacmlRequest = self._createXacmlRequestCtx(samlAuthzDecisionQuery)
282       
283        # Add a reference to this context so that the PDP can invoke queries
284        # back to the PIP
285        xacmlRequest.ctxHandler = self
286       
287        # Call the PDP
288        xacmlResponse = self.pdp.evaluate(xacmlRequest)
289       
290        # Create the SAML Response
291        samlResponse = self._createSAMLResponseAssertion(samlAuthzDecisionQuery,
292                                                         pepRequest.response)
293       
294        # Assume only a single assertion authorisation decision statements
295        samlAuthzDecisionStatement = samlResponse.assertions[0
296                                                ].authzDecisionStatements[0]
297       
298        # Convert the decision status
299        if (xacmlResponse.results[0].decision == 
300            _xacmlContext.result.Decision.PERMIT):
301            log.info("PDP granted access for URI path [%s]", 
302                     samlAuthzDecisionQuery.resource)
303           
304            samlAuthzDecisionStatement.decision = _saml.DecisionType.PERMIT
305       
306        elif (xacmlResponse.results[0].decision == 
307              _xacmlContext.result.Decision.INDETERMINATE):
308            log.info("PDP returned a status of [%s] denying access for URI "
309                     "path [%s]", _xacmlContext.result.Decision.INDETERMINATE,
310                     samlAuthzDecisionQuery.resource) 
311           
312            samlAuthzDecisionStatement.decision = \
313                                                _saml.DecisionType.INDETERMINATE
314        else:
315            log.info("PDP returned a status of [%s] denying access for URI "
316                     "path [%s]", _xacmlContext.result.Decision.DENY,
317                     samlAuthzDecisionQuery.resource) 
318           
319            samlAuthzDecisionStatement.decision = _saml.DecisionType.DENY
320
321        return samlResponse
322       
323    def pipQuery(self, request, designator):
324        """Implements interface method:
325       
326        Query a Policy Information Point to retrieve the attribute values
327        corresponding to the specified input designator.  Optionally, update the
328        request context.  This could be a subject, environment or resource. 
329        Matching attributes values are returned
330       
331        @param request: request context
332        @type request: ndg.xacml.core.context.request.Request
333        @param designator: designator requiring additional subject attribute
334        information
335        @type designator: ndg.xacml.core.expression.Expression derived type
336        @return: list of attribute values for subject corresponding to given
337        policy designator.  Return None if none can be found
338        @rtype: ndg.xacml.utils.TypedList(<designator attribute type>) / None
339        """
340        return self.pip.attributeQuery(request, designator)
341   
342    def _createXacmlRequestCtx(self, samlAuthzDecisionQuery):
343        """Translate SAML authorisation decision query into a XACML request
344        context
345        """
346        xacmlRequest = _xacmlContext.request.Request()
347        xacmlSubject = _xacmlContext.subject.Subject()
348       
349        xacmlAttributeValueFactory = XacmlAttributeValueClassFactory()
350       
351        openidSubjectAttribute = XacmlAttribute()
352        roleAttribute = XacmlAttribute()
353       
354        openidSubjectAttribute.attributeId = \
355                                samlAuthzDecisionQuery.subject.nameID.format
356                                       
357        XacmlAnyUriAttributeValue = xacmlAttributeValueFactory(
358                                    'http://www.w3.org/2001/XMLSchema#anyURI')
359       
360        openidSubjectAttribute.dataType = XacmlAnyUriAttributeValue.IDENTIFIER
361       
362        openidSubjectAttribute.attributeValues.append(
363                                                    XacmlAnyUriAttributeValue())
364        openidSubjectAttribute.attributeValues[-1].value = \
365                                samlAuthzDecisionQuery.subject.nameID.value
366       
367        xacmlSubject.attributes.append(openidSubjectAttribute)
368
369        XacmlStringAttributeValue = xacmlAttributeValueFactory(
370                                    'http://www.w3.org/2001/XMLSchema#string')
371
372        # TODO: get attributes - replace hard coded values
373        roleAttribute.attributeId = "urn:ndg:security:authz:1.0:attr"
374        roleAttribute.dataType = XacmlStringAttributeValue.IDENTIFIER
375       
376        roleAttribute.attributeValues.append(XacmlStringAttributeValue())
377        roleAttribute.attributeValues[-1].value = 'staff' 
378       
379        xacmlSubject.attributes.append(roleAttribute)
380                                 
381        xacmlRequest.subjects.append(xacmlSubject)
382       
383        resource = _xacmlContext.resource.Resource()
384        resourceAttribute = XacmlAttribute()
385        resource.attributes.append(resourceAttribute)
386       
387        resourceAttribute.attributeId = \
388                            "urn:oasis:names:tc:xacml:1.0:resource:resource-id"
389                           
390        resourceAttribute.dataType = XacmlAnyUriAttributeValue.IDENTIFIER
391        resourceAttribute.attributeValues.append(XacmlAnyUriAttributeValue())
392        resourceAttribute.attributeValues[-1].value = \
393                                                samlAuthzDecisionQuery.resource
394
395        xacmlRequest.resources.append(resource)
396       
397        xacmlRequest.action = _xacmlContext.action.Action()
398       
399        for action in samlAuthzDecisionQuery.actions:
400            xacmlActionAttribute = XacmlAttribute()
401            xacmlRequest.action.attributes.append(xacmlActionAttribute)
402           
403            xacmlActionAttribute.attributeId = \
404                                "urn:oasis:names:tc:xacml:1.0:action:action-id"
405            xacmlActionAttribute.dataType = XacmlStringAttributeValue.IDENTIFIER
406            xacmlActionAttribute.attributeValues.append(
407                                                    XacmlStringAttributeValue())
408            xacmlActionAttribute.attributeValues[-1].value = action.value
409       
410        return xacmlRequest
411   
412    def _createSAMLResponseAssertion(self, authzDecisionQuery, response):
413        """Helper method to add an assertion containing an Authorisation
414        Decision Statement to the SAML response
415       
416        @param authzDecisionQuery: SAML Authorisation Decision Query
417        @type authzDecisionQuery: ndg.saml.saml2.core.AuthzDecisionQuery
418        @param response: SAML response
419        @type response: ndg.saml.saml2.core.Response
420        """
421       
422        # Check for a response set, if none present create one.
423        if response is None:
424            response = _saml.Response()
425           
426            now = datetime.utcnow()
427            response.issueInstant = now
428           
429            # Make up a request ID that this response is responding to
430            response.inResponseTo = authzDecisionQuery.id
431            response.id = str(uuid4())
432            response.version = SAMLVersion(SAMLVersion.VERSION_20)
433               
434            response.issuer = _saml.Issuer()
435            response.issuer.format = self.issuerFormat
436            response.issuer.value = self.issuerName
437   
438            response.status = _saml.Status()
439            response.status.statusCode = _saml.StatusCode()
440            response.status.statusMessage = _saml.StatusMessage()       
441           
442            response.status.statusCode.value = _saml.StatusCode.SUCCESS_URI
443            response.status.statusMessage.value = ("Response created "
444                                                   "successfully")
445       
446        assertion = _saml.Assertion()
447        response.assertions.append(assertion)
448           
449        assertion.version = SAMLVersion(SAMLVersion.VERSION_20)
450        assertion.id = str(uuid4())
451       
452        now = datetime.utcnow()
453        assertion.issueInstant = now
454       
455        # Add a conditions statement for a validity of 8 hours
456        assertion.conditions = _saml.Conditions()
457        assertion.conditions.notBefore = now
458        assertion.conditions.notOnOrAfter = now + timedelta(
459                                                seconds=self.assertionLifetime)
460               
461        assertion.subject = _saml.Subject()
462        assertion.subject.nameID = _saml.NameID()
463        assertion.subject.nameID.format = \
464            authzDecisionQuery.subject.nameID.format
465        assertion.subject.nameID.value = \
466            authzDecisionQuery.subject.nameID.value
467       
468        authzDecisionStatement = _saml.AuthzDecisionStatement()
469        assertion.authzDecisionStatements.append(authzDecisionStatement)
470                   
471        authzDecisionStatement.resource = authzDecisionQuery.resource
472       
473        for action in authzDecisionQuery.actions:
474            authzDecisionStatement.actions.append(_saml.Action())
475            authzDecisionStatement.actions[-1].namespace = action.namespace
476            authzDecisionStatement.actions[-1].value = action.value
477
478        return response
Note: See TracBrowser for help on using the repository browser.