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

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

Fix to ndg.security.server.wsgi.authn.AuthenticationRedirectMiddleware?: when user is authenticated return next app in stack rather raising 403 response. 403 is not needed here in order to activate the authorisation middleware. The latter can trigger itself.

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