source: TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/wsgi/authz/authzservice.py @ 7168

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/wsgi/authz/authzservice.py@7168
Revision 7168, 18.3 KB checked in by pjkersha, 9 years ago (diff)

Incomplete - task 2: XACML-Security Integration

  • testing ndg.security.server.wsgi.authz.pep.SamlPepMiddleware? in ndg.security.test.unit.authz.test_authz
  • Property svn:keywords set to Id
Line 
1"""Authorization service with SAML 2.0 authorisation decision query interface
2
3NERC DataGrid Project
4"""
5__author__ = "P J Kershaw"
6__date__ = "17/02/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 datetime import datetime, timedelta
15from uuid import uuid4
16
17# SAML authorisation decision query interface
18from ndg.saml.saml2.core import (Response, AuthzDecisionStatement, Assertion,
19                                 Action, DecisionType, SAMLVersion, Issuer, 
20                                 Status, StatusCode, StatusMessage, NameID, 
21                                 Subject, Conditions)
22
23from ndg.security.common.authz.pip.esginterface import PIP
24
25# XACML Policy Decision Point
26from ndg.xacml.parsers.etree.factory import ReaderFactory
27from ndg.xacml.core.attribute import Attribute
28from ndg.xacml.core.attributevalue import AttributeValueClassFactory
29from ndg.xacml.core.context.pdpinterface import PDPInterface
30from ndg.xacml.core.context.pdp import PDP
31from ndg.xacml.core.context.request import Request
32from ndg.xacml.core.context.resource import Resource
33from ndg.xacml.core import context
34
35
36class AuthzServiceMiddlewareError(Exception):
37    """Authorisation Service generic exception type"""
38   
39
40class AuthzServiceMiddlewareConfigError(AuthzServiceMiddlewareError):
41    """Authorisation Service configuration error"""
42   
43   
44class AuthzServiceMiddleware(object):
45    '''WSGI to add an NDG Security Authorization Service in the environ.
46    '''
47    DEFAULT_QUERY_IFACE_KEYNAME = \
48                        'ndg.security.server.wsgi.authzservice.queryInterface'
49   
50    ENVIRON_KEYNAME_QUERY_IFACE_OPTNAME = 'queryInterfaceKeyName'
51    ASSERTION_LIFETIME_OPTNAME = 'assertionLifetime'
52   
53    # For loop based assignment where possible of config options in initialise()
54    AUTHZ_SRVC_OPTION_DEFAULTS = {
55        ENVIRON_KEYNAME_QUERY_IFACE_OPTNAME: DEFAULT_QUERY_IFACE_KEYNAME,
56        ASSERTION_LIFETIME_OPTNAME: 60*60*8, # 8 hours as default
57    }
58   
59    POLICY_FILEPATH_OPTNAME = 'policyFilePath'
60    PIP_CFG_PREFIX = 'pip.'
61   
62    __slots__ = (
63        '__policyFilePath',
64        '__queryInterface', 
65        '__' + ENVIRON_KEYNAME_QUERY_IFACE_OPTNAME,
66        '__' + ASSERTION_LIFETIME_OPTNAME,
67        '__pdp', 
68        '_app',
69    )
70       
71    def __init__(self, app):
72        '''Set-up an Authorization Service instance
73        '''
74        self._app = app
75        self.__policyFilePath = None
76        self.__pdp = None
77        self.__queryInterface = None
78        self.__queryInterfaceKeyName = None
79        self.__assertionLifetime = 0.
80       
81    def initialise(self, global_conf, prefix='authorizationservice.',
82                   **app_conf):
83        """Set-up Authorization Service middleware using a Paste app factory
84        pattern.  Overloaded base class method to enable custom settings from
85        app_conf
86       
87        @type app: callable following WSGI interface
88        @param app: next middleware application in the chain     
89        @type global_conf: dict       
90        @param global_conf: PasteDeploy global configuration dictionary
91        @type prefix: basestring
92        @param prefix: prefix for configuration items
93        @type app_conf: dict       
94        @param app_conf: PasteDeploy application specific configuration
95        dictionary
96        """
97        cls = AuthzServiceMiddleware
98       
99        # Loop based assignment where possible
100        for optName, default in cls.AUTHZ_SRVC_OPTION_DEFAULTS.items():
101            value = app_conf.get(prefix + optName, default)
102            setattr(self, optName, value)
103       
104        self.queryInterface = self.createQueryInterface()   
105
106        # Initialise the Authorisation Policy
107        policyFilePathOptName = prefix + cls.POLICY_FILEPATH_OPTNAME
108        policyFilePath = app_conf.get(policyFilePathOptName)
109        if policyFilePath is None:
110            raise AuthzServiceMiddlewareConfigError('No policy file path set - '
111                                                    '"policyFilePath" option '
112                                                    'name')
113       
114        self.policyFilePath = policyFilePath
115       
116        # Initialise the Policy Information Point
117        pipCfgPrefix = prefix + cls.PIP_CFG_PREFIX
118        pip = PIP.fromConfig(app_conf, prefix=pipCfgPrefix)
119           
120        # Initialise the PDP reading in the policy       
121        self.pdp = PDP.fromPolicySource(self.policyFilePath, ReaderFactory)
122       
123    @classmethod
124    def filter_app_factory(cls, app, global_conf, **app_conf):
125        '''Wrapper to enable instantiation compatible with Paste Deploy
126        filter application factory function signature
127       
128        @type app: callable following WSGI interface
129        @param app: next middleware application in the chain     
130        @type global_conf: dict       
131        @param global_conf: PasteDeploy global configuration dictionary
132        @type prefix: basestring
133        @param prefix: prefix for configuration items
134        @type app_conf: dict       
135        @param app_conf: PasteDeploy application specific configuration
136        dictionary
137        '''
138        app = cls(app)
139        app.initialise(global_conf, **app_conf)
140       
141        return app
142   
143    def __call__(self, environ, start_response):
144        '''Set the Authorization Decision function in environ
145       
146        @type environ: dict
147        @param environ: WSGI environment variables dictionary
148        @type start_response: function
149        @param start_response: standard WSGI start response function
150        @rtype: iterable
151        @return: next application in the WSGI stack
152        '''
153        environ[self.queryInterfaceKeyName] = self.queryInterface
154        return self._app(environ, start_response)
155
156    _getPolicyFilePath = lambda self: self.__policyFilePath
157   
158    def _setPolicyFilePath(self, value):
159        if not isinstance(value, basestring):
160            raise TypeError('Expecting string type for "policyFilePath" '
161                            'attribute; got %r' % type(value))
162        self.__policyFilePath = value
163
164    policyFilePath = property(_getPolicyFilePath, 
165                              _setPolicyFilePath, 
166                              doc="Policy file path")
167
168    def _getIssuerFormat(self):
169        if self.__issuerProxy is None:
170            return None
171        else:
172            return self.__issuerProxy.value
173
174    def _setIssuerFormat(self, value):
175        if self.__issuerProxy is None:
176            self.__issuerProxy = Issuer()
177           
178        self.__issuerProxy.format = value
179
180    issuerFormat = property(_getIssuerFormat, _setIssuerFormat, 
181                            doc="Issuer format")
182#
183#    def _getIssuerName(self):
184#        if self.__issuerProxy is None:
185#            return None
186#        else:
187#            return self.__issuerProxy.value
188#
189#    def _setIssuerName(self, value):
190#        if self.__issuerProxy is None:
191#            self.__issuerProxy = Issuer()
192#           
193#        self.__issuerProxy.value = value
194#
195#    issuerName = property(_getIssuerName, _setIssuerName,
196#                          doc="Name of issuer of SAML Authorisation Query "
197#                              "Response")
198   
199    _getAssertionLifetime = lambda self: self.__assertionLifetime
200   
201    def _setAssertionLifetime(self, value):
202        if isinstance(value, (int, float, basestring)):
203            self.__assertionLifetime = float(value)
204        else:
205            raise TypeError('Expecting int, float or string type for '
206                            '"assertionLifetime" attribute; got %s instead' % 
207                            type(value))
208
209    assertionLifetime = property(fget=_getAssertionLifetime,
210                                 fset=_setAssertionLifetime,
211                                 doc="lifetime of assertion in seconds used to "
212                                     "set assertion conditions notOnOrAfter "
213                                     "time")
214         
215    def _getPdp(self):
216        return self.__pdp
217
218    def _setPdp(self, value):
219        if not isinstance(value, PDPInterface):
220            raise TypeError('Expecting %r type for "pdp" attribute; got %r '
221                            'instead' % (PDPInterface, value))
222           
223        self.__pdp = value
224
225    pdp = property(_getPdp, _setPdp, None, "Policy Decision Point")
226
227    def _get_queryInterfaceKeyName(self):
228        return self.__queryInterfaceKeyName
229
230    def _set_queryInterfaceKeyName(self, val):
231        if not isinstance(val, basestring):
232            raise TypeError('Expecting %r for "getAuthzDecisionKeyName" '
233                            'attribute; got %r' % (basestring, type(val)))
234        self.__queryInterfaceKeyName = val
235       
236    queryInterfaceKeyName = property(fget=_get_queryInterfaceKeyName, 
237                                     fset=_set_queryInterfaceKeyName, 
238                                     doc="Key name used to index "
239                                         "Authorization Service SAML authz "
240                                         "decision query function in environ "
241                                         "dictionary")
242   
243    def _get_queryInterface(self):
244        return self.__queryInterface
245   
246    def _set_queryInterface(self, value):
247        if isinstance(value, basestring):
248            self.__queryInterface = importModuleObject(value)
249           
250        elif callable(value):
251            self.__queryInterface = value
252        else:
253            raise TypeError('Expecting callable for "queryInterface" '
254                            'attribute; got %r instead.' % type(value))
255   
256    queryInterface = property(_get_queryInterface,
257                              _set_queryInterface,
258                              doc="authorisation decision function set in "
259                                  "environ for downstream SAML Query "
260                                  "middleware to invoke in response to "
261                                  "<authzDecisionQuery>s")
262         
263    def createQueryInterface(self):
264        """Return the authorisation decision function so that __call__ can add
265        it to environ for the SAML Query middleware to pick up and invoke
266       
267        @return: SAML authorisation decision function
268        @rtype: callable
269        """
270       
271        # Nest function within AuthzServiceMiddleware method so that self is
272        # in its scope
273        def getAuthzDecision(authzDecisionQuery, samlResponse):
274            """Authorisation decision function accepts a SAML AuthzDecisionQuery
275            and calls the Policy Decision Point returning a response
276           
277            @type authzDecisionQuery: saml.saml2.core.AuthzDecisionQuery
278            @param authzDecisionQuery: WSGI environment variables dictionary
279            @rtype: saml.saml2.core.Response
280            @return: SAML response containing Authorisation Decision Statement
281            """       
282            request = self._createXacmlRequestCtx(authzDecisionQuery)
283           
284            # Call the PDP
285            xacmlResponse = self.pdp.evaluate(request)
286           
287            # Create the SAML Response
288            self._createSAMLResponseAssertion(authzDecisionQuery, samlResponse)
289            authzDecisionStatement = samlResponse.assertions[0
290                                                    ].authzDecisionStatements[0]
291           
292            if (xacmlResponse.results[0].decision == 
293                context.result.Decision.PERMIT):
294                log.info("AuthzServiceMiddleware.__call__: PDP granted "
295                         "access for URI path [%s] using policy [%s]", 
296                         authzDecisionQuery.resource, 
297                         self.policyFilePath)
298               
299                authzDecisionStatement.decision = DecisionType.PERMIT
300           
301            elif (xacmlResponse.results[0].decision == 
302                  context.result.Decision.INDETERMINATE):
303                log.info("AuthzServiceMiddleware.__call__: PDP returned a "
304                         "status of [%s] denying access for URI path [%s] "
305                         "using policy [%s]", 
306                         context.result.Decision.INDETERMINATE,
307                         authzDecisionQuery.resource,
308                         self.policyFilePath) 
309               
310                authzDecisionStatement.decision = DecisionType.INDETERMINATE
311                 
312            else:
313                log.info("AuthzServiceMiddleware.__call__: PDP returned a "
314                         "status of [%s] denying access for URI path [%s] "
315                         "using policy [%s]", 
316                         context.result.Decision.DENY,
317                         authzDecisionQuery.resource,
318                         self.policyFilePath) 
319               
320                authzDecisionStatement.decision = DecisionType.DENY
321   
322            return samlResponse
323       
324        return getAuthzDecision
325   
326    def _createXacmlRequestCtx(self, authzDecisionQuery):
327        """Translate SAML authorisation decision query into a XACML request
328        context
329        """
330        request = context.request.Request()
331        subject = context.subject.Subject()
332       
333        attributeValueFactory = AttributeValueClassFactory()
334       
335        openidSubjectAttribute = Attribute()
336        roleAttribute = Attribute()
337       
338        openidSubjectAttribute.attributeId = \
339                                        authzDecisionQuery.subject.nameID.format
340                                       
341        AnyUriAttributeValue = attributeValueFactory(
342                                    'http://www.w3.org/2001/XMLSchema#anyURI')
343       
344        openidSubjectAttribute.dataType = AnyUriAttributeValue.IDENTIFIER
345       
346        openidSubjectAttribute.attributeValues.append(AnyUriAttributeValue())
347        openidSubjectAttribute.attributeValues[-1].value = \
348                                    authzDecisionQuery.subject.nameID.value
349       
350        subject.attributes.append(openidSubjectAttribute)
351
352        StringAttributeValue = attributeValueFactory(
353                                    'http://www.w3.org/2001/XMLSchema#string')
354
355        # TODO: get attributes - replace hard coded values
356        roleAttribute.attributeId = "urn:ndg:security:authz:1.0:attr"
357        roleAttribute.dataType = StringAttributeValue.IDENTIFIER
358       
359        roleAttribute.attributeValues.append(StringAttributeValue())
360        roleAttribute.attributeValues[-1].value = 'staff' 
361       
362        subject.attributes.append(roleAttribute)
363                                 
364        request.subjects.append(subject)
365       
366        resource = Resource()
367        resourceAttribute = Attribute()
368        resource.attributes.append(resourceAttribute)
369       
370        resourceAttribute.attributeId = \
371                            "urn:oasis:names:tc:xacml:1.0:resource:resource-id"
372                           
373        resourceAttribute.dataType = AnyUriAttributeValue.IDENTIFIER
374        resourceAttribute.attributeValues.append(AnyUriAttributeValue())
375        resourceAttribute.attributeValues[-1].value = \
376                                                    authzDecisionQuery.resource
377
378        request.resources.append(resource)
379       
380        request.action = context.action.Action()
381        actionAttribute = Attribute()
382        request.action.attributes.append(actionAttribute)
383       
384        actionAttribute.attributeId = \
385                                "urn:oasis:names:tc:xacml:1.0:action:action-id"
386        actionAttribute.dataType = StringAttributeValue.IDENTIFIER
387        actionAttribute.attributeValues.append(StringAttributeValue())
388        actionAttribute.attributeValues[-1].value = authzDecisionQuery.actions[0
389                                                                        ].value
390       
391        return request
392   
393    def _createSAMLResponseAssertion(self, authzDecisionQuery, response):
394        """Helper method to add an assertion containing an Authorisation
395        Decision Statement to the SAML response
396       
397        @param authzDecisionQuery: SAML Authorisation Decision Query
398        @type authzDecisionQuery: ndg.saml.saml2.core.AuthzDecisionQuery
399        @param response: SAML response
400        @type response: ndg.saml.saml2.core.Response
401        """
402#        response = Response()
403#       
404        now = datetime.utcnow()
405        response.issueInstant = now
406#       
407#        # Make up a request ID that this response is responding to
408#        response.inResponseTo = authzDecisionQuery.id
409#        response.id = str(uuid4())
410#        response.version = SAMLVersion(SAMLVersion.VERSION_20)
411#           
412#        response.issuer = Issuer()
413#        response.issuer.format = self.issuerFormat
414#        response.issuer.value = self.issuerName
415#
416#        response.status = Status()
417#        response.status.statusCode = StatusCode()
418#        response.status.statusMessage = StatusMessage()       
419#       
420#        response.status.statusCode.value = StatusCode.SUCCESS_URI
421#        response.status.statusMessage.value = ("Response created "
422#                                               "successfully")
423       
424        assertion = Assertion()
425        response.assertions.append(assertion)
426           
427        assertion.version = SAMLVersion(SAMLVersion.VERSION_20)
428        assertion.id = str(uuid4())
429        assertion.issueInstant = now
430       
431        # Add a conditions statement for a validity of 8 hours
432        assertion.conditions = Conditions()
433        assertion.conditions.notBefore = now
434        assertion.conditions.notOnOrAfter = now + timedelta(
435                                                seconds=self.assertionLifetime)
436               
437        assertion.subject = Subject()
438        assertion.subject.nameID = NameID()
439        assertion.subject.nameID.format = \
440            authzDecisionQuery.subject.nameID.format
441        assertion.subject.nameID.value = \
442            authzDecisionQuery.subject.nameID.value
443       
444        authzDecisionStatement = AuthzDecisionStatement()
445        assertion.authzDecisionStatements.append(authzDecisionStatement)
446                   
447        authzDecisionStatement.resource = authzDecisionQuery.resource
448       
449        for action in authzDecisionQuery.actions:
450            authzDecisionStatement.actions.append(Action())
451            authzDecisionStatement.actions[-1].namespace = action.namespace
452            authzDecisionStatement.actions[-1].value = action.value
453
454        return response
Note: See TracBrowser for help on using the repository browser.