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

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