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

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