source: TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/wsgi/session.py @ 6605

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

Setting up tests for the SAML Authorisation Service.

Line 
1"""Session handling middleware module
2
3Refactored authn module moving session specific code to here
4 
5NERC DataGrid Project
6"""
7__author__ = "P J Kershaw"
8__date__ = "05/01/10"
9__copyright__ = "(C) 2010 Science and Technology Facilities Council"
10__license__ = "BSD - see LICENSE file in top-level directory"
11__contact__ = "Philip.Kershaw@stfc.ac.uk"
12__revision__ = "$Id: $"
13import logging
14log = logging.getLogger(__name__)
15
16import urllib
17from paste.request import parse_querystring
18
19from ndg.security.server.wsgi import (NDGSecurityMiddlewareBase,
20                                      NDGSecurityMiddlewareError)
21
22
23class SessionMiddlewareBase(NDGSecurityMiddlewareBase):
24    """Base class for Authentication redirect middleware and Session Handler
25    middleware
26   
27    @type propertyDefaults: dict
28    @cvar propertyDefaults: valid configuration property keywords
29    """   
30    propertyDefaults = {
31        'sessionKey': 'beaker.session.ndg.security'
32    }
33
34    # Key names for PEP context information
35    PEPCTX_SESSION_KEYNAME = 'pepCtx'
36    PEPCTX_REQUEST_SESSION_KEYNAME = 'request'
37    PEPCTX_RESPONSE_SESSION_KEYNAME = 'response'
38    PEPCTX_TIMESTAMP_SESSION_KEYNAME = 'timestamp'
39   
40    _isAuthenticated = lambda self: \
41        SessionMiddlewareBase.USERNAME_SESSION_KEYNAME in \
42        self.environ.get(self.sessionKey, ())
43       
44    isAuthenticated = property(fget=_isAuthenticated,
45                               doc='boolean to indicate is user logged in')
46       
47
48class SessionHandlerMiddlewareError(NDGSecurityMiddlewareError):
49    """Base exception for SessionHandlerMiddleware"""
50           
51           
52class SessionHandlerMiddlewareConfigError(SessionHandlerMiddlewareError):
53    """Configuration errors from SessionHandlerMiddleware"""
54   
55   
56class OpenIdAXConfigError(SessionHandlerMiddlewareError):
57    """Error parsing OpenID Ax (Attribute Exchange) parameters"""
58   
59   
60class SessionHandlerMiddleware(SessionMiddlewareBase):
61    '''Middleware to:
62    - establish user session details following redirect from OpenID Relying
63    Party sign-in or SSL Client authentication
64    - end session redirecting back to referrer URI following call to a logout
65    URI as implemented in AuthKit
66    '''
67    AX_SESSION_KEYNAME = 'openid.ax'
68    SM_URI_SESSION_KEYNAME = 'sessionManagerURI'
69    ID_SESSION_KEYNAME = 'sessionId'
70    PEP_CTX_SESSION_KEYNAME = 'pepCtx'
71    CREDENTIAL_WALLET_SESSION_KEYNAME = 'credentialWallet'
72   
73    SESSION_KEYNAMES = (
74        SessionMiddlewareBase.USERNAME_SESSION_KEYNAME, 
75        SM_URI_SESSION_KEYNAME, 
76        ID_SESSION_KEYNAME, 
77        PEP_CTX_SESSION_KEYNAME, 
78        CREDENTIAL_WALLET_SESSION_KEYNAME
79    )
80   
81    AX_KEYNAME = 'ax'
82    SM_URI_AX_KEYNAME = 'value.sessionManagerURI.1'
83    SESSION_ID_AX_KEYNAME = 'value.sessionId.1'
84   
85    AUTHKIT_COOKIE_SIGNOUT_PARAMNAME = 'authkit.cookie.signoutpath'
86    SIGNOUT_PATH_PARAMNAME = 'signoutPath'
87    SESSION_KEY_PARAMNAME = 'sessionKey'
88    propertyDefaults = {
89        SIGNOUT_PATH_PARAMNAME: None,
90        SESSION_KEY_PARAMNAME: 'beaker.session.ndg.security'
91    }
92   
93    AUTH_TKT_SET_USER_ENVIRON_KEYNAME = 'paste.auth_tkt.set_user'
94   
95    LOGOUT_RETURN2URI_ARGNAME = 'ndg.security.logout.r'
96   
97    PARAM_PREFIX = 'sessionHandler.'
98   
99    def __init__(self, app, global_conf, prefix=PARAM_PREFIX, **app_conf):
100        '''
101        @type app: callable following WSGI interface
102        @param app: next middleware application in the chain     
103        @type global_conf: dict       
104        @param global_conf: PasteDeploy global configuration dictionary
105        @type prefix: basestring
106        @param prefix: prefix for configuration items
107        @type app_conf: dict       
108        @param app_conf: PasteDeploy application specific configuration
109        dictionary
110        '''
111        signoutPathParamName = prefix + \
112                                SessionHandlerMiddleware.SIGNOUT_PATH_PARAMNAME
113       
114        if signoutPathParamName not in app_conf:
115            authKitSignOutPath = app_conf.get(
116                    SessionHandlerMiddleware.AUTHKIT_COOKIE_SIGNOUT_PARAMNAME)
117           
118            if authKitSignOutPath:
119                app_conf[signoutPathParamName] = authKitSignOutPath
120               
121                log.info('Set signoutPath=%s from "%s" setting', 
122                     authKitSignOutPath,
123                     SessionHandlerMiddleware.AUTHKIT_COOKIE_SIGNOUT_PARAMNAME)
124            else:
125                raise SessionHandlerMiddlewareConfigError(
126                                        '"signoutPath" parameter is not set')
127           
128        super(SessionHandlerMiddleware, self).__init__(app,
129                                                       global_conf,
130                                                       prefix=prefix, 
131                                                       **app_conf)
132       
133    @NDGSecurityMiddlewareBase.initCall
134    def __call__(self, environ, start_response):
135        """Manage setting of session from AuthKit following OpenID Relying
136        Party sign in and manage logout
137       
138        @type environ: dict
139        @param environ: WSGI environment variables dictionary
140        @type start_response: function
141        @param start_response: standard WSGI start response function
142        """
143        log.debug("SessionHandlerMiddleware.__call__ ...")
144       
145        session = environ.get(self.sessionKey)
146        if session is None:
147            raise SessionHandlerMiddlewareConfigError(
148                   'SessionHandlerMiddleware.__call__: No beaker session key '
149                   '"%s" found in environ' % self.sessionKey)
150       
151        if self.signoutPath and self.pathInfo == self.signoutPath:
152            log.debug("SessionHandlerMiddleware.__call__: caught sign out "
153                      "path [%s]", self.signoutPath)
154           
155            _start_response = self._doLogout(environ, start_response, session)
156        else:
157            log.debug("SessionHandlerMiddleware.__call__: checking for "
158                      "REMOTE_* environment variable settings set by OpenID "
159                      "Relying Party signin...")
160            self._setSession(environ, session)
161
162            _start_response = start_response
163           
164        return self._app(environ, _start_response)
165   
166    def _doLogout(self, environ, start_response, session):
167        """Execute logout action,
168         - clear the beaker session
169         - set the referrer URI to redirect back to by setting a custom
170        start_response function which modifies the HTTP header setting the
171        location field for a redirect
172       
173        @param environ: environment dictionary
174        @type environ: dict like object
175        @type start_response: function
176        @param start_response: standard WSGI start response function
177        @param session: beaker session
178        @type session: beaker.session.SessionObject
179        """
180           
181        # Clear user details from beaker session
182        for keyName in self.__class__.SESSION_KEYNAMES:
183            session.pop(keyName, None)
184        session.save()
185       
186        if self.__class__.LOGOUT_RETURN2URI_ARGNAME in environ['QUERY_STRING']:
187            params = dict(parse_querystring(environ))
188       
189            # Store the return URI query argument in a beaker session
190            quotedReferrer = params.get(
191                                self.__class__.LOGOUT_RETURN2URI_ARGNAME, '')
192            referrer = urllib.unquote(quotedReferrer)
193        else:
194            referrer = environ.get('HTTP_REFERER')
195       
196        if referrer is not None:
197            def _start_response(status, header, exc_info=None):
198                """Alter the header to send a redirect to the logout
199                referrer address"""
200                filteredHeader = [(field, val) for field, val in header
201                                  if field.lower() != 'location']       
202                filteredHeader.extend([('Location', referrer)])
203                return start_response(self.getStatusMessage(302), 
204                                      filteredHeader,
205                                      exc_info)
206               
207            return _start_response
208        else:
209            log.error('No referrer set for redirect following logout')
210            return start_response
211       
212    def _setSession(self, environ, session):
213        """Check for REMOTE_USER and REMOTE_USER_DATA set by authentication
214        handlers and set a new session from them if present
215       
216        @type environ: dict like object
217        @param environ: WSGI environment variables dictionary
218        @param session: beaker session
219        @type session: beaker.session.SessionObject
220        """
221       
222        # Set user id
223        if (SessionHandlerMiddleware.USERNAME_SESSION_KEYNAME not in session
224            and SessionHandlerMiddleware.USERNAME_ENVIRON_KEYNAME in environ):
225           
226            log.debug("SessionHandlerMiddleware.__call__: updating session "
227                      "username=%s", environ[
228                        SessionHandlerMiddleware.USERNAME_ENVIRON_KEYNAME])
229           
230            session[SessionHandlerMiddleware.USERNAME_SESSION_KEYNAME
231                    ] = environ[
232                        SessionHandlerMiddleware.USERNAME_ENVIRON_KEYNAME]
233            session.save()
234           
235        # Check for auxiliary user data
236        remoteUserData = environ.get(
237                        SessionHandlerMiddleware.USERDATA_ENVIRON_KEYNAME, '')   
238        if remoteUserData:
239            log.debug("SessionHandlerMiddleware.__call__: found "
240                      "REMOTE_USER_DATA=%s, set from OpenID Relying Party "
241                      "signin", 
242                      environ[
243                          SessionHandlerMiddleware.USERDATA_ENVIRON_KEYNAME
244                      ])
245           
246            if (SessionHandlerMiddleware.SM_URI_SESSION_KEYNAME not in 
247                session or 
248                SessionHandlerMiddleware.ID_SESSION_KEYNAME not in session):
249               
250                # eval is safe here because AuthKit cookie is signed and
251                # AuthKit middleware checks for tampering           
252                axData = eval(remoteUserData)
253                if (isinstance(axData, dict) and 
254                    SessionHandlerMiddleware.AX_KEYNAME in axData):
255                   
256                    ax = axData[SessionHandlerMiddleware.AX_KEYNAME]
257                   
258                    # Save attributes keyed by attribute name
259                    session[SessionHandlerMiddleware.AX_SESSION_KEYNAME
260                            ] = SessionHandlerMiddleware._parseOpenIdAX(ax)
261                   
262                    log.debug("SessionHandlerMiddleware.__call__: updated "
263                              "session with OpenID AX values: %r",
264                              session[
265                                SessionHandlerMiddleware.AX_SESSION_KEYNAME
266                              ])
267                       
268                    # Save Session Manager specific attributes
269                    sessionManagerURI = ax.get(
270                            SessionHandlerMiddleware.SM_URI_AX_KEYNAME)
271                       
272                    session[SessionHandlerMiddleware.SM_URI_SESSION_KEYNAME
273                            ] = sessionManagerURI
274
275                    sessionId = ax.get(
276                            SessionHandlerMiddleware.SESSION_ID_AX_KEYNAME)
277                    session[SessionHandlerMiddleware.ID_SESSION_KEYNAME
278                            ] = sessionId
279                           
280                    session.save()
281                   
282                    log.debug("SessionHandlerMiddleware.__call__: updated "
283                              "session "
284                              "with sessionManagerURI=%s and "
285                              "sessionId=%s", 
286                              sessionManagerURI, 
287                              sessionId)
288               
289            # Reset cookie removing user data by accessing the Auth ticket
290            # function available from environ
291            setUser = environ[
292                    SessionHandlerMiddleware.AUTH_TKT_SET_USER_ENVIRON_KEYNAME]
293            setUser(session[SessionHandlerMiddleware.USERNAME_SESSION_KEYNAME])
294           
295            # Also reset the environment variable to prevent AuthKit from
296            # restoring the AX values in the cookie.
297            environ[SessionHandlerMiddleware.USERDATA_ENVIRON_KEYNAME] = ''     
298        else:
299            log.debug("SessionHandlerMiddleware.__call__: REMOTE_USER_DATA "
300                      "is not set")
301                   
302    @staticmethod                   
303    def _parseOpenIdAX(ax):
304        """Return a dictionary of attribute exchange attributes parsed from the
305        OpenID Provider response set in the REMOTE_USER_DATA AuthKit environ
306        key
307       
308        @param ax: dictionary of AX parameters - format of keys is e.g.
309        count.paramName, value.paramName.<n>, type.paramName
310        @type ax: dict
311        @return: dictionary of parameters keyed by parameter with values for
312        each parameter a tuple of count.paramName values
313        @rtype: dict
314        """
315       
316        # Copy Attributes into session
317        outputKeys = [k.replace('type.', '') for k in ax.keys()
318                      if k.startswith('type.')]
319       
320        output = {}
321        for outputKey in outputKeys:
322            axCountKeyName = 'count.' + outputKey
323            axCount = int(ax[axCountKeyName])
324           
325            axValueKeyPrefix = 'value.%s.' % outputKey
326            output[outputKey] = tuple([v for k, v in ax.items() 
327                                       if k.startswith(axValueKeyPrefix)])
328           
329            nVals = len(output[outputKey])
330            if nVals != axCount:
331                raise OpenIdAXConfigError('Got %d parameters for AX attribute '
332                                          '"%s"; but "%s" AX key is set to %d'
333                                          % (nVals,
334                                             axCountKeyName,
335                                             axCountKeyName,
336                                             axCount))
337                                             
338        return output
Note: See TracBrowser for help on using the repository browser.