Welcome, guest | Sign In | My Account | Store | Cart

I was writing a custom Twisted XML-RPC server for radio station DJs to use, but one station was managing all of its internal web app users and groups through Zope. Twisted has an amazingly pluggable authentication framework, so the requirement was satisfied with the following.

Note that in this simple example, every time a user executes an XML-RPC method, they are authenticating against Zope. This involves an xmlrpclib.ServerProxy instantiation as well as the overhead of making a network connection to Zope. For this reason, as well as in the event of Zope being down, one might want to implement some form of caching (without passwords), so that application functionality is not impacted by Zope downtime and overhead from authentication is only incurred when necessary.

radix makes a very good point in the comments: the use of xmlrpclib in this recipe is blocking. This means that if you have 10, 20, whatever number of people using the app, and someone new logs in, the twisted reactor can't do it's usual thing and cycle through requests until that user finishes logging in. I will post an update that makes use of the non-blocking t.w.xmlrpc code. The solution will be messier, though, as t.w.xmlrpc is broken in that it doesn't handle scheme://username:pass@host:port URLs. This is due to the fact that t.w.xmlrpc.QueryProtocol doesn't set an Authentication header... and there's no mechanism in t.w.xmlrpc.Proxy and QueryFactory for parsing and setting user and password into from the URL.

Update: see the comments below for how to work around this limitation.

Python, 34 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import xmlrpclib

from zope.interface import implements

from twisted.internet import defer
from twisted.cred import checkers
from twisted.cred import credentials

class ZopeChecker(object):
    '''
    A Zope cred checker for twisted applications.
    '''
    implements(checkers.ICredentialsChecker)
    credentialInterfaces = (credentials.IUsernamePassword,)

    def __init__(self, host, port=8080, scheme="https", path="/"):
        self.scheme = scheme
        self.host = host
        self.port = port
        self.path = path

    def requestAvatarId(self, c):
        '''
        Please, please for the love of all that is holy to you, 
        make sure that your scheme is 'https'
        '''
        url = '%s://%s:%s@%s:%s%s' % (self.scheme, c.username, c.password, 
            self.host, self.port, self.path)
        server = xmlrpclib.ServerProxy(url)
        info = server.getXMLRPCUserInfo()
        if info.get('authenticated'):
            return defer.succeed(c.username)
        else:
            return defer.fail(error.UnauthorizedLogin())

The idea behind this is to not so much use the internal authentication machinery of Zope, but rather to simply attempt to authenticate in Zope via XML-RPC. The success or failure of that is evaluated and used to determine whether or not you are an authenticated user in the twisted application.

ZopeChecker makes use of the following Zope/Plone python script, named getXMLRPCUserInfo. This needs to be in your Zope/Plone path. <pre> member=context.portal_membership.getAuthenticatedMember() if context.portal_membership.isAnonymousUser(): authenticated = False else: authenticated = True

return { 'name': member.getUserName(), 'id': member.getId(), 'roles': member.getRoles(), 'authenticated': authenticated, } </pre> You could use this from an interactive python session in the following manner: <pre>

import xmlrpclib good = 'joe:blow' bad = 'homer:doh!' url = 'https://%s@your.zopeinstance.com:8080/site01'

>>> server = ServerProxy(url % good)
>>> info = server.getXMLRPCUserInfo()
>>> info.get('authenticated')
True
>>> info.get('roles')
['Authenticated']
>>> info.get('name')
"Joseph O'Blow"

>>> server = ServerProxy(url % badpass)
>>> info = server.getXMLRPCUserInfo()
>>> info.get('authenticated')
False
>>> info.get('roles')
['Anonymous']
>>> info.get('name')
'Anonymous User'

</pre> As for how the Zope checker gets plugged into a Twisted application, here is a .tac file that demonstrates this: <pre> from twisted.web import server from twisted.cred import portal from twisted.application import service, internet from twisted.internet.ssl import DefaultOpenSSLContextFactory

from kxxx.musicdb import auth from kxxx.musicdb.utils import log from kxxx.musicdb.conf import cfg, getResource

