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

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

Incomplete - task 12: ESG Yadis identity service discovery

  • Altered Python OpenID Provider adding support for custom XRDS document via Gensho templating. This enables service endpoints other than the default OpenID one to be advertised by the Yadis doc incl. the Attribute Service which is now required for ESG.
  • 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(environ, 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(environ, start_response, text)
1331            return response
1332       
1333        hdr = webresponse.headers.items()
1334       
1335        # If the content length exceeds the maximum to represent on a URL, it's
1336        # rendered as a form instead
1337        # FIXME: Commented out oidResponse.renderAsForm() test as it doesn't
1338        # give consistent answers.  Testing based on body content should work
1339        # OK
1340        if webresponse.body:
1341        #if oidResponse.renderAsForm():
1342            # Wrap in HTML with Javascript OnLoad to submit the form
1343            # automatically without user intervention
1344            response = OpenIDProviderMiddleware.FORM_RESP_WRAPPER_TMPL % \
1345                                                        webresponse.body
1346        else:
1347            response = webresponse.body
1348           
1349        hdr += [('Content-type', 'text/html' + self.charset),
1350                ('Content-length', str(len(response)))]
1351       
1352        log.debug("Sending response to Relying Party:\n\nheader=%r\nbody=%r",
1353                  hdr, response)
1354           
1355        self.start_response('%d %s' % (webresponse.code,
1356                                       httplib.responses[webresponse.code]),
1357                            hdr)
1358        return response
1359
1360    def _redirect(self, start_response, url):
1361        """Do a HTTP 302 redirect
1362       
1363        @type start_response: function
1364        @param start_response: WSGI start response callable
1365        @type url: basestring
1366        @param url: URL to redirect to
1367        @rtype: list
1368        @return: empty HTML body
1369        """
1370        start_response('302 %s' % httplib.responses[302],
1371                       [('Content-type', 'text/html' + self.charset),
1372                        ('Location', url)])
1373        return []
1374
1375    def getBase_url(self):
1376        return self.__base_url
1377
1378    def getUrls(self):
1379        return self.__urls
1380   
1381    def setUrls(self, value):
1382        if not isinstance(value, dict):
1383            raise TypeError('Expecting dict type for '
1384                            '"urls" attribute; got %r' %
1385                            type(value))
1386        self.__urls = value
1387
1388    urls = property(getUrls, setUrls, None, "dictionary of URLs for app")
1389
1390    def getCharset(self):
1391        return self.__charset
1392   
1393    def setCharset(self, value):       
1394        # Convert from string type where required   
1395        if not value:
1396            self.__charset = ''
1397        elif isinstance(value, basestring):
1398            self.__charset = '; charset=' + value
1399        else:
1400            raise TypeError('Expecting string type for "charset" attribute; '
1401                            'got %r' % type(value))
1402   
1403    charset = property(getCharset, setCharset, None, "Charset's Docstring")
1404   
1405    def getPaths(self):
1406        return self.__paths
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    paths = property(getPaths, setPaths, None, 
1416                     "Dictionary of Paths for the app")
1417   
1418    def setBase_url(self, value):
1419        if not isinstance(value, basestring):
1420            raise TypeError('Expecting string type for '
1421                            '"base_url" attribute; got %r' %
1422                            type(value))
1423        self.__base_url = value
1424
1425    base_url = property(getBase_url, setBase_url, None, "Base URL for the app")
1426       
1427    def getMethod(self):
1428        return self.__method
1429
1430    def setMethod(self, value):
1431        if not isinstance(value, dict):
1432            raise TypeError('Expecting dict type for '
1433                            '"method" attribute; got %r' %
1434                            type(value))
1435        self.__method = value
1436
1437    method = property(getMethod, setMethod, None, 
1438                      "Method name keyed from requested URL")
1439   
1440    def getSessionMiddlewareEnvironKeyName(self):
1441        return self.__sessionMiddlewareEnvironKeyName
1442
1443    def setSessionMiddlewareEnvironKeyName(self, value):
1444        if not isinstance(value, basestring):
1445            raise TypeError('Expecting string type for '
1446                            '"sessionMiddlewareEnvironKeyName" attribute; '
1447                            'got %r' %
1448                            type(value))
1449        self.__sessionMiddlewareEnvironKeyName = value
1450
1451    sessionMiddlewareEnvironKeyName = property(
1452                                  getSessionMiddlewareEnvironKeyName, 
1453                                  setSessionMiddlewareEnvironKeyName, 
1454                                  None, 
1455                                  "environ key name for Beaker Session "
1456                                  "middleware")
1457    def getSession(self):
1458        return self.__session
1459
1460    def setSession(self, value):
1461        if not isinstance(value, beaker.session.SessionObject):
1462            raise TypeError('Expecting beaker.session.SessionObject type for '
1463                            '"session" attribute; got %r' %
1464                            type(value))
1465
1466        self.__session = value
1467
1468    session = property(getSession, setSession, None, "Session's Docstring")
1469
1470    def getOidResponse(self):
1471        return self.__session.get(
1472                        OpenIDProviderMiddleware.OID_RESPONSE_SESSION_KEYNAME)
1473
1474    def setOidResponse(self, value):
1475        """Set property method - oidResponse property is a wrapper to the
1476        session oidResponse dictionary item
1477        """
1478        if not isinstance(value, server.OpenIDResponse):
1479            raise TypeError('Expecting OpenIDResponse type for '
1480                            '"oidResponse" attribute; got %r' %
1481                            type(value))
1482           
1483        self.__session[OpenIDProviderMiddleware.OID_RESPONSE_SESSION_KEYNAME
1484                       ] = value
1485                       
1486        self.__session.save()
1487                       
1488    oidResponse = property(getOidResponse, 
1489                           setOidResponse, 
1490                           None, 
1491                           "OpenID response object")
1492   
1493    def getSregResponse(self):
1494        return self.__sregResponse
1495
1496    def setSregResponse(self, value):
1497        self.__sregResponse = value
1498
1499    sregResponse = property(getSregResponse, 
1500                            setSregResponse, 
1501                            None, 
1502                            "SReg response handler class")
1503           
1504    def getAxResponse(self):
1505        return self.__axResponse
1506
1507    def setAxResponse(self, value):
1508        if not isinstance(value, AXInterface):
1509            raise TypeError('Expecting AXInterface type for '
1510                            '"axResponse" attribute; got %r' %
1511                            type(value))
1512        self.__axResponse = value
1513       
1514    def getOidserver(self):
1515        return self.__oidserver
1516
1517    def setOidserver(self, value):
1518        if not isinstance(value, server.Server):
1519            raise TypeError('Expecting openid.server.server.Server type for '
1520                            '"oidserver" attribute; got %r' %
1521                            type(value))
1522        self.__oidserver = value
1523
1524    oidserver = property(getOidserver, setOidserver, None, 
1525                         "OpenID server instance")
1526           
1527    def getQuery(self):
1528        return self.__query
1529
1530    def setQuery(self, value):
1531        if not isinstance(value, dict):
1532            raise TypeError('Expecting dict type for '
1533                            '"query" attribute; got %r' %
1534                            type(value))
1535        self.__query = value
1536
1537    query = property(getQuery, setQuery, None, 
1538                     "dictionary of HTML query parameters")
1539
1540    def getTrustedRelyingParties(self):
1541        return self.__trustedRelyingParties
1542
1543    def setTrustedRelyingParties(self, value):
1544        if isinstance(value, basestring):
1545            pat = OpenIDProviderMiddleware.TRUSTED_RELYINGPARTIES_SEP_PAT
1546            self.__trustedRelyingParties = tuple([i for i in pat.split(value)])
1547           
1548        elif isinstance(value, (list, tuple)):
1549            self.__trustedRelyingParties = tuple(value)
1550        else:
1551            raise TypeError('Expecting list or tuple type for '
1552                            '"trustedRelyingParties" attribute; got %r' %
1553                            type(value))
1554
1555    trustedRelyingParties = property(getTrustedRelyingParties, 
1556                                     setTrustedRelyingParties, 
1557                                     None, 
1558                                     "Relying Parties trusted by this Provider")
1559
1560    axResponse = property(getAxResponse, setAxResponse, None, 
1561                          "Attribute Exchange response object")
1562
1563
1564   
1565class RenderingInterfaceError(Exception):
1566    """Base class for RenderingInterface exceptions
1567   
1568    A standard message is raised set by the msg class variable but the actual
1569    exception details are logged to the error log.  The use of a standard
1570    message enables callers to use its content for user error messages.
1571   
1572    @type msg: basestring
1573    @cvar msg: standard message to be raised for this exception"""
1574    userMsg = ("An internal error occurred with the page layout,  Please "
1575               "contact your system administrator")
1576    errorMsg = "RenderingInterface error"
1577   
1578    def __init__(self, *arg, **kw):
1579        if len(arg) > 0:
1580            msg = arg[0]
1581        else:
1582            msg = self.__class__.errorMsg
1583           
1584        log.error(msg)
1585        Exception.__init__(self, msg, **kw)
1586       
1587       
1588class RenderingInterfaceInitError(RenderingInterfaceError):
1589    """Error with initialisation of RenderingInterface.  Raise from __init__"""
1590    errorMsg = "RenderingInterface initialisation error"
1591   
1592   
1593class RenderingInterfaceConfigError(RenderingInterfaceError):
1594    """Error with configuration settings.  Raise from __init__"""
1595    errorMsg = "RenderingInterface configuration error"   
1596
1597 
1598class RenderingInterface(object):
1599    """Interface class for rendering of OpenID Provider pages.  It implements
1600    methods for handling Yadis requests only.  All other interface methods
1601    return a 404 error response.  Create a derivative from this class to
1602    implement the other rendering methods as required. 
1603    ndg.security.server.wsgi.openid.provider.renderingInterface.demo.DemoRenderingInterface
1604    provides an example of how to do this.  To apply a custom
1605    RenderingInterface class pass it's name in the OpenIDProviderMiddleware
1606    app_conf dict or as a keyword argument using the option name
1607    renderingClass.
1608   
1609    @cvar tmplServerYadis: template for returning Yadis document to Relying
1610    Party.  Derived classes can reset this or completely override the
1611    serverYadis method.
1612   
1613    @type tmplServerYadis: basestring
1614   
1615    @cvar tmplYadis: template for returning Yadis document containing user
1616    URL to Relying Party.  Derived classes can reset this or completely
1617    override the yadis method.
1618   
1619    @type tmplYadis: basestring"""
1620   
1621    # Enable slot support for derived classes if they require it
1622    __slots__ = ('_authN', 'base_url', 'urls', 'charset')
1623   
1624    tmplServerYadis = """\
1625<?xml version="1.0" encoding="UTF-8"?>
1626<xrds:XRDS
1627    xmlns:xrds="xri://$xrds"
1628    xmlns="xri://$xrd*($v*2.0)">
1629  <XRD>
1630    <Service priority="0">
1631      <Type>%(openid20type)s</Type>
1632      <URI>%(endpoint_url)s</URI>
1633    </Service>
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    <Service priority="0">
1645      <Type>%(openid20type)s</Type>
1646      <Type>%(openid10type)s</Type>
1647      <URI>%(endpoint_url)s</URI>
1648      <LocalID>%(user_url)s</LocalID>
1649    </Service>
1650  </XRD>
1651</xrds:XRDS>"""   
1652   
1653    def __init__(self, authN, base_url, urls, **opt):
1654        """
1655        @type authN: AuthNInterface
1656        @param param: reference to authentication interface to enable OpenID
1657        user URL construction from username
1658        @type base_url: basestring
1659        @param base_url: base URL for OpenID Provider to which individual paths
1660        are appended
1661        @type urls: dict
1662        @param urls: full urls for all the paths used by all the exposed
1663        methods - keyed by method name - see OpenIDProviderMiddleware.paths
1664        @type opt: dict
1665        @param opt: additional custom options passed from the
1666        OpenIDProviderMiddleware config
1667        """
1668        self._authN = authN
1669        self.base_url = base_url
1670        self.urls = urls
1671        self.charset = ''
1672   
1673    def serverYadis(self, environ, start_response):
1674        '''Render Yadis info for ID Select mode request
1675       
1676        @type environ: dict
1677        @param environ: dictionary of environment variables
1678        @type start_response: callable
1679        @param start_response: WSGI start response function.  Should be called
1680        from this method to set the response code and HTTP header content
1681        @rtype: basestring
1682        @return: WSGI response
1683        '''
1684        endpoint_url = self.urls['url_openidserver']
1685        response = RenderingInterface.tmplServerYadis % \
1686                                {'openid20type': discover.OPENID_IDP_2_0_TYPE,
1687                                 'endpoint_url': endpoint_url}
1688             
1689        start_response("200 OK",
1690                       [('Content-type', 'application/xrds+xml'),
1691                        ('Content-length', str(len(response)))])
1692        return response
1693
1694    def yadis(self, environ, start_response):
1695        """Render Yadis document containing user URL
1696       
1697        @type environ: dict
1698        @param environ: dictionary of environment variables
1699        @type start_response: callable
1700        @param start_response: WSGI start response function.  Should be called
1701        from this method to set the response code and HTTP header content
1702        @rtype: basestring
1703        @return: WSGI response
1704        """
1705        # Override this method to implement an alternate means to derive the
1706        # username identifier
1707        userIdentifier = OpenIDProviderMiddleware.parseIdentityURI(
1708                                                    environ['PATH_INFO'])[-1]
1709       
1710        endpoint_url = self.urls['url_openidserver']
1711        user_url = self.urls['url_id'] + '/' + userIdentifier
1712       
1713        yadisDict = dict(openid20type=discover.OPENID_2_0_TYPE,
1714                         openid10type=discover.OPENID_1_0_TYPE,
1715                         endpoint_url=endpoint_url,
1716                         user_url=user_url)
1717       
1718        response = RenderingInterface.tmplYadis % yadisDict
1719     
1720        start_response('200 OK',
1721                       [('Content-type', 'application/xrds+xml' + self.charset),
1722                        ('Content-length', str(len(response)))])
1723        return response
1724   
1725    def identityPage(self, environ, start_response):
1726        """Render the identity page.
1727       
1728        @type environ: dict
1729        @param environ: dictionary of environment variables
1730        @type start_response: callable
1731        @param start_response: WSGI start response function.  Should be called
1732        from this method to set the response code and HTTP header content
1733        @rtype: basestring
1734        @return: WSGI response
1735        """
1736        response = "Page is not implemented"
1737        start_response('%d %s' % (404, httplib.responses[404]),
1738                       [('Content-type', 'text/html' + self.charset),
1739                        ('Content-length', str(len(response)))])
1740        return response
1741         
1742    def login(self, environ, start_response,
1743              success_to=None, fail_to=None, msg=''):
1744        """Render the login form.
1745       
1746        @type environ: dict
1747        @param environ: dictionary of environment variables
1748        @type start_response: callable
1749        @param start_response: WSGI start response function.  Should be called
1750        from this method to set the response code and HTTP header content
1751        @type success_to: basestring
1752        @param success_to: URL put into hidden field telling 
1753        OpenIDProviderMiddleware.do_loginsubmit() where to forward to on
1754        successful login
1755        @type fail_to: basestring
1756        @param fail_to: URL put into hidden field telling 
1757        OpenIDProviderMiddleware.do_loginsubmit() where to forward to on
1758        login error
1759        @type msg: basestring
1760        @param msg: display (error) message below login form e.g. following
1761        previous failed login attempt.
1762        @rtype: basestring
1763        @return: WSGI response
1764        """
1765       
1766        response = "Page is not implemented"
1767        start_response('%d %s' % (404, httplib.responses[404]),
1768                       [('Content-type', 'text/html' + self.charset),
1769                        ('Content-length', str(len(response)))])
1770        return response
1771
1772    def mainPage(self, environ, start_response):
1773        """Rendering the main page.
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        @rtype: basestring
1781        @return: WSGI response
1782        """   
1783        response = "Page is not implemented"
1784        start_response('%d %s' % (404, httplib.responses[404]),
1785                       [('Content-type', 'text/html' + self.charset),
1786                        ('Content-length', str(len(response)))])
1787        return response
1788
1789    def decidePage(self, environ, start_response, oidRequest, oidResponse):
1790        """Show page giving the user the option to approve the return of their
1791        credentials to the Relying Party.  This page is also displayed for
1792        ID select mode if the user is already logged in at the OpenID Provider.
1793        This enables them to confirm the OpenID to be sent back to the
1794        Relying Party
1795
1796        These fields should be posted by this page ready for
1797        OpenIdProviderMiddleware.do_allow to process:
1798       
1799        'Yes'/'No': for return authentication details back to the RP or
1800        abort return to RP respectively
1801        'remember': remember the decision corresponding to the above 'Yes'
1802        /'No'.
1803        This may be set to 'Yes' or 'No'
1804        'identity': set to the user's identity URL.  This usually is not
1805        required since it can be obtained from oidRequest.identity attribute
1806        but in ID Select mode, the identity URL will have been selected or set
1807        here.
1808       
1809       
1810        @type environ: dict
1811        @param environ: dictionary of environment variables
1812        @type start_response: callable
1813        @param start_response: WSGI start response function.  Should be called
1814        from this method to set the response code and HTTP header content
1815        @type oidRequest: openid.server.server.CheckIDRequest
1816        @param oidRequest: OpenID Check ID Request object
1817        @type oidResponse: openid.server.server.OpenIDResponse
1818        @param oidResponse: OpenID response object
1819        @rtype: basestring
1820        @return: WSGI response
1821        """
1822        response = "Page is not implemented"
1823        start_response('%d %s' % (404, httplib.responses[404]),
1824                       [('Content-type', 'text/html' + self.charset),
1825                        ('Content-length', str(len(response)))])
1826        return response
1827
1828    def errorPage(self, environ, start_response, msg, code=500):
1829        """Display error page
1830       
1831        @type environ: dict
1832        @param environ: dictionary of environment variables
1833        @type start_response: callable
1834        @param start_response: WSGI start response function.  Should be called
1835        from this method to set the response code and HTTP header content
1836        @type msg: basestring
1837        @param msg: optional message for page body
1838        @type code: int
1839        @param code: HTTP Error code to return
1840        @rtype: basestring
1841        @return: WSGI response
1842        """     
1843        response = "Page is not implemented"
1844        start_response('%d %s' % (404, httplib.responses[code]),
1845                       [('Content-type', 'text/html' + self.charset),
1846                        ('Content-length', str(len(response)))])
1847        return response
1848       
Note: See TracBrowser for help on using the repository browser.