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

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@5782
Revision 5782, 66.7 KB checked in by pjkersha, 10 years ago (diff)

Re-testing OpenID Attribute Exchange interface - added CSV file based test AX Response class.

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