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

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

Added a wrapper class to Paste Deploy httpserver to enable automated setup and teardown of services for unit tests.

Line 
1"""WSGI Policy Enforcement Point Package
2
3NERC DataGrid Project
4"""
5__author__ = "P J Kershaw"
6__date__ = "16/01/2009"
7__copyright__ = "(C) 2009 Science and Technology Facilities Council"
8__contact__ = "Philip.Kershaw@stfc.ac.uk"
9__revision__ = "$Id$"
10__license__ = "BSD - see LICENSE file in top-levle directory"
11import logging
12log = logging.getLogger(__name__)
13from time import time
14from urlparse import urlunsplit
15from httplib import UNAUTHORIZED, FORBIDDEN
16
17from ndg.security.common.utils.classfactory import importClass
18from ndg.security.common.X509 import X500DN
19from ndg.security.server.wsgi import NDGSecurityMiddlewareBase, \
20    NDGSecurityMiddlewareConfigError
21
22from ndg.security.server.wsgi import NDGSecurityMiddlewareBase, \
23    NDGSecurityMiddlewareConfigError
24from ndg.security.server.wsgi.authn import SessionMiddlewareBase
25
26from ndg.security.common.authz.msi import Policy, PIP, PDP, Request, \
27    Response, Resource, Subject
28
29class PEPResultHandlerMiddleware(SessionMiddlewareBase):
30    """This middleware is invoked if access is denied to a given resource.  It
31    is incorporated into the call stack by passing it in to a MultiHandler
32    instance.  The MultiHandler is configured in the AuthorizationMiddleware
33    class below.  The MultiHandler is passed a checker method which determines
34    whether to allow access, or call this interface.   The checker is
35    implemented in the AuthorizationHandler.  See below ...
36   
37    This class can be overridden to define custom behaviour for the access
38    denied response e.g. include an interface to enable users to register for
39    the dataset from which they have been denied access.  See
40    AuthorizationMiddleware pepResultHandler keyword.
41   
42    SessionMiddlewareBase base class defines user session key and
43    isAuthenticated property
44    """
45   
46    def __init__(self, app, global_conf, prefix='', **app_conf):
47        '''
48        @type app: callable following WSGI interface
49        @param app: next middleware application in the chain     
50        @type global_conf: dict       
51        @param global_conf: PasteDeploy global configuration dictionary
52        @type prefix: basestring
53        @param prefix: prefix for configuration items
54        @type app_conf: dict       
55        @param app_conf: PasteDeploy application specific configuration
56        dictionary
57        '''
58        super(PEPResultHandlerMiddleware, self).__init__(app,
59                                                         global_conf,
60                                                         prefix=prefix,
61                                                         **app_conf)
62               
63    @NDGSecurityMiddlewareBase.initCall
64    def __call__(self, environ, start_response):
65       
66        log.debug("PEPResultHandlerMiddleware.__call__ ...")
67       
68        self.session = self.environ.get(self.sessionKey)
69        if not self.isAuthenticated:
70            # This check is included as a precaution: this condition should be
71            # caught be the AuthNRedirectHandlerMiddleware or PEPFilter
72            log.warning("PEPResultHandlerMiddleware: user is not "
73                        "authenticated - setting HTTP 401 response")
74            return self._setErrorResponse(code=UNAUTHORIZED)
75        else:
76            # Get response message from PDP recorded by PEP
77            pepCtx = self.session.get('pepCtx', {})
78            pdpResponse = pepCtx.get('response')
79            msg = getattr(pdpResponse, 'message', '')
80               
81            response = ("Access is forbidden for this resource:%s"
82                        "Please check with your site administrator that you "
83                        "have the required access privileges." % 
84                        msg.join('\n\n'*2))
85
86            return self._setErrorResponse(code=FORBIDDEN, msg=response)
87
88
89class PEPFilterError(Exception):
90    """Base class for PEPFilter exception types"""
91   
92class PEPFilterConfigError(PEPFilterError):
93    """Configuration related error for PEPFilter"""
94
95class PEPFilter(SessionMiddlewareBase):
96    """PEP (Policy Enforcement Point) WSGI Middleware.  The PEP enforces
97    access control decisions made by the PDP (Policy Decision Point).  In
98    this case, it follows the WSG middleware filter pattern and is configured
99    in a pipeline upstream of the application(s) which it protects.  if an
100    access denied decision is made, the PEP enforces this by returning a
101    403 Forbidden HTTP response without the application middleware executing
102   
103    SessionMiddlewareBase base class defines user session key and
104    isAuthenticated property
105    """
106    TRIGGER_HTTP_STATUS_CODE = str(FORBIDDEN)
107    MIDDLEWARE_ID = 'PEPFilter'
108    POLICY_PARAM_PREFIX = 'policy.'
109    SESSION_KEYNAME = 'sessionKey'
110    POLICY_FILEPATH_PARAMNAME = 'filePath'
111   
112    def __init__(self, app, global_conf, prefix='', **local_conf):
113        """Initialise the PIP (Policy Information Point) and PDP (Policy
114        Decision Point).  The PDP makes access control decisions based on
115        a given policy.  The PIP manages the retrieval of user credentials on
116        behalf of the PDP
117       
118        @type app: callable following WSGI interface
119        @param app: next middleware application in the chain     
120        @type global_conf: dict       
121        @param global_conf: PasteDeploy global configuration dictionary
122        @type prefix: basestring
123        @param prefix: prefix for configuration items
124        @type local_conf: dict       
125        @param local_conf: PasteDeploy application specific configuration
126        dictionary
127       
128        """       
129        # Initialise the PDP reading in the policy
130        policyCfg = PEPFilter._filterKeywords(local_conf, 
131                                              PEPFilter.POLICY_PARAM_PREFIX)
132        self.policyFilePath = policyCfg[PEPFilter.POLICY_FILEPATH_PARAMNAME]
133        policy = Policy.Parse(policyCfg[PEPFilter.POLICY_FILEPATH_PARAMNAME])
134       
135        # Initialise the Policy Information Point to None.  This object is
136        # created and set later.  See AuthorizationMiddleware.
137        self.pdp = PDP(policy, None)
138       
139        self.sessionKey = local_conf.get(PEPFilter.SESSION_KEYNAME, 
140                                         PEPFilter.propertyDefaults[
141                                                    PEPFilter.SESSION_KEYNAME])
142       
143        super(PEPFilter, self).__init__(app,
144                                        global_conf,
145                                        prefix=prefix,
146                                        **local_conf)
147
148    @NDGSecurityMiddlewareBase.initCall
149    def __call__(self, environ, start_response):
150        """
151        @type environ: dict
152        @param environ: WSGI environment variables dictionary
153        @type start_response: function
154        @param start_response: standard WSGI start response function
155        @rtype: iterable
156        @return: response
157        """
158        session = environ.get(self.sessionKey)
159        if session is None:
160            raise PEPFilterConfigError('No beaker session key "%s" found in '
161                                       'environ' % self.sessionKey)
162           
163        queryString = environ.get('QUERY_STRING', '')
164        resourceURI = urlunsplit(('', '', self.pathInfo, queryString, ''))
165       
166        # Check for a secured resource
167        matchingTargets = self._getMatchingTargets(resourceURI)
168        targetMatch = len(matchingTargets) > 0
169        if not targetMatch:
170            log.debug("PEPFilter.__call__: granting access - no matching URI "
171                      "path target was found in the policy for URI path [%s]", 
172                      resourceURI)
173            return self._app(environ, start_response)
174
175        log.debug("PEPFilter.__call__: found matching target(s):\n\n %s\n"
176                  "\nfrom policy file [%s] for URI Path=[%s]\n",
177                  '\n'.join(["RegEx=%s" % t for t in matchingTargets]), 
178                  self.policyFilePath,
179                  resourceURI)
180       
181        if not self.isAuthenticated:
182            log.info("PEPFilter.__call__: user is not authenticated - setting "
183                     "HTTP 401 response ...")
184           
185            # Set a 401 response for an authentication handler to capture
186            return self._setErrorResponse(code=UNAUTHORIZED)
187       
188        log.debug("PEPFilter.__call__: creating request to call PDP to check "
189                  "user authorisation ...")
190       
191        # Make a request object to pass to the PDP
192        request = Request()
193        request.subject[Subject.USERID_NS] = session['username']
194       
195        # IdP Session Manager specific settings:
196        #
197        # The following won't be set if the IdP running the OpenID Provider
198        # hasn't also deployed a Session Manager.  In this case, the
199        # Attribute Authority will be queried directly from here without a
200        # remote Session Manager intermediary to cache credentials
201        request.subject[Subject.SESSIONID_NS] = session.get('sessionId')
202        request.subject[Subject.SESSIONMANAGERURI_NS] = session.get(
203                                                        'sessionManagerURI')
204        request.resource[Resource.URI_NS] = resourceURI
205
206       
207        # Call the PDP
208        response = self.pdp.evaluate(request)       
209       
210        # Record the result in the user's session to enable later
211        # interrogation by the AuthZResultHandlerMiddleware
212        session['pepCtx'] = {'request': request, 'response': response,
213                             'timestamp': time()}
214        session.save()
215       
216        if response.status == Response.DECISION_PERMIT:
217            log.info("PEPFilter.__call__: PDP granted access for URI path "
218                     "[%s] using policy [%s]", 
219                     resourceURI, 
220                     self.policyFilePath)
221           
222            return self._app(environ, start_response)
223        else:
224            log.info("PEPFilter.__call__: PDP returned a status of [%s] "
225                     "denying access for URI path [%s] using policy [%s]", 
226                     response.decisionValue2String[response.status],
227                     resourceURI,
228                     self.policyFilePath) 
229           
230            # Trigger AuthZResultHandlerMiddleware by setting a response
231            # with HTTP status code equal to the TRIGGER_HTTP_STATUS_CODE class
232            # attribute value
233            triggerStatusCode = int(PEPFilter.TRIGGER_HTTP_STATUS_CODE)
234            return self._setErrorResponse(code=triggerStatusCode)
235
236    def _getMatchingTargets(self, resourceURI):
237        """This method may only be called following __call__ as __call__
238        updates the pathInfo property
239       
240        @type resourceURI: basestring
241        @param resourceURI: the URI of the requested resource
242        @rtype: list
243        @return: return list of policy target objects matching the current
244        path
245        """
246        matchingTargets = [target for target in self.pdp.policy.targets
247                           if target.regEx.match(resourceURI) is not None]
248        return matchingTargets
249
250    def multiHandlerInterceptFactory(self):
251        """Return a checker function for use with AuthKit's MultiHandler.
252        MultiHandler can be used to catch HTTP 403 Forbidden responses set by
253        an application and call middleware (AuthZResultMiddleware) to handle
254        the access denied message.
255        """
256       
257        def multiHandlerIntercept(environ, status, headers):
258            """AuthKit MultiHandler checker function to intercept
259            unauthorised response status codes from applications to be
260            protected.  This function's definition is embedded into a
261            factory method so that this function has visibility to the
262            PEPFilter object's attributes if required.
263           
264            @type environ: dict
265            @param environ: WSGI environment dictionary
266            @type status: basestring
267            @param status: HTTP response code set by application middleware
268            that this intercept function is to protect
269            @type headers: list
270            @param headers: HTTP response header content"""
271           
272            if status.startswith(PEPFilter.TRIGGER_HTTP_STATUS_CODE):
273                log.debug("PEPFilter: found [%s] status for URI path [%s]: "
274                          "invoking access denied response",
275                          PEPFilter.TRIGGER_HTTP_STATUS_CODE,
276                          environ['PATH_INFO'])
277                return True
278            else:
279                # No match - it's publicly accessible
280                log.debug("PEPFilter: the return status [%s] for this URI "
281                          "path [%s] didn't match the trigger status [%s]",
282                          status,
283                          environ['PATH_INFO'],
284                          PEPFilter.TRIGGER_HTTP_STATUS_CODE)
285                return False
286       
287        return multiHandlerIntercept
288       
289    @staticmethod
290    def _filterKeywords(conf, prefix):
291        filteredConf = {}
292        prefixLen = len(prefix)
293        for k, v in conf.items():
294            if k.startswith(prefix):
295                filteredConf[k[prefixLen:]] = conf.pop(k)
296               
297        return filteredConf
298
299    def _getPDP(self):
300        if self._pdp is None:
301            raise TypeError("PDP object has not been initialised")
302        return self._pdp
303   
304    def _setPDP(self, pdp):
305        if not isinstance(pdp, (PDP, None.__class__)):
306            raise TypeError("Expecting %s or None type for pdp; got %r" %
307                            (PDP.__class__.__name__, pdp))
308        self._pdp = pdp
309
310    pdp = property(fget=_getPDP,
311                   fset=_setPDP,
312                   doc="Policy Decision Point object makes access control "
313                       "decisions on behalf of the PEP")
314
315   
316from ndg.security.common.authz.msi import PIP
317from ndg.security.common.credentialwallet import CredentialWallet
318
319class PIPMiddlewareError(Exception):
320    """Base class for Policy Information Point WSGI middleware exception types
321    """
322   
323class PIPMiddlewareConfigError(PIPMiddlewareError):
324    """Configuration related error for Policy Information Point WSGI middleware
325    """
326   
327class PIPMiddleware(PIP, NDGSecurityMiddlewareBase):
328    '''Extend Policy Information Point to enable caching of credentials in
329    a CredentialWallet object held in beaker.session
330    '''
331    environKey = 'ndg.security.server.wsgi.authz.PIPMiddleware'
332       
333    propertyDefaults = {
334        'sessionKey': 'beaker.session.ndg.security',
335    }
336    propertyDefaults.update(NDGSecurityMiddlewareBase.propertyDefaults)
337 
338    def __init__(self, app, global_conf, prefix='', **local_conf):
339        '''
340        @type app: callable following WSGI interface
341        @param app: next middleware application in the chain     
342        @type global_conf: dict       
343        @param global_conf: PasteDeploy global configuration dictionary
344        @type prefix: basestring
345        @param prefix: prefix for configuration items
346        @type local_conf: dict       
347        @param local_conf: PasteDeploy application specific configuration
348        dictionary
349        '''
350       
351        # Pre-process list items splitting as needed
352        if isinstance(local_conf.get('caCertFilePathList'), basestring):
353            local_conf['caCertFilePathList'] = \
354                NDGSecurityMiddlewareBase.parseListItem(
355                                        local_conf['caCertFilePathList'])
356           
357        if isinstance(local_conf.get('sslCACertFilePathList'), basestring):
358            local_conf['sslCACertFilePathList'] = \
359                NDGSecurityMiddlewareBase.parseListItem(
360                                        local_conf['sslCACertFilePathList'])
361           
362        PIP.__init__(self, prefix=prefix, **local_conf)
363       
364        for k in local_conf.keys():
365            if k.startswith(prefix):
366                del local_conf[k]
367               
368        NDGSecurityMiddlewareBase.__init__(self,
369                                           app,
370                                           global_conf,
371                                           prefix=prefix,
372                                           **local_conf)
373       
374    def __call__(self, environ, start_response):
375        """Take a copy of the session object so that it is in scope for
376        _getAttributeCertificate call and add this instance to the environ
377        so that the PEPFilter can retrieve it and pass on to the PDP
378       
379        @type environ: dict
380        @param environ: WSGI environment variables dictionary
381        @type start_response: function
382        @param start_response: standard WSGI start response function
383        @rtype: iterable
384        @return: response
385        """
386        self.session = environ.get(self.sessionKey)
387        if self.session is None:
388            raise PIPMiddlewareConfigError('No beaker session key "%s" found '
389                                           'in environ' % self.sessionKey)
390        environ[PIPMiddleware.environKey] = self
391       
392        return self._app(environ, start_response)
393               
394    def _getAttributeCertificate(self, attributeAuthorityURI, **kw):
395        '''Extend base class implementation to make use of the CredentialWallet
396        Attribute Certificate cache held in the beaker session.  If no suitable
397        certificate is present invoke default behaviour and retrieve an
398        Attribute Certificate from the Attribute Authority or Session Manager
399        specified
400
401        @type attributeAuthorityURI: basestring
402        @param attributeAuthorityURI: URI to Attribute Authority service
403        @type username: basestring
404        @param username: subject user identifier - could be an OpenID       
405        @type sessionId: basestring
406        @param sessionId: Session Manager session handle
407        @type sessionManagerURI: basestring
408        @param sessionManagerURI: URI to remote session manager service
409        @rtype: ndg.security.common.AttCert.AttCert
410        @return: Attribute Certificate containing user roles
411        '''
412        # Check for a wallet in the current session - if not present, create
413        # one.  See ndg.security.server.wsgi.authn.SessionHandlerMiddleware
414        # for session keys.  The 'credentialWallet' key is deleted along with
415        # any other security keys when the user logs out
416        if not 'credentialWallet' in self.session:
417            log.debug("PIPMiddleware._getAttributeCertificate: adding a "
418                      "Credential Wallet to user session [%s] ...",
419                      self.session['username'])
420           
421            self.session['credentialWallet'] = CredentialWallet(
422                                            userId=self.session['username'])
423            self.session.save()
424           
425        # Take reference to wallet for efficiency
426        credentialWallet = self.session['credentialWallet']   
427       
428        # Check for existing credentials cached in wallet           
429        credential = credentialWallet.credentialsKeyedByURI.get(
430                                                    attributeAuthorityURI, {})
431       
432        attrCert = credential.get('attCert')
433        if attrCert is not None:
434            log.debug("PIPMiddleware._getAttributeCertificate: retrieved "
435                      "existing Attribute Certificate cached in Credential "
436                      "Wallet for user session [%s]",
437                      self.session['username'])
438
439            # Existing cached credential found - skip call to remote Session
440            # Manager / Attribute Authority and return this certificate instead
441            return attrCert
442        else:   
443            attrCert = PIP._getAttributeCertificate(self,
444                                                    attributeAuthorityURI,
445                                                    **kw)
446           
447            log.debug("PIPMiddleware._getAttributeCertificate: updating "
448                      "Credential Wallet with retrieved Attribute "
449                      "Certificate for user session [%s]",
450                      self.session['username'])
451       
452            # Update the wallet with this Attribute Certificate so that it's
453            # cached for future calls
454            credentialWallet.addCredential(attrCert,
455                                attributeAuthorityURI=attributeAuthorityURI)
456           
457            return attrCert
458
459       
460from authkit.authenticate.multi import MultiHandler
461
462class AuthorizationMiddlewareError(Exception):
463    """Base class for AuthorizationMiddleware exceptions"""
464   
465class AuthorizationMiddlewareConfigError(Exception):
466    """AuthorizationMiddleware configuration related exceptions"""
467   
468class AuthorizationMiddleware(NDGSecurityMiddlewareBase):
469    '''Handler to call Policy Enforcement Point middleware to intercept
470    requests and enforce access control decisions.  Add THIS class to any
471    WSGI middleware chain ahead of the application(s) which it is to
472    protect.  Use in conjunction with
473    ndg.security.server.wsgi.authn.AuthenticationMiddleware
474    '''
475    PEP_PARAM_PREFIX = 'pep.filter.'
476    PIP_PARAM_PREFIX = 'pip.'
477    PEP_RESULT_HANDLER_PARAMNAME = "pepResultHandler"
478   
479    def __init__(self, app, global_conf, prefix='', **app_conf):
480        """Set-up Policy Enforcement Point to enforce access control decisions
481        based on the URI path requested and/or the HTTP response code set by
482        application(s) to be protected.  An AuthKit MultiHandler is setup to
483        handle the latter.  PEPResultHandlerMiddleware handles the output
484        set following an access denied decision
485        @type app: callable following WSGI interface
486        @param app: next middleware application in the chain     
487        @type global_conf: dict       
488        @param global_conf: PasteDeploy global configuration dictionary
489        @type prefix: basestring
490        @param prefix: prefix for configuration items
491        @type app_conf: dict       
492        @param app_conf: PasteDeploy application specific configuration
493        dictionary
494        """
495        authzPrefix = prefix + AuthorizationMiddleware.PEP_PARAM_PREFIX
496        pepFilter = PEPFilter(app,
497                              global_conf,
498                              prefix=authzPrefix,
499                              **app_conf)
500        pepInterceptFunc = pepFilter.multiHandlerInterceptFactory()
501       
502        # Slot in the Policy Information Point in the WSGI stack at this point
503        # so that it can take a copy of the beaker session object from environ
504        # ahead of the PDP's request to it for an Attribute Certificate
505        pipPrefix = AuthorizationMiddleware.PIP_PARAM_PREFIX
506        pipFilter = PIPMiddleware(pepFilter,
507                                  global_conf,
508                                  prefix=pipPrefix,
509                                  **app_conf)
510        pepFilter.pdp.pip = pipFilter
511       
512        app = MultiHandler(pipFilter)
513
514        pepResultHandlerClassName = app_conf.pop(
515                prefix+AuthorizationMiddleware.PEP_RESULT_HANDLER_PARAMNAME, 
516                None)
517        if pepResultHandlerClassName is None:
518            pepResultHandler = PEPResultHandlerMiddleware
519        else:
520            pepResultHandler = importClass(pepResultHandlerClassName,
521                                        objectType=PEPResultHandlerMiddleware)
522           
523        app.add_method(PEPFilter.MIDDLEWARE_ID,
524                       pepResultHandler.filter_app_factory,
525                       global_conf,
526                       prefix=prefix,
527                       **app_conf)
528       
529        app.add_checker(PEPFilter.MIDDLEWARE_ID, pepInterceptFunc)               
530       
531        super(AuthorizationMiddleware, self).__init__(app,
532                                                      global_conf,
533                                                      prefix=prefix,
534                                                      **app_conf)
535               
Note: See TracBrowser for help on using the repository browser.