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

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@5042
Revision 5042, 10.8 KB checked in by pjkersha, 11 years ago (diff)

Moved Combined Services Test into integration test package.

RevLine 
[4855]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
[4905]16import httplib # to get official status code messages
[5037]17import urllib # decode quoted URI in query arg
[5041]18from urlparse import urlsplit, urlunsplit
[4905]19
[5037]20from paste.request import parse_querystring, parse_formvars
[4863]21import authkit.authenticate
22import beaker.middleware
23
[4855]24from ndg.security.server.wsgi import NDGSecurityMiddlewareBase
[5041]25from ndg.security.server.wsgi.authn import AuthNRedirectMiddleware
[4863]26from ndg.security.common.utils.classfactory import instantiateClass
[4855]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):
[4863]35    '''Implementation of OpenID Relying Party based on AuthKit'''
36    propertyDefaults = {
[4907]37        'signinInterfaceMiddlewareClass': None,
38        'baseURL': '',
[4909]39        'sessionKey': 'beaker.session'
[4863]40    }
41    propertyDefaults.update(NDGSecurityMiddlewareBase.propertyDefaults)
42   
43    def __init__(self, app, global_conf, prefix='openid.relyingparty.', 
44                 **app_conf):
45        """Add AuthKit and Beaker middleware dependencies to WSGI stack
46       
47        @type app: callable following WSGI interface signature
48        @param app: next middleware application in the chain     
49        @type global_conf: dict       
50        @param global_conf: PasteDeploy application global configuration -
51        must follow format of propertyDefaults class variable
52        @type prefix: basestring
53        @param prefix: prefix for OpenID Relying Party configuration items
54        @type app_conf: dict
55        @param app_conf: application specific configuration - must follow
56        format of propertyDefaults class variable"""   
[4905]57
58           
[4863]59        # Check for sign in template settings
60        if prefix+'signinInterfaceMiddlewareClass' in app_conf:
[4907]61            if 'authkit.openid.template.obj' in app_conf or \
62               'authkit.openid.template.string' in app_conf or \
63               'authkit.openid.template.file' in app_conf:
[4863]64                log.warning("OpenID Relying Party "
65                            "'signinInterfaceMiddlewareClass' "
[4907]66                            "setting overrides 'authkit.openid.template.*' "
67                            "AuthKit settings")
[4863]68               
69            moduleName, className = \
70                app_conf[prefix+'signinInterfaceMiddlewareClass'].rsplit('.',1)
71           
72            signinInterfacePrefix = prefix+'signinInterface.'
73            classProperties = {'prefix': signinInterfacePrefix}
74            classProperties.update(app_conf)
[4907]75            app = instantiateClass(moduleName, 
76                                   className, 
[4863]77                                   objectType=SigninInterface, 
78                                   classArgs=(app, global_conf),
79                                   classProperties=classProperties)           
80           
81            # Delete sign in interface middleware settings
82            for conf in app_conf, global_conf or {}:
83                for k in conf.keys():
84                    if k.startswith(signinInterfacePrefix):
85                        del conf[k]
86       
[4907]87            app_conf['authkit.openid.template.string'] = app.makeTemplate()
88               
89        self.signoutPath = app_conf.get('authkit.cookie.signoutpath')
[4863]90
[4907]91        authKitApp = authkit.authenticate.middleware(app, app_conf)
[5037]92        _app = authKitApp
93        while True:
94            if isinstance(_app,authkit.authenticate.open_id.AuthOpenIDHandler):
95                self._authKitVerifyPath = _app.path_verify
96                self._authKitProcessPath = _app.path_process
97                break
98           
99            elif hasattr(_app, 'app'):
100                _app = _app.app
101            else:
102                break
103         
104        if not hasattr(self, '_authKitVerifyPath'):
105            raise OpenIDRelyingPartyConfigError("Error locating the AuthKit "
106                                                "AuthOpenIDHandler in the "
107                                                "WSGI stack")
[5041]108       
109        # Check for return to argument in query key value pairs
110        self._return2URIKey = AuthNRedirectMiddleware.return2URIArgName + '='
111   
[4907]112        super(OpenIDRelyingPartyMiddleware, self).__init__(authKitApp, 
[4863]113                                                           global_conf, 
114                                                           prefix=prefix, 
115                                                           **app_conf)
[4907]116
117       
118    @NDGSecurityMiddlewareBase.initCall     
[4863]119    def __call__(self, environ, start_response):
[4905]120        '''Alter start_response to override the status code and force to 401.
[5037]121        This will enable non-browser based client code to bypass the OpenID
122        interface
[4907]123       
124        @type environ: dict
125        @param environ: WSGI environment variables dictionary
126        @type start_response: builtin_function_or_method
127        @param start_response: standard WSGI start response function
[4905]128        '''
[4907]129        session = environ[self.sessionKey]
[5037]130       
131        # Check for return to address in URI query args
132        if environ['REQUEST_METHOD'] == "GET":
133            params = dict(parse_querystring(environ))
134        else:
135            params = dict(parse_formvars(environ))
136       
[5041]137        quotedReferrer=params.get(AuthNRedirectMiddleware.return2URIArgName,'')
138        referrer = urllib.unquote(quotedReferrer)
139        referrerPathInfo = urlsplit(referrer)[2]
140        if referrer and \
141           not referrerPathInfo.endswith(self._authKitVerifyPath) and \
142           not referrerPathInfo.endswith(self._authKitProcessPath):
[5037]143            # Set-up for authkit.authenticate.open_id.AuthOpenIDHandler.process
[5041]144            session['referer'] = referrer
[5037]145            session.save()
[5041]146           
147        if self._return2URIKey in environ.get('HTTP_REFERER', ''):
148            # Remove return to arg to avoid interfering with AuthKit OpenID
149            # processing
150            splitURI = urlsplit(environ['HTTP_REFERER'])
151            query = splitURI[3]
152           
153            filteredQuery = '&'.join([arg for arg in query.split('&')
154                                if not arg.startswith(self._return2URIKey)])
155           
156            environ['HTTP_REFERER'] = urlunsplit(splitURI[:3] + \
157                                                 (filteredQuery,) + \
158                                                 splitURI[4:])
159
[4907]160        if self.signoutPath is not None and self.pathInfo == self.signoutPath:
161            # TODO: Redirect to referrer ...
[5041]162            referrer = session.get(
[4907]163                        'ndg.security.server.wsgi.openid.relyingparty.referer')
[5041]164            if referrer is not None:
[4907]165                def setRedirectResponse(status, header, exc_info=None):
[5041]166                    header.extend([('Location', referrer)])
167                    return start_response(self.getStatusMessage(302), 
[4907]168                                          header,
169                                          exc_info)
170                   
171                return self._app(environ, setRedirectResponse)
172            else:
[5041]173                log.debug('No referrer set for redirect following logout')
[4907]174               
[4909]175        # Set a return to address following logout. 
176        # TODO: This code will need to be refactored if this middleware is
177        # deployed externally via a proxy - HTTP_REFERER will be the internal
178        # URI instead of the one exposed outside
[4907]179        if 'HTTP_REFERER' in environ:
180            session['ndg.security.server.wsgi.openid.relyingparty.referer'] = \
181                environ['HTTP_REFERER']
182            session.save()
[4905]183           
[4907]184        def set401UnauthorizedReponse(status, header, exc_info=None):
185            '''Make OpenID Relying Party OpenID prompt page return a 401
186            status to signal to non-browser based clients that authentication
187            is required.  Requests are filtered on content type so that
188            static content such as graphics and style sheets associated with
189            the page are let through unaltered
190           
191            @type status: str
192            @param status: HTTP status code and status message
193            @type header: list
194            @param header: list of field, value tuple HTTP header content
195            @type exc_info: Exception
196            @param exc_info: exception info
197            '''
198            _status = status
199            for name, val in header:
200                if name.lower() == 'content-type' and \
201                   val.startswith('text/html'):
[5042]202                    _status = self.getStatusMessage(401)
[4907]203                    break
204               
205            return start_response(_status, header, exc_info)
[4863]206
[4907]207        return self._app(environ, set401UnauthorizedReponse)
208
209   
[4863]210class SigninInterfaceError(Exception):
211    """Base class for SigninInterface exceptions
212   
213    A standard message is raised set by the msg class variable but the actual
214    exception details are logged to the error log.  The use of a standard
215    message enables callers to use its content for user error messages.
216   
217    @type msg: basestring
218    @cvar msg: standard message to be raised for this exception"""
219    userMsg = ("An internal error occurred with the page layout,  Please "
220               "contact your system administrator")
221    errorMsg = "SigninInterface error"
222   
223    def __init__(self, *arg, **kw):
224        if len(arg) > 0:
225            msg = arg[0]
226        else:
227            msg = self.__class__.errorMsg
228           
229        log.error(msg)
230        Exception.__init__(self, msg, **kw)
231       
232class SigninInterfaceInitError(SigninInterfaceError):
233    """Error with initialisation of SigninInterface.  Raise from __init__"""
234    errorMsg = "SigninInterface initialisation error"
235   
236class SigninInterfaceConfigError(SigninInterfaceError):
237    """Error with configuration settings.  Raise from __init__"""
238    errorMsg = "SigninInterface configuration error"   
239
240class SigninInterface(NDGSecurityMiddlewareBase):
241    """Base class for sign in rendering.  This is implemented as WSGI
242    middleware to enable additional middleware to be added into the call
243    stack e.g. StaticFileParser to enable rendering of graphics and other
244    static content in the Sign In page"""
245   
246    def getTemplateFunc(self):
247        """Return template function for AuthKit to render OpenID Relying
248        Party Sign in page"""
249        raise NotImplementedError()
250   
251    def __call__(self, environ, start_response):
[5037]252        return self._app(self, environ, start_response)
Note: See TracBrowser for help on using the repository browser.