source: TI12-security/trunk/python/ndg.security.server/ndg/security/server/wsgi/openid/relyingparty/__init__.py @ 5355

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

Implemented a caching scheme for Attribute Certificates in the security filter deployed on the application middleware stack:

  • Credentials are already cached in the Session Manager but this resides on a separate WSGI stack so that in order to make a retrieval, a SOAP call is required
  • Caching is implemented on the Security filter by extending the Policy Information Point class (PIP) to make it a WSGI app - PIPMiddleware. This gives it visibility to the current beaker session. When PIPMiddleware makes a request to retrieve an Attribute Certificate it can query the certificate cache held in a CredentialWallet? tied to the beaker session.
  • The CredentialWallet? is pickleable so that beaker session can pickle its content and retrieve when the middleware comes back from being offline.
Line 
1"""NDG Security OpenID Relying Party Middleware
2
3Wrapper to AuthKit OpenID Middleware
4
5NERC DataGrid Project
6"""
7__author__ = "P J Kershaw"
8__date__ = "20/01/2009"
9__copyright__ = "(C) 2009 Science and Technology Facilities Council"
10__license__ = "BSD - see top-level directory for LICENSE file"
11__contact__ = "Philip.Kershaw@stfc.ac.uk"
12__revision__ = "$Id$"
13import logging
14log = logging.getLogger(__name__)
15
16import httplib # to get official status code messages
17import urllib # decode quoted URI in query arg
18from urlparse import urlsplit, urlunsplit
19
20from paste.request import parse_querystring, parse_formvars
21import authkit.authenticate
22from beaker.middleware import SessionMiddleware
23
24from ndg.security.server.wsgi import NDGSecurityMiddlewareBase
25from ndg.security.server.wsgi.authn import AuthNRedirectMiddleware
26from ndg.security.common.utils.classfactory import instantiateClass
27
28class OpenIDRelyingPartyMiddlewareError(Exception):
29    """OpenID Relying Party WSGI Middleware Error"""
30
31class OpenIDRelyingPartyConfigError(OpenIDRelyingPartyMiddlewareError):
32    """OpenID Relying Party Configuration Error"""
33 
34class OpenIDRelyingPartyMiddleware(NDGSecurityMiddlewareBase):
35    '''OpenID Relying Party middleware which wraps the AuthKit implementation.
36    This middleware is to be hosted in it's own security middleware stack.
37    WSGI middleware applications to be protected can be hosted in a separate
38    stack.  The AuthNRedirectMiddleware filter can respond to a HTTP
39    401 response from this stack and redirect to this middleware to initiate
40    OpenID based sign in.  AuthNRedirectMiddleware passes a query
41    argument in its request containing the URI return address for this
42    middleware to return to following OpenID sign in.
43    '''
44    propertyDefaults = {
45        'signinInterfaceMiddlewareClass': None,
46        'baseURL': '',
47        'sessionKey': 'beaker.session.ndg.security'
48    }
49    propertyDefaults.update(NDGSecurityMiddlewareBase.propertyDefaults)
50   
51    def __init__(self, app, global_conf, prefix='openid.relyingparty.', 
52                 **app_conf):
53        """Add AuthKit and Beaker middleware dependencies to WSGI stack
54       
55        @type app: callable following WSGI interface signature
56        @param app: next middleware application in the chain     
57        @type global_conf: dict       
58        @param global_conf: PasteDeploy application global configuration -
59        must follow format of propertyDefaults class variable
60        @type prefix: basestring
61        @param prefix: prefix for OpenID Relying Party configuration items
62        @type app_conf: dict
63        @param app_conf: application specific configuration - must follow
64        format of propertyDefaults class variable"""   
65
66           
67        # Check for sign in template settings
68        if prefix+'signinInterfaceMiddlewareClass' in app_conf:
69            if 'authkit.openid.template.obj' in app_conf or \
70               'authkit.openid.template.string' in app_conf or \
71               'authkit.openid.template.file' in app_conf:
72                log.warning("OpenID Relying Party "
73                            "'signinInterfaceMiddlewareClass' "
74                            "setting overrides 'authkit.openid.template.*' "
75                            "AuthKit settings")
76               
77            moduleName, className = \
78                app_conf[prefix+'signinInterfaceMiddlewareClass'].rsplit('.',1)
79           
80            signinInterfacePrefix = prefix+'signinInterface.'
81            classProperties = {'prefix': signinInterfacePrefix}
82            classProperties.update(app_conf)
83            app = instantiateClass(moduleName, 
84                                   className, 
85                                   objectType=SigninInterface, 
86                                   classArgs=(app, global_conf),
87                                   classProperties=classProperties)           
88           
89            # Delete sign in interface middleware settings
90            for conf in app_conf, global_conf or {}:
91                for k in conf.keys():
92                    if k.startswith(signinInterfacePrefix):
93                        del conf[k]
94       
95            app_conf['authkit.openid.template.string'] = app.makeTemplate()
96               
97        self.signoutPath = app_conf.get('authkit.cookie.signoutpath')
98
99        authKitApp = authkit.authenticate.middleware(app, app_conf)
100        _app = authKitApp
101        while True:
102            if isinstance(_app,authkit.authenticate.open_id.AuthOpenIDHandler):
103                self._authKitVerifyPath = _app.path_verify
104                self._authKitProcessPath = _app.path_process
105                break
106           
107            elif hasattr(_app, 'app'):
108                _app = _app.app
109            else:
110                break
111         
112        if not hasattr(self, '_authKitVerifyPath'):
113            raise OpenIDRelyingPartyConfigError("Error locating the AuthKit "
114                                                "AuthOpenIDHandler in the "
115                                                "WSGI stack")
116       
117        # Check for return to argument in query key value pairs
118        self._return2URIKey = AuthNRedirectMiddleware.return2URIArgName + '='
119   
120        super(OpenIDRelyingPartyMiddleware, self).__init__(authKitApp, 
121                                                           global_conf, 
122                                                           prefix=prefix, 
123                                                           **app_conf)
124
125       
126    @NDGSecurityMiddlewareBase.initCall     
127    def __call__(self, environ, start_response):
128        '''
129        - Alter start_response to override the status code and force to 401.
130        This will enable non-browser based client code to bypass the OpenID
131        interface
132        - Manage AuthKit verify and process actions setting the referrer URI
133        to manage redirects
134       
135        @type environ: dict
136        @param environ: WSGI environment variables dictionary
137        @type start_response: function
138        @param start_response: standard WSGI start response function
139        @rtype: iterable
140        @return: response
141        '''
142        session = environ[self.sessionKey]
143       
144        # Check for return to address in URI query args set by
145        # AuthNRedirectMiddleware in application code stack
146        if environ['REQUEST_METHOD'] == "GET":
147            params = dict(parse_querystring(environ))
148        else:
149            params = {}
150       
151        quotedReferrer=params.get(AuthNRedirectMiddleware.return2URIArgName,'')
152        referrer = urllib.unquote(quotedReferrer)
153        referrerPathInfo = urlsplit(referrer)[2]
154
155        if referrer and \
156           not referrerPathInfo.endswith(self._authKitVerifyPath) and \
157           not referrerPathInfo.endswith(self._authKitProcessPath):
158            # Subvert authkit.authenticate.open_id.AuthOpenIDHandler.process
159            # reassigning it's session 'referer' key to the URI specified in
160            # the referrer query argument set in the request URI
161            session['referer'] = referrer
162            session.save()
163           
164        if self._return2URIKey in environ.get('HTTP_REFERER', ''):
165            # Remove return to arg to avoid interfering with AuthKit OpenID
166            # processing
167            splitURI = urlsplit(environ['HTTP_REFERER'])
168            query = splitURI[3]
169           
170            filteredQuery = '&'.join([arg for arg in query.split('&')
171                                if not arg.startswith(self._return2URIKey)])
172           
173            environ['HTTP_REFERER'] = urlunsplit(splitURI[:3] + \
174                                                 (filteredQuery,) + \
175                                                 splitURI[4:])
176
177        if self.signoutPath is not None and self.pathInfo == self.signoutPath:
178            # Redirect to referrer ...
179            referrer = session.get(
180                    'ndg.security.server.wsgi.openid.relyingparty.referer')
181            if referrer is not None:
182                def setRedirectResponse(status, header, exc_info=None):
183                    header.extend([('Location', referrer)])
184                    return start_response(self.getStatusMessage(302), 
185                                          header,
186                                          exc_info)
187                   
188                return self._app(environ, setRedirectResponse)
189            else:
190                log.debug('No referrer set for redirect following logout')
191               
192        # Set a return to address following logout. 
193        # TODO: This code will need to be refactored if this middleware is
194        # deployed externally via a proxy - HTTP_REFERER will be the internal
195        # URI instead of the one exposed outside
196        if 'HTTP_REFERER' in environ:
197            session['ndg.security.server.wsgi.openid.relyingparty.referer'] = \
198                environ['HTTP_REFERER']
199            session.save()
200       
201        # See _start_response doc for an explanation...
202        if environ['PATH_INFO'] == self._authKitVerifyPath: 
203            def _start_response(status, header, exc_info=None):
204                '''Make OpenID Relying Party OpenID prompt page return a 401
205                status to signal to non-browser based clients that
206                authentication is required.  Requests are filtered on content
207                type so that static content such as graphics and style sheets
208                associated with the page are let through unaltered
209               
210                @type status: str
211                @param status: HTTP status code and status message
212                @type header: list
213                @param header: list of field, value tuple HTTP header content
214                @type exc_info: Exception
215                @param exc_info: exception info
216                '''
217                _status = status
218                for name, val in header:
219                    if name.lower() == 'content-type' and \
220                       val.startswith('text/html'):
221                        _status = self.getStatusMessage(401)
222                        break
223                   
224                return start_response(_status, header, exc_info)
225        else:
226            _start_response = start_response
227
228        return self._app(environ, _start_response)
229
230   
231class SigninInterfaceError(Exception):
232    """Base class for SigninInterface exceptions
233   
234    A standard message is raised set by the msg class variable but the actual
235    exception details are logged to the error log.  The use of a standard
236    message enables callers to use its content for user error messages.
237   
238    @type msg: basestring
239    @cvar msg: standard message to be raised for this exception"""
240    userMsg = ("An internal error occurred with the page layout,  Please "
241               "contact your system administrator")
242    errorMsg = "SigninInterface error"
243   
244    def __init__(self, *arg, **kw):
245        if len(arg) > 0:
246            msg = arg[0]
247        else:
248            msg = self.__class__.errorMsg
249           
250        log.error(msg)
251        Exception.__init__(self, msg, **kw)
252       
253class SigninInterfaceInitError(SigninInterfaceError):
254    """Error with initialisation of SigninInterface.  Raise from __init__"""
255    errorMsg = "SigninInterface initialisation error"
256   
257class SigninInterfaceConfigError(SigninInterfaceError):
258    """Error with configuration settings.  Raise from __init__"""
259    errorMsg = "SigninInterface configuration error"   
260
261class SigninInterface(NDGSecurityMiddlewareBase):
262    """Base class for sign in rendering.  This is implemented as WSGI
263    middleware to enable additional middleware to be added into the call
264    stack e.g. StaticFileParser to enable rendering of graphics and other
265    static content in the Sign In page"""
266   
267    def getTemplateFunc(self):
268        """Return template function for AuthKit to render OpenID Relying
269        Party Sign in page"""
270        raise NotImplementedError()
271   
272    def __call__(self, environ, start_response):
273        return self._app(self, environ, start_response)
274
Note: See TracBrowser for help on using the repository browser.