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.
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.
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.
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.
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: