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

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

Fixes for testing OpenID Relying Party running in the application code stack instead of the separate services stack:

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