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

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

Further improvements to the authorization middleware:

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