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

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.server/ndg/security/server/wsgi/authz.py@5329
Revision 5329, 14.1 KB checked in by pjkersha, 11 years ago (diff)
  • Added AuthorizationMiddleware? unit tests: ndg.security.test.unit.wsgi.authz
  • Added check for ndg.security.server.wsgi.authz.PEPFilter to catch beaker session not set in a custom PEPFilter exception type
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
14import httplib
15
16from ndg.security.server.wsgi import NDGSecurityPathFilter
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    TODO: possible refactor to incorporate user role registration interface.
36    For ESG collaboration, the scenario following access denied is to g"""
37    propertyDefaults = {
38        'sessionKey': 'beaker.session.ndg.security'
39    }
40
41    _isAuthenticated = lambda self: \
42                            'username' in self.environ.get(self.sessionKey,())
43    isAuthenticated = property(fget=_isAuthenticated,
44                               doc='boolean to indicate is user logged in')
45
46    def __init__(self, app, global_conf, prefix='', **app_conf):
47       
48        super(PEPResultHandlerMiddleware, self).__init__(app,
49                                                         global_conf,
50                                                         prefix=prefix,
51                                                         **app_conf)
52               
53    @NDGSecurityMiddlewareBase.initCall
54    def __call__(self, environ, start_response):
55       
56        log.debug("PEPResultHandlerMiddleware.__call__ ...")
57       
58        self.session = self.environ.get(self.sessionKey)
59        if not self.isAuthenticated:
60            # This check is included as a precaution: this condition should be
61            # caught be the AuthNRedirectHandlerMiddleware or PEPFilter
62            log.warning("PEPResultHandlerMiddleware: user is not "
63                        "authenticated - setting HTTP 401 response")
64            return self._setErrorResponse(code=401)
65        else:
66            # TODO: refactor to include a call to another interface - possibly
67            # - another WSGI app to set a user friendly output and include
68            # links to enable the user to register for new access privileges
69           
70            # Get response message from PDP recorded by PEP
71            msg = getattr(self.session.get('pepCtx', {}).get('response'),
72                          'message', '')
73               
74            response = \
75"""Access is forbidden for this resource:
76%s
77Please check with your site administrator that you have the required access privileges.
78""" % msg.join('\n'*2)
79
80            return self._setErrorResponse(code=403, msg=response)
81
82class PEPFilterError(Exception):
83    """Base class for PEPFilter exception types"""
84   
85class PEPFilterConfigError(PEPFilterError):
86    """Configuration related error for PEPFilter"""
87
88class PEPFilter(NDGSecurityMiddlewareBase):
89    """PEP (Policy Enforcement Point) WSGI Middleware.  The PEP enforces
90    access control decisions made by the PDP (Policy Decision Point).  In
91    this case, it follows the WSG middleware filter pattern and is configured
92    in a pipeline upstream of the application(s) which it protects.  if an
93    access denied decision is made, the PEP enforces this by returning a
94    403 Forbidden HTTP response without the application middleware executing
95    """
96    triggerStatus = '403'
97    id = 'PEPFilter'
98   
99    propertyDefaults = {
100        'sessionKey': 'beaker.session.ndg.security'
101    }
102
103    _isAuthenticated = lambda self: \
104                            'username' in self.environ.get(self.sessionKey,())
105    isAuthenticated = property(fget=_isAuthenticated,
106                               doc='boolean to indicate is user logged in')
107
108    def __init__(self, app, global_conf, prefix='', **local_conf):
109        """Initialise the PIP (Policy Information Point) and PDP (Policy
110        Decision Point).  The PDP makes access control decisions based on
111        a given policy.  The PIP manages the retrieval of user credentials on
112        behalf of the PDP
113       
114        """
115        pipCfg = PEPFilter._filterKeywords(local_conf, 'pip.')
116        pip = PIP(**pipCfg)
117
118        # Initialise the  reading in the policy
119        policyCfg = PEPFilter._filterKeywords(local_conf, 'policy.')
120        self.policyFilePath = policyCfg['filePath']
121        self.policy = Policy.Parse(policyCfg['filePath'])
122        self.pdp = PDP(self.policy, pip)
123       
124        self.sessionKey = local_conf.get('sessionKey', 
125                                     PEPFilter.propertyDefaults['sessionKey'])
126       
127        super(PEPFilter, self).__init__(app,
128                                        global_conf,
129                                        prefix=prefix,
130                                        **local_conf)
131       
132    @NDGSecurityMiddlewareBase.initCall
133    def __call__(self, environ, start_response):
134       
135        log.debug("PEPFilter.__call__ ...")
136       
137        session = environ.get(self.sessionKey)
138        if session is None:
139            raise PEPFilterConfigError('No beaker session key "%s" found in '
140                                       'environ' % self.sessionKey)
141           
142        resourceURI = self.pathInfo
143       
144        # Check for a secured resource
145        matchingTargets = self._getMatchingTargets()
146        targetMatch = len(matchingTargets) > 0
147        if not targetMatch:
148            log.info("PEPFilter: granting access - no matching URI path "
149                     "target was found in the policy for URI path [%s]", 
150                     resourceURI)
151            return self._app(environ, start_response)
152
153        log.info("PEPFilter found matching target(s):\n\n %s\n"
154                 "\nfrom policy file [%s] for URI Path=[%s]\n",
155                 '\n'.join(["RegEx=%s" % t for t in matchingTargets]), 
156                 self.policyFilePath,
157                 resourceURI)
158       
159        if not self.isAuthenticated:
160            log.info("PEPFilter: user is not authenticated - setting HTTP "
161                     "401 response ...")
162           
163            # Set a 401 response for an authentication handler to capture
164            return self._setErrorResponse(code=401)
165       
166        log.debug("PEPFilter: creating request to call PDP to check user "
167                  "authorisation ...")
168       
169        # Make a request object to pass to the PDP
170        request = Request()
171        request.subject[Subject.USERID_NS] = session['username']
172       
173        # IdP Session Manager specific settings:
174        #
175        # The following won't be set if the IdP running the OpenID Provider
176        # hasn't also deployed a Session Manager.  In this case, the
177        # Attribute Authority will be queried directly from here without a
178        # remote Session Manager intermediary to cache credentials
179        request.subject[Subject.SESSIONID_NS] = session.get('sessionId')
180        request.subject[Subject.SESSIONMANAGERURI_NS] = session.get(
181                                                        'sessionManagerURI')
182        request.resource[Resource.URI_NS] = resourceURI
183
184       
185        # Call the PDP
186        response = self.pdp.evaluate(request)       
187       
188        # Record the result in the user's session to enable later
189        # interrogation by the AuthZResultHandlerMiddleware
190        session['pepCtx'] = {'request': request, 'response': response,
191                             'timestamp': time()}
192        session.save()
193       
194        if response.status == Response.DECISION_PERMIT:
195            log.debug("PEPFilter: PDP granted access for URI path [%s] "
196                      "using policy [%s]", resourceURI, self.policyFilePath)
197           
198            return self._app(environ, start_response)
199        else:
200            log.info("PEPFilter: PDP returned a status of [%s] "
201                     "denying access for URI path [%s] using policy [%s]", 
202                     response.decisionValue2String[response.status],
203                     resourceURI,
204                     self.policyFilePath) 
205           
206            # Trigger AuthZResultHandlerMiddleware by setting a response
207            # with HTTP status code equal to the triggerStatus class attribute
208            # value
209            return self._setErrorResponse(code=int(PEPFilter.triggerStatus))
210
211    def _getMatchingTargets(self):
212        """This method may only be called following __call__ as __call__
213        updates the pathInfo property
214       
215        @rtype: list
216        @return: return list of policy target objects matching the current
217        path
218        """
219        resourceURI = self.pathInfo
220        matchingTargets = [target for target in self.policy.targets
221                           if target.regEx.match(resourceURI) is not None]
222        return matchingTargets
223
224    def multiHandlerInterceptFactory(self):
225        """Return a checker function for use with AuthKit's MultiHandler.
226        MultiHandler can be used to catch HTTP 403 Forbidden responses set by
227        an application and call middleware (AuthZResultMiddleware) to handle
228        the access denied message.
229        """
230       
231        def multiHandlerIntercept(environ, status, headers):
232            """AuthKit MultiHandler checker function to intercept
233            unauthorised response status codes from applications to be
234            protected.  This function's definition is embedded into a
235            factory method so that this function has visibility to the
236            PEPFilter object's attributes if required.
237           
238            @type environ: dict
239            @param environ: WSGI environment dictionary
240            @type status: basestring
241            @param status: HTTP response code set by application middleware
242            that this intercept function is to protect
243            @type headers: list
244            @param headers: HTTP response header content"""
245           
246            if status.startswith(PEPFilter.triggerStatus):
247                log.info("PEPFilter: found [%s] status for URI path [%s]: "
248                         "invoking access denied response",
249                         PEPFilter.triggerStatus,
250                         environ['PATH_INFO'])
251                return True
252            else:
253                # No match - it's publicly accessible
254                log.debug("PEPFilter: the return status [%s] for this URI "
255                          "path [%s] didn't match the trigger status [%s]",
256                          status,
257                          environ['PATH_INFO'],
258                          PEPFilter.triggerStatus)
259                return False
260       
261        return multiHandlerIntercept
262       
263    @staticmethod
264    def _filterKeywords(conf, prefix):
265        filteredConf = {}
266        prefixLen = len(prefix)
267        for k, v in conf.items():
268            if k.startswith(prefix):
269                filteredConf[k[prefixLen:]] = conf.pop(k)
270               
271        return filteredConf
272           
273
274from authkit.authenticate.multi import MultiHandler
275from ndg.security.common.utils.classfactory import importClass
276
277class AuthorizationMiddlewareError(Exception):
278    """Base class for AuthorizationMiddleware exceptions"""
279   
280class AuthorizationMiddlewareConfigError(Exception):
281    """AuthorizationMiddleware configuration related exceptions"""
282   
283class AuthorizationMiddleware(NDGSecurityMiddlewareBase):
284    '''Handler to call Policy Enforcement Point middleware to intercept
285    requests and enforce access control decisions.  Add THIS class to any
286    WSGI middleware chain ahead of the application(s) which it is to
287    protect.  Use in conjunction with
288    ndg.security.server.wsgi.authn.AuthenticationMiddleware
289    '''
290   
291    def __init__(self, app, global_conf, prefix='', **app_conf):
292        """Set-up Policy Enforcement Point to enforce access control decisions
293        based on the URI path requested and/or the HTTP response code set by
294        application(s) to be protected.  An AuthKit MultiHandler is setup to
295        handle the latter.  PEPResultHandlerMiddleware handles the output
296        set following an access denied decision"""
297       
298        pepFilter = PEPFilter(app,
299                              global_conf,
300                              prefix=prefix+'pep.filter.',
301                              **app_conf)
302        pepInterceptFunc = pepFilter.multiHandlerInterceptFactory()
303       
304        app = MultiHandler(pepFilter)
305       
306        pepResultHandlerClassName = app_conf.pop(prefix+"pep.resultHandler", 
307                                                 None) 
308        if pepResultHandlerClassName is None:
309            pepResultHandler = PEPResultHandlerMiddleware
310        else:
311            pepResultHandler = importClass(pepResultHandlerClassName)
312            if not isinstance(pepResultHandler, PEPResultHandlerMiddleware):
313                raise AuthorizationMiddlewareConfigError("Expecting "
314                    "PEPResultHandlerMiddleware derived class for "
315                    "pepResultHandler setting; got %s" % pepResultHandler)
316           
317        app.add_method(PEPFilter.id,
318                       pepResultHandler.filter_app_factory,
319                       global_conf,
320                       prefix=prefix,
321                       **app_conf)
322       
323        app.add_checker(PEPFilter.id, pepInterceptFunc)               
324       
325        super(AuthorizationMiddleware, self).__init__(app,
326                                                      global_conf,
327                                                      prefix=prefix,
328                                                      **app_conf)
329               
Note: See TracBrowser for help on using the repository browser.