source: TI12-security/branches/ndg-security-1.5.x/ndg_security_server/ndg/security/server/wsgi/openid/provider/__init__.py @ 7119

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/branches/ndg-security-1.5.x/ndg_security_server/ndg/security/server/wsgi/openid/provider/__init__.py@7119
Revision 7119, 76.9 KB checked in by pjkersha, 10 years ago (diff)

Incomplete - task 10: OpenID Provider HTML/Javascript response incompatible with OpenID4Java

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