authentication setup

checker = auth.ZopeChecker(cfg.user_database.host, port=cfg.user_database.port, scheme=cfg.user_database.scheme, path=cfg.user_database.path) realm = auth.MusicDBAppRealm() authportal = portal.Portal(realm) authportal.registerChecker(checker) authrsrc = auth.BasicAuthResource(authportal)

encryption setup

privkey_file = getResource([cfg.ssl.path, cfg.ssl.private_key]) cert_file = getResource([cfg.ssl.path, cfg.ssl.ca_cert]) ssl_context = DefaultOpenSSLContextFactory(privkey_file, cert_file)

setup the twisted XML-RPC server as an application

application = service.Application(cfg.app_name) xmlrpc_site = server.Site(authrsrc) xmlrpc_server = internet.SSLServer(cfg.port, xmlrpc_site, ssl_context) xmlrpc_server.setServiceParent(application) </pre> Note that cfg is actually a ZConfig instance, with filenames provided by setuptools' pkg_resource, so no configuration values are hard-coded. getResource() is actually a wrapper for pkg_resource.resource_filename() with the added functionality of checking for the resource in question at a configured set of locations on the file system priort to pulling it from the egg.

MusicDBAppRealm is where all the handlers are set up for XLM-RPC methods (so we can do stuff like server.query.getXXX(), server.update.addXXX(), etc.). MusicDBAppRealm instantiates MusicDBAppAvatar() which is a subclass of the XML-RPC application API.

Be sure to read everything here: http://twistedmatrix.com/projects/core/documentation/howto/

The Twisted-Python mail list is an invaluable resource; take advantage of it: http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-python

Also: Abe Fettig gives a great summary of Authentication in the new Twisted book.

But most importantly: read the source, Luke.

3 comments

Christopher Armstrong 15 years, 10 months ago  # | flag

Please, please, please. You really don't want to be using xmlrpclib here. Use twisted.web.xmlrpc.Proxy. xmlrpclib is blocking your entire Twisted process every time you authenticate, and Twisted has a nice alternative already available for you in twisted.web.xmlrpc.

Duncan McGreggor (author) 15 years, 10 months ago  # | flag

twisted.web.xmlrpc. Thanks -- I'll add that to the comments. That's what I've done in production (in addition to implementing a caching solution). I did want to keep the recipe simple, so I left that bit out. I'll make a note in the description for now, but when I clean up and pare down the prod code, I'll update the recipe.

Duncan McGreggor (author) 15 years, 10 months ago  # | flag

Using Deferreds. Okay, I have posted a couple more recipes that should make this as clean as possible. However, since twitsed.web.xmlrpc itself doesn't actually support authentication, I will not modify the recipe, but will post how to work around this here. The code to have a Twisted XML-RPC Proxy which supports Authentication is here:

http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/473865

With that said, here's what you would do to modify this recipe:

from zope.interface import implements

from twisted.internet import defer
from twisted.cred import checkers
from twisted.cred import credentials

from where.you.saved.xmlrpcauth import AuthProxy

class ZopeChecker(object):

    implements(checkers.ICredentialsChecker)
    credentialInterfaces = (credentials.IUsernamePassword,)

    def __init__(self, host, port=8080,scheme="https", path="/"):
        self.scheme = scheme
        self.host = host
        self.port = port
        self.path = path

    def _ebAuthProblem(self, failure):
        # For a real app, use some logging here instead
        print "Doh! There was a problem logging into the Zope server."
        print failure.getErrorMessage())

    def _cbCheckAuth(self, data, username):
        if data.get('authenticated'):
            return username
        return failure.Failure(error.UnauthorizedLogin())

    def requestAvatarId(self, c):
        url = '%s://%s:%s@%s:%s%s' % (self.scheme, c.username, c.password,
            self.host, self.port, self.path)
        server = AuthProxy(url)
        deferred = server.callRemote('getXMLRPCUserInfo')
        deferred.addCallback(self._cbCheckAuth, c.username)
        deferred.addErrback(self._ebAuthProblem)
        return deferred