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

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

Tested Policy with regex target URIs

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