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

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