Change Your Identity in TurboGears with Entry Points
Posted by Tim Freund Thu, 14 Jun 2007 03:15:00 GMT
Identity
defines who we are. Identity is made up of all the little
distinguishing traits that differentiate one person from another.
We've all changed our identity throughout our lives. We change from
student to graduate, single to married, dogless to dogged, and more,
but that's not what we're talking about today. Identity is the
authentication and authorization framework for TurboGears, and it is
easy to extend.
At the core of the Identity framework is an IdentityProvider. The Identity Provider interfaces with an authentication and authorization repository to determine two things: are you who you say you are, and do you belong where you are trying to go. The framework comes with two providers, one each for SQLObject and SqlAlchemy.
We will customize an IdentityProvider to authenticate against an IMAP server in the few steps that follow. This would be helpful for writing a web mail application, and the concept can be applied to other authentication mechanisms as well, including LDAP, Radius, and others.
Action Plan
- Quickstart a project
- Create an identity provider
- Define an entry point for the identity provider
- Configure the application for the new provider
- Finish the identity provider
- Test
- Relax
The code for this tutorial is available from subversion or as a tar.gz file. It is released under the MIT license. No TurboGears installation? Install it like so.
Step 1: Quickstart a Project
If you don't have an existing TurboGears project to experiment with, now would be a great time to start one.
tim@iris ~/src $ tg-admin quickstart -s -i iddemo ... tim@iris ~/src/ $ cd iddemo tim@iris ~/src/iddemo $ tg-admin sql create
Note the -s and -i flags. This is a
project with support for SqlAlchemy and Identity.
Step 2: Create an Identity Provider
Any object can be an identity provider as long as it supplies the
following methods: validate_identity, validate_password,
load_identity, anonymous_identity, authenticated_identity, but
it isn't always necessary to write one from scratch. Extending an
existing provider often gets an application authenticating as required
without much trouble. We will extend
the SqlAlchemyIdentityProvider in this example to
authenticate against an IMAP server.
iddemo/identity.py
from turbogears.identity.saprovider import SqlAlchemyIdentityProvider
class ImapSqlAlchemyIdentityProvider(SqlAlchemyIdentityProvider):
pass
Step 3: Define an Entry Point
The Identity Framework uses an entry point
named turbogears.identity.provider to decide what class
to use when authenticating users. We are about to define a new option
for this entry point, but further reading on the subject of entry
points is recommended. Scroll to the bottom of this entry for a couple
of relevant links. It's OK, we have the time.
This entry point step won't be necessary in the future, thanks to this patch, but entry points are a powerful tool and worth learning, regardless.
setup.py
setup(
name="iddemo",
... (more setup stuff) ...
entry_points="""
[turbogears.identity.provider]
imapsa = iddemo.identity:ImapSqlAlchemyIdentityProvider
""",
... (more setup stuff) ...
)
To let the setuptools system know about this new identity provider, run the following:
tim@iris ~/src/iddemo $ python setup.py develop
Step 4: Configure the Application
With our imapsa option defined for
the turbogears.identity.provider entry point, we can now
configure the application to call the new provider. There is a value
named identity.provider in app.cfg. We will replace the
existing value with imapsa. While app.cfg open is,
add the other three lines in the following example. They will be
explained in the next step.
iddemo/config/app.cfg
... identity.provider='imapsa' identity.imapprovider.imap_authoritative=True identity.imapprovider.server="localhost" identity.imapprovider.port=143 ...
The application is now ready to run with the new identity provider. Restart the application if it is currently running so that the configuration change will take effect.
Step 5: Finish the Identity Provider
Now let's dig in and implement the new authentication behavior.
iddemo/identity.py
...
class ImapSqlAlchemyIdentityProvider(SqlAlchemyIdentityProvider):
def __init__(self):
SqlAlchemyIdentityProvider.__init__(self)
# These three lines get the configuration parameters we set in app.cfg
self.imap_authoritative = get("identity.imapprovider.imap_authoritative", False)
self.server = get("identity.imapprovider.server", "localhost")
self.port = get("identity.imapprovider.port", 143)
# These four lines make the user and visit classes available for
# later use
user_class_path = get("identity.saprovider.model.user", None)
self.user_class = load_class(user_class_path)
visit_class_path = get("identity.saprovider.model.visit", None)
self.visit_class = load_class(visit_class_path)
def validate_identity(self, user_name, password, visit_key):
if self.validate_password(None, user_name, password):
user = session.query(self.user_class).get_by(user_name=user_name)
if not user:
if self.imap_authoritative:
user = self.user_class()
user.user_name = user_name
user.save()
session.flush()
else:
return None
link = session.query(self.visit_class).get_by(visit_key=visit_key)
if not link:
link = self.visit_class(visit_key=visit_key, user_id=user.user_id)
session.save(link)
else:
link.user_id = user.user_id
session.flush()
return SqlAlchemyIdentity(visit_key, user)
return None
def validate_password(self, user, user_name, password):
rc = False
try:
imapcon = imaplib.IMAP4(self.server, self.port)
except:
log.error("Could not establish connection to server at %s:%d" % (self.server, self.port))
return rc
try:
if imapcon.login(user_name, password)[0] == 'OK':
rc = True
except:
# Probably threw an error for invalid username/password
log.info("Passwords don't match for user: %s", user_name)
imapcon.shutdown()
return rc
Take a look at each method to figure out what they accomplish.
__init__ invokes the constructor of the
SqlAlchemyIdentityProvider and then collects the configuration
parameters required for authentication.
validate_identity first
invokes validate_password. If the password validate
succeeds, the user is selected from the database. If the user does
not exist in the database, the provider will create the user when
the imap_authoritative option is set. Should you not
require this capability, you can remove this method entirely and rely
upon the implementation in SqlAlchemyIdentityProvider.
Finally, this method links the user with the
current visit_key.
validate_password handles all IMAP access. It is the
perfect method to override if all you need to do is change the
identity authentication mechanism.
Step 6: Test
Open controllers.py and change the identity decorator to require an authenticated user when accessing the index page.
iddemo/controllers.py
class Root(controllers.RootController):
@expose(template="iddemo.templates.welcome")
@identity.require(identity.not_anonymous())
def index(self):
import time
flash("Your application is now running")
return dict(now=time.ctime())
The moment of truth has arrived. Point a browser at the application. You should see a login page, and a valid IMAP username/password should provide access to the application.
Step 7: Relax
We're done here. Go tell your friends and family how cool you are.
Any feedback is appreciated. Leave a comment here, post a response on your own blog, or send an email to tim -at- achievewith -dot- us. You can also usually find me lurking in #turbogears on irc.freenode.net as timphnode.
For more information, follow these links:
This is very helpful. I used your instructions with those that I found for ldap authentication on the TurboGears website to make a SA ldap login. I'm using the version 1.0.4b1 and there were some problems that needed fixing, such as in "validate_identity" instead of: "user.save()" use: "session.save(user)"