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

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

Completed AuthorizationMiddleware? unit tests ndg.security.test.unit.wsgi.authz:

  • Test 8, 'test08AccessDeniedForAdminQueryArg' tries out the use case for a URI which can display additional content for users with admin privileges. The caller needs to be able to display the correct content according to whether the user has admin rights or not:
    1. the caller invokes /securedURI?admin=1
    2. if the user has admin, rights the PDP will grant access and the PEP will deliver this URI.
    3. if the user doesn't have admin rights, a special overloaded PEP result handler class detects that access was denied for the admin URI and redirects the user to a modified URI subtracting the admin flag. The application code can then deliver the appropriate content minus admin privileges.
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.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    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        super(PEPResultHandlerMiddleware, self).__init__(app,
52                                                         global_conf,
53                                                         prefix=prefix,
54                                                         **app_conf)
55               
56    @NDGSecurityMiddlewareBase.initCall
57    def __call__(self, environ, start_response):
58       
59        log.debug("PEPResultHandlerMiddleware.__call__ ...")
60       
61        self.session = self.environ.get(self.sessionKey)
62        if not self.isAuthenticated:
63            # This check is included as a precaution: this condition should be
64            # caught be the AuthNRedirectHandlerMiddleware or PEPFilter
65            log.warning("PEPResultHandlerMiddleware: user is not "
66                        "authenticated - setting HTTP 401 response")
67            return self._setErrorResponse(code=401)
68        else:
69            # Get response message from PDP recorded by PEP
70            pepCtx = self.session.get('pepCtx', {})
71            pdpResponse = pepCtx.get('response')
72            msg = getattr(pdpResponse, '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        queryString = environ.get('QUERY_STRING', '')
143        resourceURI = urlunsplit(('', '', self.pathInfo, queryString, ''))
144       
145        # Check for a secured resource
146        matchingTargets = self._getMatchingTargets(resourceURI)
147        targetMatch = len(matchingTargets) > 0
148        if not targetMatch:
149            log.info("PEPFilter: granting access - no matching URI path "
150                     "target was found in the policy for URI path [%s]", 
151                     resourceURI)
152            return self._app(environ, start_response)
153
154        log.info("PEPFilter found matching target(s):\n\n %s\n"
155                 "\nfrom policy file [%s] for URI Path=[%s]\n",
156                 '\n'.join(["RegEx=%s" % t for t in matchingTargets]), 
157                 self.policyFilePath,
158                 resourceURI)
159       
160        if not self.isAuthenticated:
161            log.info("PEPFilter: user is not authenticated - setting HTTP "
162                     "401 response ...")
163           
164            # Set a 401 response for an authentication handler to capture
165            return self._setErrorResponse(code=401)
166       
167        log.debug("PEPFilter: creating request to call PDP to check user "
168                  "authorisation ...")
169       
170        # Make a request object to pass to the PDP
171        request = Request()
172        request.subject[Subject.USERID_NS] = session['username']
173       
174        # IdP Session Manager specific settings:
175        #
176        # The following won't be set if the IdP running the OpenID Provider
177        # hasn't also deployed a Session Manager.  In this case, the
178        # Attribute Authority will be queried directly from here without a
179        # remote Session Manager intermediary to cache credentials
180        request.subject[Subject.SESSIONID_NS] = session.get('sessionId')
181        request.subject[Subject.SESSIONMANAGERURI_NS] = session.get(
182                                                        'sessionManagerURI')
183        request.resource[Resource.URI_NS] = resourceURI
184
185       
186        # Call the PDP
187        response = self.pdp.evaluate(request)       
188       
189        # Record the result in the user's session to enable later
190        # interrogation by the AuthZResultHandlerMiddleware
191        session['pepCtx'] = {'request': request, 'response': response,
192                             'timestamp': time()}
193        session.save()
194       
195        if response.status == Response.DECISION_PERMIT:
196            log.debug("PEPFilter: PDP granted access for URI path [%s] "
197                      "using policy [%s]", resourceURI, self.policyFilePath)
198           
199            return self._app(environ, start_response)
200        else:
201            log.info("PEPFilter: PDP returned a status of [%s] "
202                     "denying access for URI path [%s] using policy [%s]", 
203                     response.decisionValue2String[response.status],
204                     resourceURI,
205                     self.policyFilePath) 
206           
207            # Trigger AuthZResultHandlerMiddleware by setting a response
208            # with HTTP status code equal to the triggerStatus class attribute
209            # value
210            return self._setErrorResponse(code=int(PEPFilter.triggerStatus))
211
212    def _getMatchingTargets(self, resourceURI):
213        """This method may only be called following __call__ as __call__
214        updates the pathInfo property
215       
216        @type resourceURI: basestring
217        @param resourceURI: the URI of the requested resource
218        @rtype: list
219        @return: return list of policy target objects matching the current
220        path
221        """
222        matchingTargets = [target for target in self.policy.targets
223                           if target.regEx.match(resourceURI) is not None]
224        return matchingTargets
225
226    def multiHandlerInterceptFactory(self):
227        """Return a checker function for use with AuthKit's MultiHandler.
228        MultiHandler can be used to catch HTTP 403 Forbidden responses set by
229        an application and call middleware (AuthZResultMiddleware) to handle
230        the access denied message.
231        """
232       
233        def multiHandlerIntercept(environ, status, headers):
234            """AuthKit MultiHandler checker function to intercept
235            unauthorised response status codes from applications to be
236            protected.  This function's definition is embedded into a
237            factory method so that this function has visibility to the
238            PEPFilter object's attributes if required.
239           
240            @type environ: dict
241            @param environ: WSGI environment dictionary
242            @type status: basestring
243            @param status: HTTP response code set by application middleware
244            that this intercept function is to protect
245            @type headers: list
246            @param headers: HTTP response header content"""
247           
248            if status.startswith(PEPFilter.triggerStatus):
249                log.info("PEPFilter: found [%s] status for URI path [%s]: "
250                         "invoking access denied response",
251                         PEPFilter.triggerStatus,
252                         environ['PATH_INFO'])
253                return True
254            else:
255                # No match - it's publicly accessible
256                log.debug("PEPFilter: the return status [%s] for this URI "
257                          "path [%s] didn't match the trigger status [%s]",
258                          status,
259                          environ['PATH_INFO'],
260                          PEPFilter.triggerStatus)
261                return False
262       
263        return multiHandlerIntercept
264       
265    @staticmethod
266    def _filterKeywords(conf, prefix):
267        filteredConf = {}
268        prefixLen = len(prefix)
269        for k, v in conf.items():
270            if k.startswith(prefix):
271                filteredConf[k[prefixLen:]] = conf.pop(k)
272               
273        return filteredConf
274           
275
276from authkit.authenticate.multi import MultiHandler
277from ndg.security.common.utils.classfactory import importClass
278
279class AuthorizationMiddlewareError(Exception):
280    """Base class for AuthorizationMiddleware exceptions"""
281   
282class AuthorizationMiddlewareConfigError(Exception):
283    """AuthorizationMiddleware configuration related exceptions"""
284   
285class AuthorizationMiddleware(NDGSecurityMiddlewareBase):
286    '''Handler to call Policy Enforcement Point middleware to intercept
287    requests and enforce access control decisions.  Add THIS class to any
288    WSGI middleware chain ahead of the application(s) which it is to
289    protect.  Use in conjunction with
290    ndg.security.server.wsgi.authn.AuthenticationMiddleware
291    '''
292   
293    def __init__(self, app, global_conf, prefix='', **app_conf):
294        """Set-up Policy Enforcement Point to enforce access control decisions
295        based on the URI path requested and/or the HTTP response code set by
296        application(s) to be protected.  An AuthKit MultiHandler is setup to
297        handle the latter.  PEPResultHandlerMiddleware handles the output
298        set following an access denied decision"""
299       
300        pepFilter = PEPFilter(app,
301                              global_conf,
302                              prefix=prefix+'pep.filter.',
303                              **app_conf)
304        pepInterceptFunc = pepFilter.multiHandlerInterceptFactory()
305       
306        app = MultiHandler(pepFilter)
307       
308        pepResultHandlerClassName = app_conf.pop(prefix+"pepResultHandler", 
309                                                 None) 
310        if pepResultHandlerClassName is None:
311            pepResultHandler = PEPResultHandlerMiddleware
312        else:
313            pepResultHandler = importClass(pepResultHandlerClassName,
314                                        objectType=PEPResultHandlerMiddleware)
315           
316        app.add_method(PEPFilter.id,
317                       pepResultHandler.filter_app_factory,
318                       global_conf,
319                       prefix=prefix,
320                       **app_conf)
321       
322        app.add_checker(PEPFilter.id, pepInterceptFunc)               
323       
324        super(AuthorizationMiddleware, self).__init__(app,
325                                                      global_conf,
326                                                      prefix=prefix,
327                                                      **app_conf)
328               
Note: See TracBrowser for help on using the repository browser.