source: TI12-security/trunk/python/ndg.security.server/ndg/security/server/wsgi/authn.py @ 5279

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

Major fix to authorisation middleware:

  • Apply request URI checking in WSGI middleware not in MultiHandler? checker function
  • MultiHandler? checker is still used but this performs the function of responding to HTTP 403 Forbidden responses from applications to be protected downstream in the WSGI stack
  • Refactored:
    • PEPFilter is a WSGI app to enforce access control decisions made by the PDP.
    • AuthZResultMiddleware -> PEPResultMiddleware
  • PEPResultMiddleware provides the response if access is denied. This can happen if a URI path matches a target in the policy or if an application downstream sets a 403 response.
Line 
1"""Module containing:
2 * HTTP Basic Authentication Middleware
3 * middleware to enable redirection to OpenID Relying Party for login
4 * logout middleware for deleting AuthKit cookie and redirecting back to
5   referrer
6 
7NERC DataGrid Project
8"""
9__author__ = "P J Kershaw"
10__date__ = "13/01/09"
11__copyright__ = "(C) 2009 Science and Technology Facilities Council"
12__license__ = "BSD - see LICENSE file in top-level directory"
13__contact__ = "Philip.Kershaw@stfc.ac.uk"
14__revision__ = "$Id$"
15import logging
16log = logging.getLogger(__name__)
17from authkit.permissions import UserIn
18
19from ndg.security.server.wsgi.utils.sessionmanagerclient import \
20    WSGISessionManagerClient
21
22class HTTPBasicAuthentication(object):
23    '''Authkit based HTTP Basic Authentication'''
24    def __init__(self):
25        self._userIn = UserIn([])
26       
27    def __call__(self, environ, username, password):
28        """validation function"""
29        try:
30            client = WSGISessionManagerClient(environ=environ,
31                                environKeyName=self.sessionManagerFilterID)
32            res = client.connect(username, passphrase=password)
33
34            if username not in self._userIn.users:
35                self._userIn.users += [username]
36           
37            # TODO: set session
38               
39        except Exception, e:
40            return False
41        else:
42            return True
43
44
45import urllib
46from urlparse import urlparse
47from paste.request import construct_url
48from beaker.middleware import SessionMiddleware
49import authkit.authenticate
50
51from ndg.security.server.wsgi import NDGSecurityMiddlewareBase, \
52    NDGSecurityMiddlewareConfigError
53
54class AuthenticationRedirectMiddleware(NDGSecurityMiddlewareBase):
55    '''Middleware to redirect to another URI if no user is set in the
56    REMOTE_USER key of environ
57   
58    AuthKit.authenticate.middleware must be in place upstream of this
59    middleware.  AuthenticationMiddleware wrapper handles this.'''
60    propertyDefaults = {
61        'redirectURI': None,
62        'sessionKey': 'beaker.session'
63    }
64    propertyDefaults.update(NDGSecurityMiddlewareBase.propertyDefaults)
65
66    return2URIArgName = 'ndg.security.r'
67   
68    _isAuthenticated = lambda self: 'REMOTE_USER' in self.environ
69    isAuthenticated = property(fget=_isAuthenticated,
70                               doc='boolean for is user logged in')
71
72    triggerStatus = '401'
73    id = 'authNMiddleware'
74
75    def __init__(self, app, global_conf, **app_conf):
76        self._redirectURI = None
77        super(AuthenticationRedirectMiddleware, self).__init__(app, 
78                                                               global_conf, 
79                                                               **app_conf)
80       
81    @NDGSecurityMiddlewareBase.initCall
82    def __call__(self, environ, start_response):
83        '''Invoke redirect if user is not authenticated'''
84       
85        if not self.isAuthenticated:
86            # Redirect to OpenID Relying Party URI for user OpenID entry
87            return self._setRedirectResponse()
88       
89        else:
90            # Retrieve OpenID response parameters and set a Forbidden response
91            # to trigger the PEP to check to see if the requested URI is a
92            # secured one
93           
94            def set403Response(status, header, exc_info=None):
95                return start_response(self.getStatusMessage(403),
96                                      header,
97                                      exc_info)
98               
99            return self._app(environ, set403Response)           
100   
101    def _setRedirectURI(self, uri):
102        if not isinstance(uri, basestring):
103            raise TypeError("Redirect URI must be set to string type")   
104         
105        self._redirectURI = uri
106       
107    def _getRedirectURI(self):
108        return self._redirectURI
109   
110    redirectURI = property(fget=_getRedirectURI,
111                       fset=_setRedirectURI,
112                       doc="URI to redirect to if user is not authenticated")
113
114    def _setRedirectResponse(self):
115        """Construct a redirect response adding in a return to address in a
116        URI query argument
117       
118        @rtype: basestring
119        @return: redirect response
120        """
121       
122        return2URI = construct_url(self.environ)
123        quotedReturn2URI = urllib.quote(return2URI, safe='')
124        return2URIQueryArg = urllib.urlencode(
125                    {AuthenticationRedirectMiddleware.return2URIArgName: 
126                     quotedReturn2URI})
127
128        redirectURI = self.redirectURI
129       
130        if '?' in redirectURI:
131            if redirectURI.endswith('&'):
132                redirectURI += return2URIQueryArg
133            else:
134                redirectURI += '&' + return2URIQueryArg
135        else:
136            if redirectURI.endswith('?'):
137                redirectURI += return2URIQueryArg
138            else:
139                redirectURI += '?' + return2URIQueryArg
140               
141        return self._redirect(redirectURI)
142       
143    @classmethod
144    def checker(cls, environ, status, headers):
145        """Set the trigger for calling this middleware.  In this case, it's a
146        HTTP 401 Unauthorized response detected in the middleware chain
147        """
148        log.debug("%s.checker received status %r, "
149                  "headers %r", cls.__name__, status, headers)
150       
151        if status.startswith(cls.triggerStatus):
152            log.debug("%s.checker caught status %s: invoking authentication "
153                      "handler", cls.__name__, cls.triggerStatus)
154            return True
155        else:
156            log.debug("%s.checker skipping status [%s]", cls.__name__, status)
157            return False
158
159
160class SessionHandlerMiddleware(NDGSecurityMiddlewareBase):
161    '''Middleware to redirect back to referrer URI following call to a logout
162    URI as implemented in AuthKit'''
163    prefix = 'sessionHandler.'
164   
165    logoutReturn2URIArgName = 'ndg.security.logout.r'
166    propertyDefaults = {
167        'signoutPath': None,
168        'sessionKey': 'beaker.session.ndg.security'
169    }
170    propertyDefaults.update(NDGSecurityMiddlewareBase.propertyDefaults)
171
172
173    def __init__(self, app, global_conf, prefix='', **app_conf):
174        super(SessionHandlerMiddleware, self).__init__(app, 
175                                                       global_conf,
176                                                       prefix=prefix, 
177                                                       **app_conf)
178       
179    @NDGSecurityMiddlewareBase.initCall
180    def __call__(self, environ, start_response):
181        session = environ[self.sessionKey]
182       
183        if self.signoutPath and self.pathInfo == self.signoutPath:
184            log.debug("SessionHandlerMiddleware: caught signout path [%s]",
185                      self.signoutPath)
186           
187            referer = session.get(self.__class__.logoutReturn2URIArgName)
188            if referer is not None:
189                def _start_response(status, header, exc_info=None):
190                    header.extend([('Location', referer)])
191                    return start_response(self.getStatusMessage(302), 
192                                          header,
193                                          exc_info)
194                   
195            else:
196                log.error('No referer set for redirect following logout')
197                _start_response = start_response
198               
199            # Clear user details from beaker session
200            session.pop('username', None)
201            session.pop('sessionManagerURI', None)
202            session.pop('sessionId', None)
203            session.save()
204        else:
205            log.debug("SessionHandlerMiddleware: checking for REMOTE_* "
206                      "environment variable settings set by OpenID Relying "
207                      "Party signin...")
208           
209            if 'username' not in session and 'REMOTE_USER' in environ:
210                log.debug("SessionHandlerMiddleware: updating session with "
211                          "username=%s", environ['REMOTE_USER'])
212               
213                session['username'] = environ['REMOTE_USER']
214               
215            if environ.get('REMOTE_USER_DATA', ''):
216                log.debug("SessionHandlerMiddleware: found REMOTE_USER_DATA="
217                          "%s, set from OpenID Relying Party signin")
218               
219                # eval is safe here because AuthKit cookie is signed and
220                # AuthKit middleware checks for tampering
221                if 'sessionManagerURI' not in session or \
222                   'sessionId' not in session:
223                    axData = eval(environ['REMOTE_USER_DATA'])
224                    sessionManagerURI = \
225                                    axData['ax']['value.sessionManagerURI.1']
226                    session['sessionManagerURI'] = sessionManagerURI
227
228                    sessionId = axData['ax']['value.sessionId.1']
229                    session['sessionId'] = sessionId
230
231                    log.debug("SessionHandlerMiddleware: updated session "
232                              "with sessionManagerURI=%s and sessionId=%s", 
233                              sessionManagerURI, sessionId)
234                   
235                # Reset cookie removing user data
236                environ['paste.auth_tkt.set_user'](session['username'])
237               
238            # Set a return to address for logout
239            session[self.__class__.logoutReturn2URIArgName] = self.pathInfo
240            session.save()
241                     
242            _start_response = start_response
243           
244        return self._app(environ, _start_response)
245
246
247from authkit.authenticate.multi import MultiHandler
248
249class AuthenticationMiddlewareConfigError(NDGSecurityMiddlewareConfigError):
250    '''Authentication Middleware Configuration error'''
251
252class AuthenticationMiddleware(MultiHandler, NDGSecurityMiddlewareBase):
253    '''Handler to intercept 401 Unauthorized HTTP responses and redirect to an
254    authentication URI.  This class also implements a redirect handler to
255    redirect back to the referrer if logout is invoked.
256    '''
257
258    def __init__(self, app, global_conf, prefix='', **app_conf):
259        '''
260        @type app: callable following WSGI interface
261        @param app: next middleware application in the chain     
262        @type global_conf: dict       
263        @param global_conf: PasteDeploy global configuration dictionary
264        @type prefix: basestring
265        @param prefix: prefix for configuration items
266        @type app_conf: dict       
267        @param app_conf: PasteDeploy application specific configuration
268        dictionary
269        '''
270       
271        # Set logout URI parameter from AuthKit settings if not otherwise set
272        sessionHandlerPrefix = prefix + SessionHandlerMiddleware.prefix
273        signoutPathParamName = sessionHandlerPrefix + 'signoutPath'
274        if signoutPathParamName not in app_conf:
275            try:                   
276                app_conf[signoutPathParamName] = app_conf[
277                                                'authkit.cookie.signoutpath']
278            except KeyError, e:
279                raise AuthenticationMiddlewareConfigError("No %s or %s keys "
280                                                    "set" % 
281                                                    (signoutPathParamName, e))
282       
283        app = SessionHandlerMiddleware(app, 
284                                       global_conf, 
285                                       prefix=sessionHandlerPrefix,
286                                       **app_conf)
287       
288        # Remove session handler middleware specific parameters
289        for k in app_conf.keys():
290            if k.startswith(sessionHandlerPrefix):
291                del app_conf[k]
292       
293        app = authkit.authenticate.middleware(app, app_conf)       
294        app = SessionMiddleware(app, 
295                                environ_key='beaker.session.ndg.security',
296                                **app_conf)
297       
298        MultiHandler.__init__(self, app)
299
300        self.add_method(AuthenticationRedirectMiddleware.id, 
301                        AuthenticationRedirectMiddleware.filter_app_factory, 
302                        global_conf,
303                        prefix=prefix,
304                        **app_conf)
305       
306        self.add_checker(AuthenticationRedirectMiddleware.id, 
307                         AuthenticationRedirectMiddleware.checker)
308
Note: See TracBrowser for help on using the repository browser.