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

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

Old Pylons SSO code moved to separate branch in trunk

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