Representing Users with Different Capabilities
It can often be useful to authenticate against an external service. You can avoid making users maintain an additional username and password by using a credentials checker that tries to log into a POP mail server, a web application, or some other service where you know your users will already have accounts. However, this approach creates a potential problem. Your realm is going to be asked for avatars after users have been authenticated by the credentials checker. But since the credentials checker is using an external service, rather than a local data source, for authentication, the realm may not have any information about the avatars it's asked for. While you might be able to spontaneously create accounts based on the avatar ID, you will frequently need additional information before users can start using their accounts.
You can handle this scenario by taking advantage of the way twisted.cred supports multiple avatar interfaces. Your realm can use different types of avatars to represent users with different capabilities . One interface can identify users from whom you need additional information, and another interface can identify users who have already provided the necessary information and are ready to use your application.
6.3.1. How Do I Do That?
Write a class implementing portal.IRealm that accepts two different avatar interfaces in requestAvatar. Use one avatar interface for users who need to supply additional information before they can start using your service. This interface should provide a way for users to submit the required information. Use a second avatar interface to provide the normal user actions and data. Example 6-3 demonstrates this technique.
Example 6-3. multiavatar.py
from twisted.cred import portal, checkers, credentials, error as credError from twisted.protocols import basic from twisted.internet import protocol, reactor, defer from zope.interface import Interface, implements class INamedUserAvatar(Interface): "should have attributes username and fullname" class NamedUserAvatar: implements(INamedUserAvatar) def _ _init_ _(self, username, fullname): self.username = username self.fullname = fullname class INewUserAvatar(Interface): "should have username attribute only" def setName(self, fullname): raise NotImplementedError class NewUserAvatar: implements(INewUserAvatar) def _ _init_ _(self, username, userDb): self.username = username self.userDb = userDb def setName(self, fullname): self.userDb[self.username] = fullname return NamedUserAvatar(self.username, fullname) class MultiAvatarRealm: implements(portal.IRealm) def _ _init_ _(self, users): self.users = users def requestAvatar(self, avatarId, mind, *interfaces): logout = lambda: None if INamedUserAvatar in interfaces and self.users.has_key(avatarId): fullname = self.users[avatarId] return (INamedUserAvatar, NamedUserAvatar(avatarId, fullname), logout) elif INewUserAvatar in interfaces: avatar = NewUserAvatar(avatarId, self.users) return (INewUserAvatar, avatar, logout) else: raise KeyError("None of the requested interfaces is supported") class PasswordDictChecker(object): implements(checkers.ICredentialsChecker) credentialInterfaces = (credentials.IUsernamePassword,) def _ _init_ _(self, passwords): "passwords: a dict-like object mapping usernames to passwords" self.passwords = passwords def requestAvatarId(self, credentials): username = credentials.username if self.passwords.has_key(username): if credentials.password == self.passwords[username]: return defer.succeed(username) else: return defer.fail( credError.UnauthorizedLogin("Bad password")) else: return defer.fail( credError.UnauthorizedLogin("No such user")) class NamedUserLoginProtocol(basic.LineReceiver): def lineReceived(self, line): cmd = getattr(self, 'handle_' + self.currentCommand) cmd(line.strip( )) def connectionMade(self): self.transport.write("User Name: ") self.currentCommand = 'user' def handle_user(self, username): self.username = username self.transport.write("Password: ") self.currentCommand = 'pass' def handle_pass(self, password): creds = credentials.UsernamePassword(self.username, password) avatarInterfaces = (INamedUserAvatar, INewUserAvatar) self.factory.portal.login(creds, None, *avatarInterfaces).addCallback( self._loginSucceeded).addErrback( self._loginFailed) def _loginSucceeded(self, avatarInfo): avatar, avatarInterface, self.logout = avatarInfo if avatarInterface == INewUserAvatar: self.transport.write("What's your full name? ") self.currentCommand = "fullname" self.avatar = avatar else: self._gotNamedUser(avatar) def handle_fullname(self, fullname): namedUserAvatar = self.avatar.setName(fullname) self._gotNamedUser(namedUserAvatar) def _gotNamedUser(self, avatar): self.transport.write("Welcome %s! " % avatar.fullname) defer.maybeDeferred(self.logout).addBoth(self._logoutFinished) def _logoutFinished(self, result): self.transport.loseConnection( ) def _loginFailed(self, failure): self.transport.write("Denied: %s. " % failure.getErrorMessage( )) self.transport.loseConnection( ) class NamedUserLoginFactory(protocol.ServerFactory): protocol = NamedUserLoginProtocol def _ _init_ _(self, portal): self.portal = portal users = { 'admin': 'Admin User', } passwords = { 'admin': 'aaa', 'user1': 'bbb', 'user2': 'ccc' } portal = portal.Portal(MultiAvatarRealm(users)) portal.registerChecker(PasswordDictChecker(passwords)) factory = NamedUserLoginFactory(portal) reactor.listenTCP(2323, factory) reactor.run( )
When you run multiavatar.py, it will start up a server on port 2323. This server uses the same simple protocol as the previous examples in this chapter, but with one addition: if you log in as a user who it hasn't seen before, it will ask you for your full name. This information won't be required for subsequent logins, as it will remember the name you entered the first time:
$ telnet localhost 2323 Trying 127.0.0.1... Connected to sparky. Escape character is '^]'. User Name: user1 Password: bbb What's your full name? Abe Fettig Welcome Abe Fettig! Connection closed by foreign host. $ telnet localhost 2323 Trying 127.0.0.1... Connected to sparky. Escape character is '^]'. User Name: user1 Password: bbb Welcome Abe Fettig! Connection closed by foreign host.
6.3.2. How Does That Work?
The server protocol and realm in Example 6-3 are both aware that this realm uses two different types of avatar. When the MultiAvatarRealm.requestAvatar method is called, it checks to see whether the avatar is listed in self.users, the dictionary of users it knows about. If it finds the user information, it returns a tuple containing INamedUserAvatar, a NamedUserAvatar object for that user, and a logout function. So far, this is identical to the behavior of TestRealm in Example 6-1 at the beginning of this chapter.
If requestAvatar is called with an unknown avatar ID, though, MultiAvatarRealm returns a different type of avatar. The INewUserAvatar interface and NewUserAvatar class represent the actions available to users from whom the server needs more information. Unlike the regular NamedUserAvatar object, NewUserAvatar doesn't provide a fullname attribute: the server doesn't know this user's name. Instead it provides the setName method, providing a way to store the user's name. As a convenience, setName returns a NamedUserAvatar. This step makes it possible to take a NewUserAvatar object and upgrade to a NamedUserAvatar by supplying the user's full name.
The NamedUserProtocol in Example 6-3 calls portal.login with the user's credentials and two interface arguments, INamedUserAvatar and INewUserAvatar. When it gets the results, it checks the first item in the tuple to see which interface is being used. If it is INewUserAvatar, the server asks the client for his full name, and calls the avatar's setName method. In this case, that's all the information that was missing. The NewUserAvatar stores the user's name for future use, and the server can proceed with the rest of the session (brief as it is).