Identity Management for TurboGears

I just committed the code for the TurboGears identity management support (revision 89). And because this is such new code, I thought it might be helpful to include a short How To for getting everything up and running.

This How To is written from the perspective of a fresh quick-started project, but most everything applies for existing projects. 1. Create new project (idtest). Set dburi.

2. Edit idtest.egg-info/sqlobject.txt

    db_module=idtest.model, turbogears.identity.model.somodel

3. Create login.kid

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml"
        xmlns:py="http://purl.org/kid/ns#"
        py:extends="'master.kid'">

    <head>
        <meta content="text/html; charset=UTF-8"
            http-equiv="content-type" py:replace="''"/>
        <title>Login to TurboGears</title>
    </head>

    <body>
        <h2>Login</h2>
        <p>${message}</p>
        <form action="${previous_url}" method="POST">
            <label for="user_name">User Name:</label>
            <input type="text" id="user_name" name="user_name"/><br/>

            <label for="password">Password:</label>
            <input type="password" id="password" name="password"/><br/>

            <input type="submit" value="Login"/>
        </form>
    </body>
    </html>

4. Create secured.kid

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml"
        xmlns:py="http://purl.org/kid/ns#"
        py:extends="'master.kid'">

    <head>
        <meta content="text/html; charset=UTF-8"
            http-equiv="content-type" py:replace="''"/>
        <title>Welcome to Secured TurboGears</title>
    </head>

    <body>
        <h2>Secure!</h2>
        <p>This page is secured.</p>
    </body>
    </html>

5. Modify controllers.py

    from turbogears import identity
    import cherrypy

    @turbogears.expose( html="idtest.templates.login" )
    def login( self, *args, **kw ):
        if hasattr(cherrypy.request,"identity_exception"):
            msg= str(cherrypy.request.identity_exception)
        else:
            msg= "Please log in"
        cherrypy.response.status=403
        return dict( message=msg, previous_url=cherrypy.request.path )

    @turbogears.expose( html="idtest.templates.secured" )
    @identity.require( group="admin" )
    def secured( self ):
        return dict()

6. Turn on Identity management and configure failure url in dev.cfg

    [global]
    identity.on=True
    identity.failure_url="/login"

7. Create the database

    tg-admin sql create

