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

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