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

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

Unit tested SamlCredentialWallet?.

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[
354                'caCertFilePathList'] = NDGSecurityMiddlewareBase.parseListItem(
355                                            local_conf['caCertFilePathList'])
356           
357        if isinstance(local_conf.get('sslCACertFilePathList'), basestring):
358            local_conf[
359                'sslCACertFilePathList'
360                ] = NDGSecurityMiddlewareBase.parseListItem(
361                                        local_conf['sslCACertFilePathList'])
362           
363        PIP.__init__(self, prefix=prefix, **local_conf)
364       
365        for k in local_conf.keys():
366            if k.startswith(prefix):
367                del local_conf[k]
368               
369        NDGSecurityMiddlewareBase.__init__(self,
370                                           app,
371                                           global_conf,
372                                           prefix=prefix,
373                                           **local_conf)
374       
375    def __call__(self, environ, start_response):
376        """Take a copy of the session object so that it is in scope for
377        _getAttributeCertificate call and add this instance to the environ
378        so that the PEPFilter can retrieve it and pass on to the PDP
379       
380        @type environ: dict
381        @param environ: WSGI environment variables dictionary
382        @type start_response: function
383        @param start_response: standard WSGI start response function
384        @rtype: iterable
385        @return: response
386        """
387        self.session = environ.get(self.sessionKey)
388        if self.session is None:
389            raise PIPMiddlewareConfigError('No beaker session key "%s" found '
390                                           'in environ' % self.sessionKey)
391        environ[PIPMiddleware.environKey] = self
392       
393        return self._app(environ, start_response)
394               
395    def _getAttributeCertificate(self, attributeAuthorityURI, **kw):
396        '''Extend base class implementation to make use of the CredentialWallet
397        Attribute Certificate cache held in the beaker session.  If no suitable
398        certificate is present invoke default behaviour and retrieve an
399        Attribute Certificate from the Attribute Authority or Session Manager
400        specified
401
402        @type attributeAuthorityURI: basestring
403        @param attributeAuthorityURI: URI to Attribute Authority service
404        @type username: basestring
405        @param username: subject user identifier - could be an OpenID       
406        @type sessionId: basestring
407        @param sessionId: Session Manager session handle
408        @type sessionManagerURI: basestring
409        @param sessionManagerURI: URI to remote session manager service
410        @rtype: ndg.security.common.AttCert.AttCert
411        @return: Attribute Certificate containing user roles
412        '''
413        # Check for a wallet in the current session - if not present, create
414        # one.  See ndg.security.server.wsgi.authn.SessionHandlerMiddleware
415        # for session keys.  The 'credentialWallet' key is deleted along with
416        # any other security keys when the user logs out
417        if not 'credentialWallet' in self.session:
418            log.debug("PIPMiddleware._getAttributeCertificate: adding a "
419                      "Credential Wallet to user session [%s] ...",
420                      self.session['username'])
421           
422            self.session['credentialWallet'] = CredentialWallet(
423                                            userId=self.session['username'])
424            self.session.save()
425           
426        # Take reference to wallet for efficiency
427        credentialWallet = self.session['credentialWallet']   
428       
429        # Check for existing credentials cached in wallet           
430        credentialItem = credentialWallet.credentialsKeyedByURI.get(
431                                                    attributeAuthorityURI, {})
432       
433        attrCert = credentialItem.credential
434        if attrCert is not None:
435            log.debug("PIPMiddleware._getAttributeCertificate: retrieved "
436                      "existing Attribute Certificate cached in Credential "
437                      "Wallet for user session [%s]",
438                      self.session['username'])
439
440            # Existing cached credential found - skip call to remote Session
441            # Manager / Attribute Authority and return this certificate instead
442            return attrCert
443        else:   
444            attrCert = PIP._getAttributeCertificate(self,
445                                                    attributeAuthorityURI,
446                                                    **kw)
447           
448            log.debug("PIPMiddleware._getAttributeCertificate: updating "
449                      "Credential Wallet with retrieved Attribute "
450                      "Certificate for user session [%s]",
451                      self.session['username'])
452       
453            # Update the wallet with this Attribute Certificate so that it's
454            # cached for future calls
455            credentialWallet.addCredential(attrCert,
456                                attributeAuthorityURI=attributeAuthorityURI)
457           
458            return attrCert
459
460       
461from authkit.authenticate.multi import MultiHandler
462
463class AuthorizationMiddlewareError(Exception):
464    """Base class for AuthorizationMiddleware exceptions"""
465   
466class AuthorizationMiddlewareConfigError(Exception):
467    """AuthorizationMiddleware configuration related exceptions"""
468   
469class AuthorizationMiddleware(NDGSecurityMiddlewareBase):
470    '''Handler to call Policy Enforcement Point middleware to intercept
471    requests and enforce access control decisions.  Add THIS class to any
472    WSGI middleware chain ahead of the application(s) which it is to
473    protect.  Use in conjunction with
474    ndg.security.server.wsgi.authn.AuthenticationMiddleware
475    '''
476    PEP_PARAM_PREFIX = 'pep.filter.'
477    PIP_PARAM_PREFIX = 'pip.'
478    PEP_RESULT_HANDLER_PARAMNAME = "pepResultHandler"
479   
480    def __init__(self, app, global_conf, prefix='', **app_conf):
481        """Set-up Policy Enforcement Point to enforce access control decisions
482        based on the URI path requested and/or the HTTP response code set by
483        application(s) to be protected.  An AuthKit MultiHandler is setup to
484        handle the latter.  PEPResultHandlerMiddleware handles the output
485        set following an access denied decision
486        @type app: callable following WSGI interface
487        @param app: next middleware application in the chain     
488        @type global_conf: dict       
489        @param global_conf: PasteDeploy global configuration dictionary
490        @type prefix: basestring
491        @param prefix: prefix for configuration items
492        @type app_conf: dict       
493        @param app_conf: PasteDeploy application specific configuration
494        dictionary
495        """
496        authzPrefix = prefix + AuthorizationMiddleware.PEP_PARAM_PREFIX
497        pepFilter = PEPFilter(app,
498                              global_conf,
499                              prefix=authzPrefix,
500                              **app_conf)
501        pepInterceptFunc = pepFilter.multiHandlerInterceptFactory()
502       
503        # Slot in the Policy Information Point in the WSGI stack at this point
504        # so that it can take a copy of the beaker session object from environ
505        # ahead of the PDP's request to it for an Attribute Certificate
506        pipPrefix = AuthorizationMiddleware.PIP_PARAM_PREFIX
507        pipFilter = PIPMiddleware(pepFilter,
508                                  global_conf,
509                                  prefix=pipPrefix,
510                                  **app_conf)
511        pepFilter.pdp.pip = pipFilter
512       
513        app = MultiHandler(pipFilter)
514
515        pepResultHandlerClassName = app_conf.pop(
516                prefix+AuthorizationMiddleware.PEP_RESULT_HANDLER_PARAMNAME, 
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.MIDDLEWARE_ID,
525                       pepResultHandler.filter_app_factory,
526                       global_conf,
527                       prefix=prefix,
528                       **app_conf)
529       
530        app.add_checker(PEPFilter.MIDDLEWARE_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.