source: TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/wsgi/openid/provider/__init__.py @ 6246

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/wsgi/openid/provider/__init__.py@6246
Revision 6246, 67.0 KB checked in by pjkersha, 10 years ago (diff)

User selection of OpenID Provider AX attributes completed: OpenIDProviderMiddleware.do_allow now correctly reviews and updates the response returned to the RP checking user selection of AX parameters POST'ed from the decide page.

Line 
1"""NDG Security OpenID Provider Middleware
2
3Compliments AuthKit OpenID Middleware used for OpenID *Relying Party*
4
5NERC DataGrid Project
6"""
7__author__ = "P J Kershaw"
8__date__ = "01/08/08"
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 httplib
14import sys
15import os
16import traceback
17import logging
18log = logging.getLogger(__name__)
19_debugLevel = log.getEffectiveLevel() <= logging.DEBUG
20
21import re
22from string import Template
23
24import paste.request
25from paste.util.import_string import eval_import
26from openid.extensions import sreg, ax
27from openid.server import server
28from openid.store.filestore import FileOpenIDStore
29from openid.consumer import discover
30
31from ndg.security.common.utils.classfactory import instantiateClass
32from ndg.security.server.wsgi import NDGSecurityMiddlewareBase 
33
34
35class IdentityMapping(object):
36    """Utility class to map between user identifiers and OpenID URIs
37    The user identifier is the user unique component of an OpenID URI
38    """
39    USER_IDENTIFIER_NAME = 'userIdentifier'
40   
41    # Template substitution may or may not contain braces
42    USER_IDENTIFIER_RE = re.compile('\$\{?%s\}?' % USER_IDENTIFIER_NAME)
43   
44    @classmethod
45    def userIdentifier2IdentityURI(cls, identityUriTmpl, userIdentifier):
46        """Convert an OpenID user identifier into an identity URI given a
47        template e.g.
48       
49        https://${userIdentifier}.openid.ac.uk + pjk => https://pjk.openid.ac.uk
50        """
51        mapping = {cls.USER_IDENTIFIER_NAME: userIdentifier}
52        tmpl = Template(identityUriTmpl)
53        return tmpl.substitute(mapping)
54       
55    @classmethod
56    def identityUri2UserIdentifier(cls, identityUriTmpl, identityUri):
57        """Parse an OpenID user identifier from an identity URI given a
58        template e.g.
59       
60        https://pjk.openid.ac.uk + https://${userIdentifier}.openid.ac.uk => pjk
61        """
62        # Subtract start and end URI snippets from the template
63        try:
64            uriPrefix, uriSuffix = cls.USER_IDENTIFIER_RE.split(identityUriTmpl)
65        except ValueError:
66            raise OpenIDProviderConfigError('Error parsing identity URI %r '
67                                            'using template %r' %
68                                            (identityUri, identityUriTmpl))
69           
70        if not identityUri.startswith(uriPrefix): 
71            raise OpenIDProviderConfigError('Identity URI %r doesn\'t match '
72                                            'the template prefix %r' %
73                                            (identityUri, uriPrefix))
74           
75        suffixIndex = identityUri.rfind(uriSuffix)   
76        if suffixIndex == -1: 
77            raise OpenIDProviderConfigError('Identity URI %r doesn\'t match '
78                                            'the template suffix %r' %
79                                            (identityUri, uriSuffix))
80       
81        userIdentifier = identityUri[:suffixIndex].replace(uriPrefix, '', 1)
82        return userIdentifier
83 
84# Place here to avoid circular import error with IdentityMapping class     
85from ndg.security.server.wsgi.openid.provider.authninterface import (
86    AbstractAuthNInterface, AuthNInterfaceError)
87from ndg.security.server.wsgi.openid.provider.axinterface import (AXInterface,
88    MissingRequiredAttrs, AXInterfaceReloginRequired)
89
90
91# Aliases to AXInterface exception types   
92class OpenIDProviderMissingRequiredAXAttrs(MissingRequiredAttrs): 
93    """Raise if a Relying Party *requires* one or more attributes via
94    the AX interface but this OpenID Provider cannot return them.  This
95    doesn't apply to attributes that are optional"""
96
97
98class OpenIDProviderReloginRequired(AXInterfaceReloginRequired):
99    pass
100
101# end aliases to AXInterface exception types
102
103
104class OpenIDProviderMiddlewareError(Exception):
105    """OpenID Provider WSGI Middleware Error"""
106
107
108class OpenIDProviderConfigError(OpenIDProviderMiddlewareError):
109    """OpenID Provider Configuration Error"""
110
111
112class OpenIDProviderMissingAXResponseHandler(OpenIDProviderMiddlewareError): 
113    """Raise if a Relying Party *requires* one or more attributes via
114    the AX interface but no AX Response handler has been set"""
115 
116 
117class OpenIDProviderMiddleware(NDGSecurityMiddlewareBase):
118    """WSGI Middleware to implement an OpenID Provider
119   
120    @cvar defOpt: app_conf options / keywords to __init__ and their default
121    values.  Input keywords must match these
122    @type defOpt: dict
123   
124    @cvar defPaths: subset of defOpt.  These are keyword items corresponding
125    to the URL paths to be set for the individual OpenID Provider functions
126    @type: defPaths: dict
127   
128    @cvar formRespWrapperTmpl: If the response to the Relying Party is too long
129    it's rendered as form with the POST method instead of query arguments in a
130    GET 302 redirect.  Wrap the form in this document to make the form submit
131    automatically without user intervention.  See _displayResponse method
132    below...
133    @type formRespWrapperTmpl: basestring"""
134   
135    formRespWrapperTmpl = """<html>
136    <head>
137        <script type="text/javascript">
138            function doRedirect()
139            {
140                document.forms[0].submit();
141            }
142        </script>
143    </head>
144    <body onLoad="doRedirect()">
145        %s
146    </body>
147</html>"""
148
149    defOpt = dict(
150        path_openidserver='/openidserver',
151        path_login='/login',
152        path_loginsubmit='/loginsubmit',
153        path_id='/id/${userIdentifier}',
154        path_yadis='/yadis/${userIdentifier}',
155        path_serveryadis='/serveryadis',
156        path_allow='/allow',
157        path_decide='/decide',
158        path_mainpage='/',
159        session_middleware='beaker.session',
160        base_url='',
161        consumer_store_dirpath='./',
162        charset=None,
163        trace=False,
164        renderingClass=None,
165        sregResponseHandler=None,
166        axResponseHandler=None,
167        authNInterface=AbstractAuthNInterface)
168   
169    defPaths = dict([(k, v) for k, v in defOpt.items() 
170                     if k.startswith('path_')])
171   
172    userIdentifierPat = '([^/]*)'
173   
174    USERNAME_SESSION_KEYNAME = 'username'
175    IDENTITY_URI_SESSION_KEYNAME = 'identityURI'
176    APPROVED_FLAG_SESSION_KEYNAME = 'approved'
177    LAST_CHECKID_REQUEST_SESSION_KEYNAME = 'lastCheckIDRequest'
178    PARAM_PREFIX = 'openid.provider.'
179   
180    IDENTITY_URI_TMPL_PARAMNAME = 'identityUriTemplate'
181   
182    # Approve and reject submit HTML input types for the Relying Party Approval
183    # page
184    APPROVE_RP_SUBMIT = "ApproveRelyingParty"
185    REJECT_RP_SUBMIT = "RejectRelyingParty"
186   
187    def __init__(self, app, app_conf=None, prefix=PARAM_PREFIX, **kw):
188        '''
189        @type app: callable following WSGI interface
190        @param app: next middleware application in the chain     
191        @type app_conf: dict       
192        @param app_conf: PasteDeploy application configuration dictionary
193        @type prefix: basestring
194        @param prefix: prefix for OpenID Provider configuration items
195        @type kw: dict
196        @param kw: keyword dictionary - must follow format of defOpt
197        class variable   
198        '''
199        self._app = app
200        self._environ = {}
201        self._start_response = None
202        self._pathInfo = None
203        self._path = None
204        self.mountPath = '/'
205       
206        # See _createResponse method
207        self.oidResponse = None
208
209        opt = OpenIDProviderMiddleware.defOpt.copy()
210        if app_conf is not None:
211            # Update from application config dictionary - filter from using
212            # prefix
213            OpenIDProviderMiddleware._filterOpts(opt, app_conf, prefix=prefix)
214                       
215        # Similarly, filter keyword input                 
216        OpenIDProviderMiddleware._filterOpts(opt, kw, prefix=prefix)
217       
218        # Update options from keywords - matching app_conf ones will be
219        # overwritten
220        opt.update(kw)
221       
222        # Convert from string type where required   
223        opt['charset'] = opt.get('charset', '')
224        opt['trace'] = opt.get('trace', 'false').lower() == 'true'
225         
226        renderingClassVal = opt.get('renderingClass', None)     
227        if renderingClassVal:
228            opt['renderingClass'] = eval_import(renderingClassVal)
229       
230        sregResponseHandlerVal = opt.get('sregResponseHandler', None) 
231        if sregResponseHandlerVal:
232            opt['sregResponseHandler'] = eval_import(sregResponseHandlerVal) 
233        else:
234            opt['sregResponseHandler'] = None
235
236        axResponseHandlerVal = opt.get('axResponseHandler', None) 
237        if axResponseHandlerVal:
238            opt['axResponseHandler'] = eval_import(axResponseHandlerVal)
239        else:
240            opt['axResponseHandler'] = None
241
242        # Authentication interface to OpenID Provider - interface to for
243        # example a user database or other means of authentication
244        authNInterfaceName = opt.get('authNInterface')
245        if authNInterfaceName:
246            authNInterfaceClass = eval_import(authNInterfaceName)
247            if not issubclass(authNInterfaceClass, AbstractAuthNInterface):
248                raise OpenIDProviderMiddlewareError("Authentication interface "
249                                                    "class %r is not a %r "
250                                                    "derived type" % 
251                                                    (authNInterfaceClass,
252                                                     AbstractAuthNInterface))
253        else:
254            authNInterfaceClass = AbstractAuthNInterface
255       
256        # Extract Authentication interface specific properties
257        authNInterfaceProperties = dict([(k.replace('authN_', ''), v) 
258                                         for k, v in opt.items() 
259                                         if k.startswith('authN_')]) 
260         
261        try:
262            self._authN = authNInterfaceClass(**authNInterfaceProperties)
263        except Exception, e:
264            log.error("Error instantiating authentication interface: %s" % e)
265            raise
266
267        # Paths relative to base URL - Nb. remove trailing '/'
268        self.paths = dict([(k, opt[k].rstrip('/'))
269                           for k in OpenIDProviderMiddleware.defPaths])
270                       
271        if not opt['base_url']:
272            raise TypeError("base_url is not set")
273       
274        self.base_url = opt['base_url']
275
276
277        self.identityUriTmpl = opt.get(
278                        OpenIDProviderMiddleware.IDENTITY_URI_TMPL_PARAMNAME)
279        if not self.identityUriTmpl:
280            raise TypeError("%s is not set" % 
281                        OpenIDProviderMiddleware.IDENTITY_URI_TMPL_PARAMNAME)
282       
283        # Full Paths
284        self.urls = dict([(k.replace('path_', 'url_'), self.base_url + v)
285                          for k, v in self.paths.items()])
286
287        self.method = dict([(v, k.replace('path_', 'do_'))
288                            for k, v in self.paths.items()])
289
290        self.session_middleware = opt['session_middleware']
291
292        if not opt['charset']:
293            self.charset = ''
294        else:
295            self.charset = '; charset=' + charset
296       
297        # If True and debug log level is set display content of response
298        self._trace = opt['trace']
299
300        log.debug("opt=%r", opt)       
301       
302        # Pages can be customised by setting external rendering interface
303        # class
304        renderingClass = opt.get('renderingClass', None) or RenderingInterface         
305        if not issubclass(renderingClass, RenderingInterface):
306            raise OpenIDProviderMiddlewareError("Rendering interface "
307                                                "class %r is not a %r "
308                                                "derived type" % \
309                                                (renderingClass,
310                                                 RenderingInterface))
311       
312        # Extract rendering interface specific properties
313        renderingProperties = dict([(k.replace('rendering_', ''), v) 
314                                         for k, v in opt.items() 
315                                         if k.startswith('rendering_')])   
316
317        try:
318            self._render = renderingClass(self._authN,
319                                          self.base_url,
320                                          self.urls,
321                                          **renderingProperties)
322        except Exception, e:
323            log.error("Error instantiating rendering interface: %s" % e)
324            raise
325                   
326        # Callable for setting of Simple Registration attributes in HTTP header
327        # of response to Relying Party
328        self.sregResponseHandler = opt.get('sregResponseHandler', None)
329        if self.sregResponseHandler and not callable(self.sregResponseHandler):
330            raise OpenIDProviderMiddlewareError("Expecting callable for "
331                                                "sregResponseHandler keyword, "
332                                                "got %r" % 
333                                                self.sregResponseHandler)
334           
335        # Class to handle OpenID Attribute Exchange (AX) requests from
336        # the Relying Party
337        axResponseClassName = opt.pop('axResponse_class', None)
338        if axResponseClassName is None:
339            # No AX Response handler set
340            self.axResponse = None
341        else:
342            axResponseModuleFilePath = opt.pop('axResponse_moduleFilePath',
343                                               None)
344            axResponseProperties = dict(
345                [(k.replace('axResponse_', ''), v) 
346                 for k,v in opt.items() if k.startswith('axResponse_')])
347               
348            try:
349                self.axResponse = instantiateClass(
350                                    axResponseClassName, 
351                                    None, 
352                                    moduleFilePath=axResponseModuleFilePath,
353                                    objectType=AXInterface, 
354                                    classProperties=axResponseProperties)
355            except Exception, e:
356                log.error("Error instantiating AX interface: %s" % e)
357                raise
358       
359        # Instantiate OpenID consumer store and OpenID consumer.  If you
360        # were connecting to a database, you would create the database
361        # connection and instantiate an appropriate store here.
362        store = FileOpenIDStore(
363                            os.path.expandvars(opt['consumer_store_dirpath']))
364        self.oidserver = server.Server(store, self.urls['url_openidserver'])
365       
366    @classmethod
367    def _filterOpts(cls, opt, newOpt, prefix=''):
368        '''Convenience utility to filter input options set in __init__ via
369        app_conf or keywords
370       
371        Nb. exclusions for authN and rendering interface properties.
372       
373        @type opt: dict
374        @param opt: existing options set.  These will be updated by this
375        method based on the content of newOpt
376        @type newOpt: dict
377        @param newOpt: new options to update opt with
378        @type prefix: basestring
379        @param prefix: if set, remove the given prefix from the input options
380        @raise KeyError: if an option is set that is not in the classes
381        defOpt class variable
382        '''
383        def _isBadOptName(optName):
384            # Allow for authN.* and rendering.* properties used by the
385            # Authentication and Rendering interfaces respectively
386            return optName not in cls.defOpt and \
387               not optName.startswith('authN_') and \
388               not optName.startswith('rendering_') and \
389               not optName.startswith('axResponse_')
390               
391        badOptNames = [] 
392        for optName, optVal in newOpt.items():
393            if prefix:
394                if optName.startswith(prefix):
395                    optName = optName.replace(prefix, '')               
396                    filtOptName = '_'.join(optName.split('.'))
397                                           
398                    # Skip assignment for bad option names and record them in
399                    # an error list instead
400                    if _isBadOptName(filtOptName):
401                        badOptNames += [optName]                   
402                    else:
403                        opt[filtOptName] = optVal
404            else:
405                filtOptName = '_'.join(optName.split('.'))
406
407                # Record any bad option names
408                if _isBadOptName(filtOptName):
409                    badOptNames += [optName]                   
410                else:
411                    opt[filtOptName] = optVal
412               
413        if len(badOptNames) > 0:
414            raise TypeError("Invalid input option(s) set: %s" % 
415                            (", ".join(badOptNames)))
416                       
417    def _matchIdentityURI(self):
418        idPaths = (self.paths['path_id'], self.paths['path_yadis'])
419        idPathMatches = [Template(path).substitute(
420                    userIdentifier=OpenIDProviderMiddleware.userIdentifierPat)
421                    for path in idPaths]
422       
423        for idPathMatch, idPath in zip(idPathMatches, idPaths):
424            if re.search(idPathMatch, self.path):
425                return idPath
426           
427        return None
428
429    def _isAnIdentityURI(self):
430        """Check input URI is an identity URI.  Use to determine whether a
431        RP discovery request has been made based on a provided user OpenID.
432        i.e. do_id / do_yadis should be invoked - see __call__ method for
433        details.  It takes the identity portion of the URI from the config
434        path_id / path_yadis settings - which ever matches e.g.
435       
436        <http/https>://<domainname>/<path>/${userIdentifier}
437       
438        e.g.
439       
440        https://badc.rl.ac.uk/openid/johnsmith
441       
442        This method should be overridden in a derived class if some
443        other means of representing identity URIs is required. e.g.
444       
445        https://johnsmith.badc.rl.ac.uk
446       
447        but note also see _matchIdentityURI method
448       
449        @rtype: bool
450        @return: return True if the given URI is an identity URI, otherwise
451        False
452        """       
453        return self._matchIdentityURI() is not None
454
455    def _parseIdentityURI(self):
456        '''Split path into identity and path fragment components
457       
458        @rtype: list
459        @return: 2 element list containing the Identity URI path fragment and
460        user identifier respectively.
461        '''
462        return OpenIDProviderMiddleware.parseIdentityURI(self.path)   
463
464    @classmethod
465    def parseIdentityURI(cls, uri):
466        '''Split uri into identity and uri fragment components
467       
468        FIXME: this method assumes a fixed identity URI scheme and should be
469        refactored to use a template based parsing
470       
471        @type uri: basestring
472        @param uri: identity URI to be parsed
473        @rtype: list
474        @return: 2 element list containing the Identity URI fragment and
475        user identifier respectively.
476        '''
477        return uri.rsplit('/', 1)
478   
479    @classmethod
480    def createIdentityURI(cls, uri, userIdentifier):
481        '''This method is the compliment to parseIdentityURI.  Make an OpenID
482        URI from a user identifier and URI fragment
483       
484        @type uri: basestring
485        @param uri: identity URI containing $userIdentifier where user id is
486        to be substituted in
487        @type userIdentifier: basestring
488        @param userIdentifier: identity URI to be parsed
489        @rtype: basestring
490        @return: user OpenID URI
491        '''
492        return Template(uri).substitute(userIdentifier=userIdentifier)
493       
494    @NDGSecurityMiddlewareBase.initCall
495    def __call__(self, environ, start_response):
496        """Standard WSGI interface.  Intercepts the path if it matches any of
497        the paths set in the path_* keyword settings to the config
498       
499        @type environ: dict
500        @param environ: dictionary of environment variables
501        @type start_response: callable
502        @param start_response: standard WSGI callable to set HTTP headers
503        @rtype: basestring
504        @return: WSGI response
505        """
506        if not environ.has_key(self.session_middleware):
507            raise OpenIDProviderConfigError('The session middleware %r is not '
508                                            'present. Have you set up the '
509                                            'session middleware?' %
510                                            self.session_middleware)
511
512        # Beware path is a property and invokes the _setPath method
513        self.session = environ[self.session_middleware]
514        self._render.session = self.session
515       
516        pathMatch = self._matchIdentityURI()
517        if not pathMatch:
518            pathMatch = self.path
519
520        if pathMatch in self.method:
521            # Calls to parse_formvars seem to gobble up the POST content such
522            # that a 2nd call yields nothing! (with Paste 1.7.1)
523            self.query = dict(paste.request.parse_formvars(environ)) 
524            log.debug("Calling method %s ..." % self.method[pathMatch]) 
525           
526            action = getattr(self, self.method[pathMatch])
527            response = action(environ, start_response) 
528            if self._trace and _debugLevel:
529                if isinstance(response, list):
530                    log.debug('Output for %s:\n%s', self.method[pathMatch],
531                                                    ''.join(response))
532                else:
533                    log.debug('Output for %s:\n%s', self.method[pathMatch],
534                                                    response)
535                   
536            return response
537        else:
538            log.debug("No match for path %s" % self.path)
539            return self._setResponse(environ, start_response)
540
541    def do_id(self, environ, start_response):
542        '''URL based discovery with an ID provided
543       
544        @type environ: dict
545        @param environ: dictionary of environment variables
546        @type start_response: callable
547        @param start_response: standard WSGI callable to set HTTP headers
548        @rtype: basestring
549        @return: WSGI response
550       
551        '''
552        response = self._render.identityPage(environ, start_response)
553        return response
554
555    def do_yadis(self, environ, start_response):
556        """Handle Yadis based discovery with an ID provided
557       
558        @type environ: dict
559        @param environ: dictionary of environment variables
560        @type start_response: callable
561        @param start_response: standard WSGI callable to set HTTP headers
562        @rtype: basestring
563        @return: WSGI response
564
565        """
566        response = self._render.yadis(environ, start_response)
567        return response
568
569    def do_serveryadis(self, environ, start_response):
570        """Yadis based discovery for ID Select mode i.e. no user ID given for
571        OpenID identifier at Relying Party
572       
573        @type environ: dict
574        @param environ: dictionary of environment variables
575        @type start_response: callable
576        @param start_response: standard WSGI callable to set HTTP headers
577        @rtype: basestring
578        @return: WSGI response
579
580        """
581        response = self._render.serverYadis(environ, start_response)
582        return response
583
584    def do_openidserver(self, environ, start_response):
585        """OpenID Server endpoint - handles OpenID Request following discovery
586       
587        @type environ: dict
588        @param environ: dictionary of environment variables
589        @type start_response: callable
590        @param start_response: standard WSGI callable to set HTTP headers
591        @rtype: basestring
592        @return: WSGI response
593        """
594        try:
595            oidRequest = self.oidserver.decodeRequest(self.query)
596           
597        except server.ProtocolError, why:
598            response = self._displayResponse(why)
599           
600        else:
601            if oidRequest is None:
602                # Display text indicating that this is an endpoint.
603                response = self.do_mainpage(environ, start_response)
604           
605            # Check mode is one of "checkid_immediate", "checkid_setup"
606            elif oidRequest.mode in server.BROWSER_REQUEST_MODES:
607                response = self._handleCheckIDRequest(oidRequest)
608            else:
609                oidResponse = self.oidserver.handleRequest(oidRequest)
610                response = self._displayResponse(oidResponse)
611           
612        return response
613
614    def do_allow(self, environ, start_response):
615        """Handle allow request processing the result of do_decide: does user
616        allow credentials to be passed back to the Relying Party?
617       
618        This method expects the follow fields to have been set in the posted
619        form created by the RenderingInterface.decidePage method called by
620        do_decide:
621       
622        'Yes'/'No': for return authentication details back to the RP or
623        abort return to RP respectively
624        'remember': remember the decision corresponding to the above 'Yes'
625        /'No'.
626        This may be set to 'Yes' or 'No'
627        'identity': set to the user's identity URL.  This usually is not
628        required since it can be obtained from oidRequest.identity attribute
629        but in ID Select mode, the identity URL will have been selected or set
630        in the decide page interface.
631       
632        @type environ: dict
633        @param environ: dictionary of environment variables
634        @type start_response: callable
635        @param start_response: standard WSGI callable to set HTTP headers
636        @rtype: basestring
637        @return: WSGI response
638        """ 
639        oidRequest = self.session.get(
640                OpenIDProviderMiddleware.LAST_CHECKID_REQUEST_SESSION_KEYNAME)
641        if oidRequest is None:
642            log.error("Suspected do_allow called from stale request")
643            return self._render.errorPage(environ, start_response,
644                                          "Invalid request.  Please report "
645                                          "this fault to your site "
646                                          "administrator.",
647                                          code=400)
648       
649        if OpenIDProviderMiddleware.APPROVE_RP_SUBMIT in self.query:
650            if oidRequest.idSelect():
651                identity = self.query.get('identity')
652                if identity is None:
653                    log.error("No identity field set from decide page for "
654                              "processing in ID Select mode")
655                    return self._render.errorPage(environ, start_response,
656                                                  "An internal error has "
657                                                  "occurred setting the "
658                                                  "OpenID user identity.  "
659                                                  "Please report this fault "
660                                                  "to your site "
661                                                  "administrator.")
662            else:
663                identity = oidRequest.identity
664
665            trust_root = oidRequest.trust_root
666            if self.query.get('remember', 'no').lower() == 'yes':
667                self.session[
668                    OpenIDProviderMiddleware.APPROVED_FLAG_SESSION_KEYNAME] = {
669                        trust_root: 'always'
670                    }
671                self.session.save()
672           
673            # Check for POST'ed user explicit setting of AX parameters
674            self._applyUserAXSelections()
675                   
676            return self._displayResponse(self.oidResponse)
677       
678        elif OpenIDProviderMiddleware.REJECT_RP_SUBMIT in self.query:
679            # TODO: Check 'No' response is OK - No causes AuthKit's Relying
680            # Party implementation to crash with 'openid.return_to' KeyError
681            # in Authkit.authenticate.open_id.process
682            self.oidResponse = oidRequest.answer(False)
683            return self._render.mainPage(environ, start_response)           
684        else:
685            log.error('Setting response following ID Approval: expecting '
686                      'Yes/No in allow post. %r' % self.query)
687            return self._render.errorPage(environ, start_response,
688                                          'Error setting Yes/No response to '
689                                          'return your credentials back to '
690                                          'the requesting site.  Please '
691                                          'report this fault to your site '
692                                          'administrator.',
693                                          code=400)
694           
695    def _applyUserAXSelections(self):
696        """Helper for do_allow method - process the query response checking
697        user Attribute Exchange settings deleting optional attributes if the
698        user has deselected them from the decide interface.  All mandatory
699        attributes will be returned regardless if the user submitted and called
700        do_allow()
701        """
702       
703        # Process decide page AX POST'ed items
704        userAXSettings = {}
705        for fieldName in self.query:
706            if fieldName.startswith('ax.'):
707                alias = fieldName.rsplit('ax.', 1)[-1]
708                userAXSettings[alias] = self.query[fieldName]
709       
710        # Apply user selections to the response
711        if userAXSettings:
712            # Get all the content namespaced as AX type
713            axArgs = self.oidResponse.fields.getArgs(ax.AXMessage.ns_uri)
714            for i in axArgs.keys():
715                # Parse alias name but note not all keys are alias and so will
716                # not use the '.' delimiter
717                keyParts = i.split('.')
718                if len(keyParts) > 1 and not keyParts[1] in userAXSettings:
719                    self.oidResponse.fields.delArg(ax.AXMessage.ns_uri, i)
720                   
721    def do_login(self, environ, start_response, **kw):
722        """Display Login form
723       
724        @type environ: dict
725        @param environ: dictionary of environment variables
726        @type start_response: callable
727        @param start_response: standard WSGI callable to set HTTP headers
728        @type kw: dict
729        @param kw: keywords to login renderer - see RenderingInterface class
730        @rtype: basestring
731        @return: WSGI response
732        """
733       
734        if 'fail_to' not in kw:
735            kw['fail_to'] = self.urls['url_login']
736           
737        response = self._render.login(environ, start_response, **kw)
738        return response
739
740    def do_loginsubmit(self, environ, start_response):
741        """Handle user submission from login and logout
742       
743        @type environ: dict
744        @param environ: dictionary of environment variables
745        @type start_response: callable
746        @param start_response: standard WSGI callable to set HTTP headers
747        @rtype: basestring
748        @return: WSGI response
749        """
750        fail2URI = self.query.get('fail_to')
751       
752        if 'submit' in self.query:
753            if 'username' in self.query:
754                return self.__loginSubmit(environ, start_response)
755            else:
756                return self.__logoutSubmit(environ, start_response)
757       
758        else:
759            if 'cancel' not in self.query:
760                log.error('Login input not recognised: %r' % self.query)
761               
762            if fail2URI is None:
763                log.error("No 'fail_to' field set in Login POST: %r" % 
764                          self.query)
765                return self._render.errorPage(environ, start_response,
766                    "An internal error occurred possibly due to a request "
767                    "that's expired.  Please retry from the site where "
768                    "you entered your OpenID.  If the problem persists "
769                    "report it to your site administrator.")               
770            else:
771                return self._redirect(start_response, fail2URI) 
772       
773    def __loginSubmit(self, environ, start_response):
774        """Helper method to do_loginsubmit - handles 'username' field in login
775        form POST
776       
777        @type environ: dict
778        @param environ: dictionary of environment variables
779        @type start_response: callable
780        @param start_response: standard WSGI callable to set HTTP headers
781        @rtype: basestring
782        @return: WSGI response
783        """
784        if (OpenIDProviderMiddleware.USERNAME_SESSION_KEYNAME in 
785            self.session):
786            log.error("Attempting login for user %s: user is already "
787                      "logged in", self.session[
788                    OpenIDProviderMiddleware.USERNAME_SESSION_KEYNAME])
789           
790            return self._redirect(start_response,
791                                  self.query['fail_to'])
792       
793        oidRequest = self.session.get(
794                OpenIDProviderMiddleware.LAST_CHECKID_REQUEST_SESSION_KEYNAME)
795       
796        if oidRequest is None:
797            log.error("Getting OpenID request for login - No request "
798                      "found in session")
799            return self._render.errorPage(environ, start_response,
800                "An internal error occurred possibly due to a request "
801                "that's expired.  Please retry from the site where "
802                "you entered your OpenID.  If the problem persists "
803                "report it to your site administrator.")
804           
805        # Get user identifier to check against credentials provided
806        if oidRequest.idSelect():
807            # ID select mode enables the user to request specifying
808            # their OpenID Provider without giving a personal user URL
809            try:
810                userIdentifiers = self._authN.username2UserIdentifiers(
811                                                environ,
812                                                self.query['username'])
813            except Exception:
814                log.error("Associating username %r with OpenID URL: %s"% 
815                          (self.query.get('username'),
816                           traceback.format_exc()))
817                msg = ("An internal error occured matching an OpenID "
818                       "to your account.  If the problem persists "
819                       "contact your system administrator.")
820
821                response = self._render.errorPage(environ, 
822                                                  start_response,
823                                                  msg) 
824                return response
825
826            if not isinstance(userIdentifiers, (list, tuple)):
827                log.error("Unexpected type %r returned from %r for "
828                          "user identifiers; expecting list or tuple"
829                          % (type(userIdentifiers), type(self._authN)))
830                         
831                return self._render.errorPage(environ, start_response,
832                    "An internal error occurred setting your default "
833                    "OpenID URL.  Please retry from the site where "
834                    "you entered your OpenID providing your full "
835                    "identity URL.  If the problem persists report it "
836                    "to your site administrator.")
837               
838            # FIXME: Assume the *first* user identifier entry is the
839            # one to use.  The user could have multiple identifiers
840            # but in practice it's more manageable to have a single one
841            identityURI = self.createIdentityURI(self.identityUriTmpl,
842                                                 userIdentifiers[0])
843        else:
844            # Get the unique user identifier from the user's OpenID URL
845            identityURI = oidRequest.identity
846           
847        # Invoke custom authentication interface plugin
848        try:
849            self._authN.logon(environ,
850                              identityURI,
851                              self.query['username'],
852                              self.query.get('password', ''))
853           
854        except AuthNInterfaceError, e:
855            return self._render.login(environ, 
856                                      start_response,
857                                      msg=e.userMsg,
858                                      success_to=self.urls['url_decide'])                   
859        except Exception, e:
860            log.error("Unexpected %s type exception raised during "
861                      "authentication: %s", type(e),
862                      traceback.format_exc())
863            msg = ("An internal error occurred.  "
864                   "Please try again or if the problems persists "
865                   "contact your system administrator.")
866
867            response = self._render.login(environ, start_response,
868                                  msg=msg,
869                                  success_to=self.urls['url_decide'])
870            return response
871       
872        # Update session details     
873        self.session[
874            OpenIDProviderMiddleware.USERNAME_SESSION_KEYNAME
875        ] = self.query['username']
876       
877        self.session[
878            OpenIDProviderMiddleware.IDENTITY_URI_SESSION_KEYNAME] = identityURI
879               
880        self.session[
881            OpenIDProviderMiddleware.APPROVED_FLAG_SESSION_KEYNAME] = {}
882           
883        self.session.save()
884       
885        log.info("user [%s] logged in", self.session[
886                 OpenIDProviderMiddleware.USERNAME_SESSION_KEYNAME])
887       
888        return self._redirect(start_response, self.query['success_to'])
889       
890    def __logoutSubmit(self, environ, start_response):
891        """Logout helper method to do_loginsubmit - triggered when 'submit'
892        field set in login POST action but no 'username' field set
893       
894        @type environ: dict
895        @param environ: dictionary of environment variables
896        @type start_response: callable
897        @param start_response: standard WSGI callable to set HTTP headers
898        @rtype: basestring
899        @return: WSGI response
900        """
901        if (OpenIDProviderMiddleware.USERNAME_SESSION_KEYNAME not in 
902            self.session):
903            log.error("No user is logged in")
904            return self._redirect(start_response, self.query['fail_to'])
905       
906        log.info("user [%s] logging out ...", self.session[
907                 OpenIDProviderMiddleware.USERNAME_SESSION_KEYNAME])
908
909        del self.session[OpenIDProviderMiddleware.USERNAME_SESSION_KEYNAME]
910        self.session.pop(OpenIDProviderMiddleware.APPROVED_FLAG_SESSION_KEYNAME, 
911                         None)
912        self.session.save()
913       
914        try:
915            self._authN.logout()
916           
917        except Exception:
918            log.error("Unexpected exception raised during logout: %s" % 
919                      traceback.format_exc())
920            msg = ("An internal error occured during logout.  If the problem "
921                   "persists contact your system administrator.")
922
923            response = self._render.errorPage(environ, start_response, msg) 
924            return response
925           
926        return self._redirect(start_response, self.query['success_to'])
927   
928    def do_mainpage(self, environ, start_response):
929        '''Show an information page about the OpenID Provider
930       
931        @type environ: dict
932        @param environ: dictionary of environment variables
933        @type start_response: callable
934        @param start_response: standard WSGI callable to set HTTP headers
935        @rtype: basestring
936        @return: WSGI response
937        '''   
938        response = self._render.mainPage(environ, start_response)
939        return response
940
941    def _getRender(self):
942        """Get method for rendering interface object
943        @rtype: RenderingInterface
944        @return: rendering interface object
945        """
946        return self._render
947   
948    render = property(fget=_getRender, doc="Rendering interface instance")
949   
950    def do_decide(self, environ, start_response):
951        """Display page prompting the user to decide whether to trust the site
952        requesting their credentials
953       
954        @type environ: dict
955        @param environ: dictionary of environment variables
956        @type start_response: callable
957        @param start_response: standard WSGI callable to set HTTP headers
958        @rtype: basestring
959        @return: WSGI response
960        """
961        oidRequest = self.session.get(
962                OpenIDProviderMiddleware.LAST_CHECKID_REQUEST_SESSION_KEYNAME)
963        if oidRequest is None:
964            log.error("No OpenID request set in session")
965            return self._render.errorPage(environ, start_response,
966                                          "Invalid request.  Please report "
967                                          "the error to your site "
968                                          "administrator.",
969                                          code=400)
970           
971        # Prepare a response with any additional AX or SReg parameters appended
972        # so that the decide page can if required show these to the user for
973        # review before return to the RP.
974        identityURI = self.session[
975            OpenIDProviderMiddleware.IDENTITY_URI_SESSION_KEYNAME]
976       
977        # Set the Response HERE to enable the inclusion of a user confirmation
978        # interface in the decide page so that the user can review the content
979        # of the response to be returned to the RP.
980        try:
981            self._createResponse(oidRequest, identifier=identityURI)
982                   
983        except (OpenIDProviderMissingRequiredAXAttrs,
984                OpenIDProviderMissingAXResponseHandler):
985            response = self._render.errorPage(self.environ, 
986                                              self.start_response,
987                'The site where you wish to signin requires '
988                'additional information which this site isn\'t '
989                'configured to provide.  Please report this fault to '
990                'your site administrator.')
991            return response
992           
993        except OpenIDProviderReloginRequired, e:
994            response = self._render.errorPage(self.environ, 
995                                              self.start_response,
996                'An error occurred setting additional parameters required by '
997                'the site requesting your ID.  Please try logging in again.')
998            return response
999           
1000        except Exception, e:
1001            log.error("%s type exception raised setting response following ID "
1002                      "Approval: %s", e.__class__.__name__, 
1003                      traceback.format_exc())
1004            response = self._render.errorPage(self.environ,
1005                                              self.start_response,
1006                'An error occurred setting additional parameters required by '
1007                'the site requesting your ID.  Please report this fault to '
1008                'your site administrator.')
1009            return response
1010       
1011        try:
1012            return self._render.decidePage(environ, 
1013                                           start_response, 
1014                                           oidRequest,
1015                                           self.oidResponse)
1016        except AuthNInterfaceError, e:
1017            log.error("%s type exception raised calling decide page "
1018                      "rendering - an OpenID identifier look-up error? "
1019                      "message is: %s", e.__class__.__name__,
1020                      traceback.format_exc())
1021            response = self._render.errorPage(environ, start_response,
1022                    'An error has occurred displaying an options page '
1023                    'which checks whether you want to return to the site '
1024                    'requesting your ID.  Please report this fault to '
1025                    'your site administrator.')
1026            return response
1027
1028    def _identityIsAuthorized(self, oidRequest):
1029        '''Check that a user is authorized i.e. does a session exist for their
1030        username and if so does it correspond to the identity URL provided.
1031        This last check doesn't apply for ID Select mode where No ID was input
1032        at the Relying Party.
1033       
1034        @type oidRequest: openid.server.server.CheckIDRequest
1035        @param oidRequest: OpenID Request object
1036        @rtype: bool
1037        @return: True/False is user authorized
1038        '''
1039        username = self.session.get(
1040                            OpenIDProviderMiddleware.USERNAME_SESSION_KEYNAME)
1041        if username is None:
1042            return False
1043
1044        if oidRequest.idSelect():
1045            log.debug("OpenIDProviderMiddleware._identityIsAuthorized - "
1046                      "ID Select mode set but user is already logged in")
1047            return True
1048       
1049        identityURI = self.session.get(
1050                        OpenIDProviderMiddleware.IDENTITY_URI_SESSION_KEYNAME)
1051        if identityURI is None:
1052            return False
1053       
1054        if oidRequest.identity != identityURI:
1055            log.debug("OpenIDProviderMiddleware._identityIsAuthorized - user "
1056                      "is already logged in with a different ID=%s and "
1057                      "identityURI=%s" %
1058                      (username, identityURI))
1059            return False
1060       
1061        log.debug("OpenIDProviderMiddleware._identityIsAuthorized - "
1062                  "user is logged in with ID matching ID URI")
1063        return True
1064   
1065
1066    def _trustRootIsAuthorized(self, trust_root):
1067        '''Return True/False for the given trust root (Relying Party)
1068        previously been approved by the user
1069       
1070        @type trust_root: dict
1071        @param trust_root: keyed by trusted root (Relying Party) URL and
1072        containing string item 'always' if approved
1073        @rtype: bool
1074        @return: True - trust has already been approved, False - trust root is
1075        not approved'''
1076        approvedRoots = self.session.get(
1077                        OpenIDProviderMiddleware.APPROVED_FLAG_SESSION_KEYNAME, 
1078                        {})
1079        return approvedRoots.get(trust_root) is not None
1080
1081    def _addSRegResponse(self, oidRequest):
1082        '''Add Simple Registration attributes to response to Relying Party
1083       
1084        @type oidRequest: openid.server.server.CheckIDRequest
1085        @param oidRequest: OpenID Check ID Request object'''
1086       
1087        if self.sregResponseHandler is None:
1088            # No Simple Registration response object was set
1089            return
1090       
1091        sreg_req = sreg.SRegRequest.fromOpenIDRequest(oidRequest)
1092
1093        # Callout to external callable sets additional user attributes to be
1094        # returned in response to Relying Party       
1095        sreg_data = self.sregResponseHandler(self.session.get(
1096                            OpenIDProviderMiddleware.USERNAME_SESSION_KEYNAME))
1097        sreg_resp = sreg.SRegResponse.extractResponse(sreg_req, sreg_data)
1098        self.oidResponse.addExtension(sreg_resp)
1099
1100    def _addAXResponse(self, oidRequest):
1101        '''Add attributes to response based on the OpenID Attribute Exchange
1102        interface
1103       
1104        @type oidRequest: openid.server.server.CheckIDRequest
1105        @param oidRequest: OpenID Check ID Request object
1106        '''
1107
1108        ax_req = ax.FetchRequest.fromOpenIDRequest(oidRequest)
1109        if ax_req is None:
1110            log.debug("No Attribute Exchange extension set in request")
1111            return
1112       
1113        ax_resp = ax.FetchResponse(request=ax_req)
1114       
1115        if self.axResponse is None:
1116            requiredAttr = ax_req.getRequiredAttrs()
1117            if len(requiredAttr) > 0:
1118                msg = ("Relying party requires these attributes: %s; but No"
1119                       "Attribute exchange handler 'axResponseHandler' has "
1120                       "been set" % requiredAttr)
1121                log.error(msg)
1122                raise OpenIDProviderMissingAXResponseHandler(msg)
1123           
1124            return
1125       
1126        log.debug("Calling AX plugin: %s ...",
1127                  self.axResponse.__class__.__name__)
1128       
1129        # Set requested values - need user intervention here to confirm
1130        # release of attributes + assignment based on required attributes -
1131        # possibly via FetchRequest.getRequiredAttrs()
1132        try:
1133            self.axResponse(ax_req, ax_resp, self._authN, self.session)
1134       
1135        except OpenIDProviderMissingRequiredAXAttrs, e:
1136            log.error("OpenID Provider is unable to set the AX attributes "
1137                      "required by the Relying Party's request: %s" % e)
1138            raise
1139       
1140        except OpenIDProviderReloginRequired, e:
1141            log.exception(e)
1142            raise
1143       
1144        except Exception, e:
1145            log.error("%s exception raised setting requested Attribute "
1146                      "Exchange values: %s", 
1147                      e.__class__.__name__, 
1148                      traceback.format_exc())
1149            raise
1150       
1151        log.debug("Adding AX parameters to response: %s ...", ax_resp)
1152        self.oidResponse.addExtension(ax_resp)
1153        log.debug("Added AX parameters to response")
1154       
1155    def _createResponse(self, oidRequest, identifier=None):
1156        '''Create a response object from the input request and add
1157        Simple Registration and/or Attribute Exchange parameters if handlers
1158        were specified - See _addSRegResponse and _addAXResponse methods - and
1159        only if the Relying Party has requested them
1160       
1161        @type oidRequest: openid.server.server.CheckIDRequest
1162        @param oidRequest: OpenID Check ID Request object
1163        @type identifier: basestring
1164        @param identifier: OpenID selected by user - for ID Select mode only
1165        '''
1166        self.oidResponse = oidRequest.answer(True, identity=identifier)
1167        self._addSRegResponse(oidRequest)
1168        self._addAXResponse(oidRequest)
1169
1170    def _handleCheckIDRequest(self, oidRequest):
1171        """Handle "checkid_immediate" and "checkid_setup" type requests from
1172        Relying Party
1173       
1174        @type oidRequest: openid.server.server.CheckIDRequest
1175        @param oidRequest: OpenID Check ID request
1176        @rtype: basestring
1177        @return: WSGI response
1178        """
1179        log.debug("OpenIDProviderMiddleware._handleCheckIDRequest ...")
1180       
1181        # Save request
1182        self.session[
1183            OpenIDProviderMiddleware.LAST_CHECKID_REQUEST_SESSION_KEYNAME
1184        ] = oidRequest
1185       
1186        self.session.save()
1187       
1188        if self._identityIsAuthorized(oidRequest):
1189           
1190            # User is logged in - check for ID Select type request i.e. the
1191            # user entered their IdP address at the Relying Party and not their
1192            # full OpenID URL.  In this case, the identity they wish to use must
1193            # be confirmed.
1194            if oidRequest.idSelect():
1195                # OpenID identifier must be confirmed
1196                return self.do_decide(self.environ, self.start_response)
1197           
1198            elif self._trustRootIsAuthorized(oidRequest.trust_root):
1199                # User entered their full OpenID URL and they have previously
1200                # approved this Relying Party
1201                try:
1202                    self._createResponse(oidRequest)
1203                   
1204                except (OpenIDProviderMissingRequiredAXAttrs,
1205                        OpenIDProviderMissingAXResponseHandler):
1206                    response = self._render.errorPage(self.environ, 
1207                                                      self.start_response,
1208                        'The site where you wish to signin requires '
1209                        'additional information which this site isn\'t '
1210                        'configured to provide.  Please report this fault to '
1211                        'your site administrator.')
1212                    return response
1213                   
1214                except OpenIDProviderReloginRequired, e:
1215                    response = self._render.errorPage(self.environ, 
1216                                                      self.start_response,
1217                        'An error occurred setting additional parameters '
1218                        'required by the site requesting your ID.  Please '
1219                        'try logging in again.')
1220                    return response
1221                   
1222                except Exception, e:
1223                    log.error("%s type exception raised setting response "
1224                              "following ID Approval: %s", 
1225                              e.__class__.__name__, 
1226                              traceback.format_exc())
1227                    response = self._render.errorPage(self.environ,
1228                                                      self.start_response,
1229                        'An error occurred setting additional parameters '
1230                        'required by the site requesting your ID.  Please '
1231                        'report this fault to your site administrator.')
1232                    return response
1233               
1234                return self._displayResponse(self.oidResponse)
1235            else:
1236                # This OpenID is being used for a login for the first time. 
1237                # Check with the user whether they want to approval the Relying
1238                # Party's request
1239                return self.do_decide(self.environ, self.start_response)
1240               
1241        elif oidRequest.immediate:
1242            oidResponse = oidRequest.answer(False)
1243            return self._displayResponse(oidResponse)       
1244        else:
1245            # User is not logged in
1246           
1247            # Call login and if successful then call decide page to confirm
1248            # user wishes to trust the Relying Party.
1249            response = self.do_login(self.environ,
1250                                     self.start_response,
1251                                     success_to=self.urls['url_decide'])
1252            return response
1253
1254    def _displayResponse(self, oidResponse):
1255        """Serialize an OpenID Response object, set headers and return WSGI
1256        response.
1257       
1258        If the URL length for a GET request exceeds a maximum, then convert the
1259        response into a HTML form and use POST method.
1260       
1261        @type oidResponse: openid.server.server.OpenIDResponse
1262        @param oidResponse: OpenID response object
1263       
1264        @rtype: basestring
1265        @return: WSGI response'''
1266        """     
1267        try:
1268            webresponse = self.oidserver.encodeResponse(oidResponse)
1269        except server.EncodingError, why:
1270            text = why.response.encodeToKVForm()
1271            response = self._render.errorPage(environ, start_response, text)
1272            return response
1273       
1274        hdr = webresponse.headers.items()
1275       
1276        # If the content length exceeds the maximum to represent on a URL, it's
1277        # rendered as a form instead
1278        # FIXME: Commented out oidResponse.renderAsForm() test as it doesn't
1279        # give consistent answers.  Testing based on body content should work
1280        # OK
1281        if webresponse.body:
1282        #if oidResponse.renderAsForm():
1283            # Wrap in HTML with Javascript OnLoad to submit the form
1284            # automatically without user intervention
1285            response = OpenIDProviderMiddleware.formRespWrapperTmpl % \
1286                                                        webresponse.body
1287        else:
1288            response = webresponse.body
1289           
1290        hdr += [('Content-type', 'text/html' + self.charset),
1291                ('Content-length', str(len(response)))]
1292           
1293        self.start_response('%d %s' % (webresponse.code,
1294                                       httplib.responses[webresponse.code]),
1295                            hdr)
1296        return response
1297
1298    def _redirect(self, start_response, url):
1299        """Do a HTTP 302 redirect
1300       
1301        @type start_response: function
1302        @param start_response: WSGI start response callable
1303        @type url: basestring
1304        @param url: URL to redirect to
1305        @rtype: list
1306        @return: empty HTML body
1307        """
1308        start_response('302 %s' % httplib.responses[302],
1309                       [('Content-type', 'text/html' + self.charset),
1310                        ('Location', url)])
1311        return []
1312
1313   
1314class RenderingInterfaceError(Exception):
1315    """Base class for RenderingInterface exceptions
1316   
1317    A standard message is raised set by the msg class variable but the actual
1318    exception details are logged to the error log.  The use of a standard
1319    message enables callers to use its content for user error messages.
1320   
1321    @type msg: basestring
1322    @cvar msg: standard message to be raised for this exception"""
1323    userMsg = ("An internal error occurred with the page layout,  Please "
1324               "contact your system administrator")
1325    errorMsg = "RenderingInterface error"
1326   
1327    def __init__(self, *arg, **kw):
1328        if len(arg) > 0:
1329            msg = arg[0]
1330        else:
1331            msg = self.__class__.errorMsg
1332           
1333        log.error(msg)
1334        Exception.__init__(self, msg, **kw)
1335       
1336       
1337class RenderingInterfaceInitError(RenderingInterfaceError):
1338    """Error with initialisation of RenderingInterface.  Raise from __init__"""
1339    errorMsg = "RenderingInterface initialisation error"
1340   
1341   
1342class RenderingInterfaceConfigError(RenderingInterfaceError):
1343    """Error with configuration settings.  Raise from __init__"""
1344    errorMsg = "RenderingInterface configuration error"   
1345
1346 
1347class RenderingInterface(object):
1348    """Interface class for rendering of OpenID Provider pages.  It implements
1349    methods for handling Yadis requests only.  All other interface methods
1350    return a 404 error response.  Create a derivative from this class to
1351    implement the other rendering methods as required. 
1352    ndg.security.server.wsgi.openid.provider.renderingInterface.demo.DemoRenderingInterface
1353    provides an example of how to do this.  To apply a custom
1354    RenderingInterface class pass it's name in the OpenIDProviderMiddleware
1355    app_conf dict or as a keyword argument using the option name
1356    renderingClass.
1357   
1358    @cvar tmplServerYadis: template for returning Yadis document to Relying
1359    Party.  Derived classes can reset this or completely override the
1360    serverYadis method.
1361   
1362    @type tmplServerYadis: basestring
1363   
1364    @cvar tmplYadis: template for returning Yadis document containing user
1365    URL to Relying Party.  Derived classes can reset this or completely
1366    override the yadis method.
1367   
1368    @type tmplYadis: basestring"""
1369   
1370    # Enable slot support for derived classes if they require it
1371    __slots__ = ('_authN', 'base_url', 'urls', 'charset')
1372   
1373    tmplServerYadis = """\
1374<?xml version="1.0" encoding="UTF-8"?>
1375<xrds:XRDS
1376    xmlns:xrds="xri://$xrds"
1377    xmlns="xri://$xrd*($v*2.0)">
1378  <XRD>
1379
1380    <Service priority="0">
1381      <Type>%(openid20type)s</Type>
1382      <URI>%(endpoint_url)s</URI>
1383    </Service>
1384
1385  </XRD>
1386</xrds:XRDS>
1387"""
1388
1389    tmplYadis = """\
1390<?xml version="1.0" encoding="UTF-8"?>
1391<xrds:XRDS
1392    xmlns:xrds="xri://$xrds"
1393    xmlns="xri://$xrd*($v*2.0)">
1394  <XRD>
1395
1396    <Service priority="0">
1397      <Type>%(openid20type)s</Type>
1398      <Type>%(openid10type)s</Type>
1399      <URI>%(endpoint_url)s</URI>
1400      <LocalID>%(user_url)s</LocalID>
1401    </Service>
1402
1403  </XRD>
1404</xrds:XRDS>"""   
1405   
1406    def __init__(self, authN, base_url, urls, **opt):
1407        """
1408        @type authN: AuthNInterface
1409        @param param: reference to authentication interface to enable OpenID
1410        user URL construction from username
1411        @type base_url: basestring
1412        @param base_url: base URL for OpenID Provider to which individual paths
1413        are appended
1414        @type urls: dict
1415        @param urls: full urls for all the paths used by all the exposed
1416        methods - keyed by method name - see OpenIDProviderMiddleware.paths
1417        @type opt: dict
1418        @param opt: additional custom options passed from the
1419        OpenIDProviderMiddleware config
1420        """
1421        self._authN = authN
1422        self.base_url = base_url
1423        self.urls = urls
1424        self.charset = ''
1425   
1426    def serverYadis(self, environ, start_response):
1427        '''Render Yadis info for ID Select mode request
1428       
1429        @type environ: dict
1430        @param environ: dictionary of environment variables
1431        @type start_response: callable
1432        @param start_response: WSGI start response function.  Should be called
1433        from this method to set the response code and HTTP header content
1434        @rtype: basestring
1435        @return: WSGI response
1436        '''
1437        endpoint_url = self.urls['url_openidserver']
1438        response = RenderingInterface.tmplServerYadis % \
1439                                {'openid20type': discover.OPENID_IDP_2_0_TYPE,
1440                                 'endpoint_url': endpoint_url}
1441             
1442        start_response("200 OK",
1443                       [('Content-type', 'application/xrds+xml'),
1444                        ('Content-length', str(len(response)))])
1445        return response
1446
1447    def yadis(self, environ, start_response):
1448        """Render Yadis document containing user URL
1449       
1450        @type environ: dict
1451        @param environ: dictionary of environment variables
1452        @type start_response: callable
1453        @param start_response: WSGI start response function.  Should be called
1454        from this method to set the response code and HTTP header content
1455        @rtype: basestring
1456        @return: WSGI response
1457        """
1458        # Override this method to implement an alternate means to derive the
1459        # username identifier
1460        userIdentifier = OpenIDProviderMiddleware.parseIdentityURI(
1461                                                    environ['PATH_INFO'])[-1]
1462       
1463        endpoint_url = self.urls['url_openidserver']
1464        user_url = self.urls['url_id'] + '/' + userIdentifier
1465       
1466        yadisDict = dict(openid20type=discover.OPENID_2_0_TYPE,
1467                         openid10type=discover.OPENID_1_0_TYPE,
1468                         endpoint_url=endpoint_url,
1469                         user_url=user_url)
1470       
1471        response = RenderingInterface.tmplYadis % yadisDict
1472     
1473        start_response('200 OK',
1474                       [('Content-type', 'application/xrds+xml' + self.charset),
1475                        ('Content-length', str(len(response)))])
1476        return response
1477   
1478    def identityPage(self, environ, start_response):
1479        """Render the identity page.
1480       
1481        @type environ: dict
1482        @param environ: dictionary of environment variables
1483        @type start_response: callable
1484        @param start_response: WSGI start response function.  Should be called
1485        from this method to set the response code and HTTP header content
1486        @rtype: basestring
1487        @return: WSGI response
1488        """
1489        response = "Page is not implemented"
1490        start_response('%d %s' % (404, httplib.responses[404]),
1491                       [('Content-type', 'text/html' + self.charset),
1492                        ('Content-length', str(len(response)))])
1493        return response
1494         
1495    def login(self, environ, start_response,
1496              success_to=None, fail_to=None, msg=''):
1497        """Render the login form.
1498       
1499        @type environ: dict
1500        @param environ: dictionary of environment variables
1501        @type start_response: callable
1502        @param start_response: WSGI start response function.  Should be called
1503        from this method to set the response code and HTTP header content
1504        @type success_to: basestring
1505        @param success_to: URL put into hidden field telling 
1506        OpenIDProviderMiddleware.do_loginsubmit() where to forward to on
1507        successful login
1508        @type fail_to: basestring
1509        @param fail_to: URL put into hidden field telling 
1510        OpenIDProviderMiddleware.do_loginsubmit() where to forward to on
1511        login error
1512        @type msg: basestring
1513        @param msg: display (error) message below login form e.g. following
1514        previous failed login attempt.
1515        @rtype: basestring
1516        @return: WSGI response
1517        """
1518       
1519        response = "Page is not implemented"
1520        start_response('%d %s' % (404, httplib.responses[404]),
1521                       [('Content-type', 'text/html' + self.charset),
1522                        ('Content-length', str(len(response)))])
1523        return response
1524
1525    def mainPage(self, environ, start_response):
1526        """Rendering the main page.
1527       
1528        @type environ: dict
1529        @param environ: dictionary of environment variables
1530        @type start_response: callable
1531        @param start_response: WSGI start response function.  Should be called
1532        from this method to set the response code and HTTP header content
1533        @rtype: basestring
1534        @return: WSGI response
1535        """   
1536        response = "Page is not implemented"
1537        start_response('%d %s' % (404, httplib.responses[404]),
1538                       [('Content-type', 'text/html' + self.charset),
1539                        ('Content-length', str(len(response)))])
1540        return response
1541
1542    def decidePage(self, environ, start_response, oidRequest, oidResponse):
1543        """Show page giving the user the option to approve the return of their
1544        credentials to the Relying Party.  This page is also displayed for
1545        ID select mode if the user is already logged in at the OpenID Provider.
1546        This enables them to confirm the OpenID to be sent back to the
1547        Relying Party
1548
1549        These fields should be posted by this page ready for
1550        OpenIdProviderMiddleware.do_allow to process:
1551       
1552        'Yes'/'No': for return authentication details back to the RP or
1553        abort return to RP respectively
1554        'remember': remember the decision corresponding to the above 'Yes'
1555        /'No'.
1556        This may be set to 'Yes' or 'No'
1557        'identity': set to the user's identity URL.  This usually is not
1558        required since it can be obtained from oidRequest.identity attribute
1559        but in ID Select mode, the identity URL will have been selected or set
1560        here.
1561       
1562       
1563        @type environ: dict
1564        @param environ: dictionary of environment variables
1565        @type start_response: callable
1566        @param start_response: WSGI start response function.  Should be called
1567        from this method to set the response code and HTTP header content
1568        @type oidRequest: openid.server.server.CheckIDRequest
1569        @param oidRequest: OpenID Check ID Request object
1570        @type oidResponse: openid.server.server.OpenIDResponse
1571        @param oidResponse: OpenID response object
1572        @rtype: basestring
1573        @return: WSGI response
1574        """
1575        response = "Page is not implemented"
1576        start_response('%d %s' % (404, httplib.responses[404]),
1577                       [('Content-type', 'text/html' + self.charset),
1578                        ('Content-length', str(len(response)))])
1579        return response
1580
1581    def errorPage(self, environ, start_response, msg, code=500):
1582        """Display error page
1583       
1584        @type environ: dict
1585        @param environ: dictionary of environment variables
1586        @type start_response: callable
1587        @param start_response: WSGI start response function.  Should be called
1588        from this method to set the response code and HTTP header content
1589        @type msg: basestring
1590        @param msg: optional message for page body
1591        @type code: int
1592        @param code: HTTP Error code to return
1593        @rtype: basestring
1594        @return: WSGI response
1595        """     
1596        response = "Page is not implemented"
1597        start_response('%d %s' % (404, httplib.responses[code]),
1598                       [('Content-type', 'text/html' + self.charset),
1599                        ('Content-length', str(len(response)))])
1600        return response
1601       
Note: See TracBrowser for help on using the repository browser.