8. Create a user and group

    tg-admin shell

    Python 2.4.1 (#2, Mar 31 2005, 00:05:10) 
    [GCC 3.3 20030304 (Apple Computer, Inc. build 1666)] on darwin
    Type "help", "copyright", "credits" or "license" for more information.
    (InteractiveConsole)
    >>> from turbogears.identity.model.somodel import *
    >>> hub.begin()
    >>> u=User( userId="jeff", emailAddress="jeff@metrocat.org",
                displayName="Jeff Watkins", password="xxxxx" )
    >>> g=Group( groupId="admin", displayName="Administrators" )
    >>> hub.commit()
    >>>

9. Start project and visit secured page and login. Should fail with message:

    Not member of group: admin

10. Add user to admin group

    tg-admin shell

    Python 2.4.1 (#2, Mar 31 2005, 00:05:10) 
    [GCC 3.3 20030304 (Apple Computer, Inc. build 1666)] on darwin
    Type "help", "copyright", "credits" or "license" for more information.
    (InteractiveConsole)
    >>> from turbogears.identity.model.somodel import *
    >>> hub.begin()
    >>> u=User.get(1)
    >>> g=Group.get(1)
    >>> u.addGroup(g)
    >>> hub.commit()
    >>>

11. Revisit secured page and login. Should succeed.

Update: There is now a sample identity project which demonstrates this and some of the latest features of the identity framework. It’s actually the project I use to test features of the framework.

Comments

Very Interesting October 24th, 2005 @ 1:27 pm

I am starting to work with turbogears and I am missing this feature to implement what I want.
This seems to be very well detailed tank you for your effort to formalize this how to. However I am still missing something how can I add this to my existing sqlite database. I think that I need to add something to my model.py and then run again tg-admin sql create. If my assumption is correct could you help me to find what exactly is the something?
Thank you

Christopher Kelly October 25th, 2005 @ 4:29 pm

Very nice, thank you.

To preserve the session state after login I modified your login function from:

return dict( message=msg, previous_url=cherrypy.request.path )

To:

return dict( message = msg, previous_url = turbogears.url( cherrypy.request.path, cherrypy.request.paramMap ) )

Jeff Watkins October 25th, 2005 @ 6:37 pm

Christopher, I’m glad that worked for you. I’m planning to update the How To with a better login form that does exactly what you suggested. I assume you use a py:for attribute on the template side of things?

I confess I don’t know Kid very well (and don’t like it much either).

Jeff Watkins October 25th, 2005 @ 6:38 pm

Unfortunately, I’m not exactly certain what must be done to add the model to your existing database. You could try running the sql-admin commands directly. I know you can specify a module to sql-admin and it will create tables for the classes the module contains.

Christopher Kelly October 25th, 2005 @ 10:02 pm

I assume you use a py:for attribute on the template side of things?

No, I left the login form unchanged. It was a GET request that triggered the login prompt, so just setting previous_url correctly key did the trick.

Question: how do I retrieve the userId in my application code? I can see the cookie parsing code in identity_from_cookie() but I don’t really want to duplicate that, right?

Jeff Watkins October 25th, 2005 @ 10:09 pm

Actually you can access the entire User object by accessing turbogears.identity.current.user. This gives you access to the userId, displayName, emailAddress, and creation date.

So if you have the proverbial welcome message:

from turbogears import identity
@turbogears.expose( html="welcome.kid" )
def welcome( self ):
    return dict( user_name=identity.current.user.user.displayName )
Christopher Kelly October 25th, 2005 @ 10:56 pm

Genius! Thank you.

Jared Kuolt October 27th, 2005 @ 2:41 pm

So, if I wanted to access the users’ group info, how would I do that? For example, per group I’d like a different banner to be displayed. That should be rather simple, right?

Jeff Watkins October 27th, 2005 @ 4:10 pm

Jared, there are two ways you can access the group information.

  1. Via the current identity object:

    from turbogears import identity
    if 'admin' in identity.current.groups:
        pass
    
  2. Via the user object on the current identity:

    from turbogears import identity
    if 'admin' in [g.groupId for g in identity.current.user.groups]:
        pass
    

Option number 2 only works if your using a Model that supports groups on the user object. So, with the default model you’ll be set. Other models might not work so well.

I’m not really certain how either option works in your Kid templates.

Jared Kuolt October 28th, 2005 @ 4:45 pm

That’s the kind of information I like to see! Thanks!!

I’m planning on just sending a dictionary of user info to the kid templates.

Jared Kuolt October 28th, 2005 @ 4:47 pm

Duplicate comments because I got a 500 Internal Server Error on the first try…

Just thought you’d like to know.

Benoit Masson October 31st, 2005 @ 10:41 pm

Hi, i’m trying to use you tutorial.
First point : if I follow your step the tg-admin sql create tells me there is nothing to do, So I fetch the somodel.py content and paste it into my model.py for sql create to actually create the table. Is this normal ? I thought the “db_module=idtest.model, turbogears.identity.model” would have include your model…

second point: I have my sql database, I get through the shell part I have a user which is in admin group. Then I start my qucikstart based app anf fetch http://localhost:8080/secured. I get “Not member of group: frozenset([’admin’])” then I type my user/pass and I get the same message again and again … How to debug or see any output there is no error but I never get logged in …
Thanks

Jeff Watkins November 1st, 2005 @ 6:36 pm

Benoit, this seems to be another flaw in SQLObject. When I factored out the data model into a specific SQLObject model, this ran into a known design flaw in SQLObject.

If you are using the default model, you need to add turbogears.identity.model.somodel to the list of database modules. Sorry.

The second issue a bit more confusing. Perhaps you could email me (or post) the code for your controller…

eli yukelzon November 2nd, 2005 @ 5:31 am

I am actually having exactly the same problem as Benoit.
Adding turbogears.identity.model.somodel to the list of database modules did help, and the model db was created ok, but all i get at start is the “Not member of group: frozenset([‘admin’])” message, no matter what.

I am using trunk SVN checkout.

Jeff Watkins November 2nd, 2005 @ 8:23 am

Ah ha! This is a bug I introduced when I added code to strip the submit button from the login form. It’s fixed in the latest SVN code. I apologise for the inconvenience. It seems the login page I posted doesn’t include a name for the login button (which should be login unless you specify otherwise in the config file).

The identity framework should allow you to leave the name off, but this bug was making it mandatory.

Swaroop C H November 2nd, 2005 @ 10:28 am

I had to replace ‘turbogears.identity.model’ with ‘turbogears.identity.model.somodel’ in the above set of steps to get it working. Other than that, it works great.

Swaroop C H November 2nd, 2005 @ 10:32 am

Oops, I just saw that the earlier comments have already mentioned that. Sorry for the noise.

Garrett Smith November 2nd, 2005 @ 3:37 pm

Hi Jeff,

Silly question perhaps, but if I want to control the user attributes (and the table name used for the table), should I subclass the identify model(s) and include my own in their stead?

Benoit Masson November 6th, 2005 @ 8:21 am

Hello, Does someone know how to logout a user ?
I’ve tried “identity.current.logout” as seen in the code but nothing hapen … :( if I reload the page the cookie hasn’t been erased and the user is still connected.
Next question does your system is based on cherryPy session ?
Thanks

Jeff Watkins November 6th, 2005 @ 8:25 am

Benoit, I wouldn’t expect the cookie to have been deleted after calling logout; however, I would expect the SecretToken to be invalid. I know this is a dumb question, but are you using the latest version of the code from SVN?

The Identity framework does not use CherryPy sessions. For the most part, sessions aren’t terribly scalable. And given that the identity framework has so little data, it doesn’t seem necessary. On the other hand, as someone else pointed out, not using sessions means we have to fetch all the user data on each request.

I think a post 1.0 feature of the framework should include an identity cache or something.

joerg November 6th, 2005 @ 8:51 am

in which version of turbogears is your identity mangement integrated?

I use 0.8a4 and I can’t import it (”from turbogears import identity” fails).

Jeff Watkins November 6th, 2005 @ 9:05 am

The identity framework is present in the latest code in Subversion — sort of a pre-0.9.

Benoit Masson November 12th, 2005 @ 9:03 pm

Still stuck with my logout. I have the latest code from SVN, I’ve updated it this morning I get the same result.
What would the code look like for loging out a user ?

Thanks

Jeff Watkins November 13th, 2005 @ 9:16 am

Benoit, take a look at the sample project I’ve uploaded and let me know if this is roughly what you’ve been trying to do.

sharky December 7th, 2005 @ 7:21 am

hummmm, how are your class for db (user/groups)?

If i do something like:
>>> from turbogears.identity.model.somodel import *
>>> hub.begin()
>>> u=User.get(1)
>>> g=Group.get(1)
>>> u.addGroup(g)
>>> u.addGroup(g)
>>> hub.commit()

Than i get a user with [’admin’, ‘admin’] groups right?

DWSmall June 27th, 2007 @ 3:13 am

Some of the best documentation of tg admin that I have seen in a long while. One of the reasons that I avoid the Django crowd is because of a failure to focus on some of the deeper aspects of development.