wiki:IdentityManagement
Warning: Can't synchronize with repository "(default)" (Unsupported version control system "svn": No module named svn). Look in the Trac log for more information.

Note: This document has been migrated to http://docs.turbogears.org/1.0/IdentityManagment. Please make any further changes there.

Identity Management

TurboGears provides user- and permission-based identity (authentication and authorization) support right out of the box.

Identity Management can be used in both controllers and in templates:

  • In controllers to implement access restrictions
  • In templates to adapt the appearance depending on the user's identity

This tutorial will show you how to enable and use identity support in a new quickstarted project. We'll use the SQLObject object-relational mapper, and the default SQLite database, but the same principles apply when using the SQLAlchemy object-relational mapper or other database back-ends.

Getting Started

Create the Project

First, let's create a new project called "identity_tutorial":

$ tg-admin quickstart --identity identity_tutorial

Accept the default package name of "identity_tutorial", and TurboGears will generate the project skeleton for you, including identity-specific data objects and controllers.

Now initialize the database:

$ tg-admin sql create

You now have an empty database called "devdata.sqlite", which contains the empty tables needed by identity management.

Test the Login Page

Now let's check whether we have all set up correctly so far. Start the project as usual:

$ start-identity_tutorial

Visit the login page  http://localhost:8080/login. You should see a pretty login page with username and password fields. Try to log in.

It will fail with an error message, because you haven't added any users yet.

Add a User and Group

If you chose SQLObject for database support, you can use either the TurboGears shell or CatWalk to add your first users. CatWalk does not yet support SQLAlchemy, so if you use that ORM, then you'll need to use the TurboGears shell.

Using CatWalk

Start the TurboGears toolbox:

$tg-admin toolbox

Enter CatWalk (a browser window should open automatically; if it doesn't, go to  http://localhost:7654/catwalk/).

You will see the Identity-related classes listed on the left side of the page.

Create a user

Select User. Click the "Add User+" button, then enter the user information. For example:

email_address: jdoe@example.com
display_name: Jane Doe
user_name : jdoe
password : xxx

Click "Save". You've created your first user.

Create an admin group

Select Group. Click the "Add Group+" button. Enter the display_name and group_name, for example:

display_name : Administrators
group_name :admin

Click "Save". Now you have a group and a user; let's combine them:

Add the new user to the admin group

Note that we are now in the "Browse" tab, with the new group displayed.

Click the expansion triangle in front of "users". This will reveal a "Manage Relations" link. Click on it.

CatWalk will display two lists. The list on the left-hand side shows the users currently assigned to this group; the list on the right-hand side contains all users. Select the new user on the right list and click on "Add Selected" to move it to the left list. Then click "Save" to confirm the change.

Using the Shell

If you'd rather use the shell to create the user and group, or if you're using SQLAlchemy, you can do this from the top-level project directory:

$ tg-admin shell

Recall that your model is always imported into the shell environment, as if you'd typed "from model import *".

Now create the initial user:

u = User(user_name='jdoe', email_address='jdoe@example.com', display_name='Jane Doe', password='xxx')

You will see messages indicating that the shell has loaded identity models, and a warning that you're using plaintext passwords. That's fine for now, but in a production environment, you'll want to specify a password encryption scheme (see below).

Similarly, create the group:

g = Group(group_name='admin', display_name='Administrators')

Now add the user to the group:

g.addUser(u)

You can see that the user is indeed part of the group:

>>> g.users
[<User 1 user_name=u'jdoe' email_address=u'jdoe@example.com' display_name=u'Jane Doe' password=u'xxx' created='datetime.datetime...)'>]

>>> u.groups
[<Group 1 group_name=u'admin' display_name=u'Administrators' created='datetime.datetime...)'>]

Now exit the shell (using Ctrl-Z on Windows, or Ctrl-D on other systems). You'll be asked whether you want to commit you database changes; choose "yes".

Using Identity in the Controller

If you want to protect a controller method, add an @identity.require(...) decorator to the method. This decorator has a single argument, the predicate, which specifies the conditions to check.

The identity module provides many predicates that you can use. For example to protect the index page so that only members of the admin group can access it, we can use the identity.in_group("admin") predicate:

    @turbogears.expose(template=".templates.welcome")
    @identity.require(identity.in_group("admin"))
    def index(self):
        ...

Let's try this now. Visit  http://localhost:8080/. As the index page is protected, you will be redirected to the login page. Log in using the name and password of the account you created. Now you should see the index page, with "Welcome, Jane Doe." and a "Logout" link at the top of the page. This is created for you in the master.kid template.

Identity Predicates

Here are a few commonly-used identity predicates:

Single Permission Checks

Checking that the user is logged in

@identity.require(identity.not_anonymous())

Checking access groups

@identity.require(identity.in_group("admin"))

You can also specify multiple groups:

@identity.require(identity.in_all_groups("admin", "editor"))

@identity.require(identity.in_any_group("admin", "editor"))

Checking access permissions

@identity.require(identity.has_permission("edit"))

@identity.require(identity.has_all_permissions("edit", "delete", "update"))

@identity.require(identity.has_any_permission("edit", "delete", "update"))

Checking hosts

@identity.require(identity.from_host("127.0.0.1"))

@identity.require(identity.from_any_host(("127.0.0.1", "10.0.0.1")))

Combining Predicates

You can combine several predicates using identity.Any and identity.All:

     @identity.require(identity.Any(identity.in_group("admin"), identity.has_permission("edit")))

This decorator grants access to members of the "admin" group as well as any user who has the "edit" permission.

     @identity.require(identity.All(identity.from_host("127.0.0.1"), identity.has_permission("edit")))

     @identity.require(identity.All(identity.from_any_host(("127.0.0.1", "10.0.0.1")), identity.in_group("editor")))

Using Identity checks in templates

Identity checks can also be used in Kid templates to customize the appearance of the page depending on the user's identity. For example, you might show links to administrative functions only if the user is an administrator. (Naturally, you will also need to check identity in the controllers that handle those links, since just hiding them doesn't prevent a knowledgeable user from accessing those URIs directly.)

Within a template, 'tg.identity' is an alias for 'turbogears.identity.current'.

Checking access groups

<a py:if="'admin' in tg.identity.groups" href="/admin">This is a link for admins</a>

Checking access permissions

<div py:if="'write' in tg.identity.permissions">This is a write permissions area</div>

Displaying user-specific information

  <div py:if="tg.identity.anonymous">Welcome, guest!</div>
  <div py:if="not tg.identity.anonymous">Welcome, ${tg.identity.user.display_name}!</div>

Additional Identity Recipes

Sometimes, just restricting access to individual methods (pages) isn't enough. For example, you might want to protect an entire controller, or maybe your access permissions depend on the data viewed.

Protecting a Directory

To restrict access to an entire controller (subdirectory), derive your Controller from identity.SecureResource? and add a require attribute at the class level.

class MySecureController(turbogears.Controller, identity.SecureResource):
    require = identity.in_group("admin")

    # etc. ... 

You can apply whatever decorators you want on the methods of the MySecureController? instance. So each method could have additional restrictions. And MySecureController? could have SecureObjects? as well. However, access to exposed methods of MySecureController? and any SecureObjects? would have to satisfy the authorisation requirements for MySecureController?.

Explicit Permission Checking

Let's say you are creating a web site where users can add their own content, like a blogging tool or a photo sharing site. Users should be able to edit their own content, but not the content added by other users. You can't do these checks in a decorator, as you need access to the actual data, which is only loaded in the method body. (And you don't want to load the data twice.) So you'll have to perform the identity checks in the method body.

Again, derive your controller from identity.SecureResource?. Perform your identity check at the method level. If the user doesn't have the required permissions, throw a suitable IdentityException?:

class MyController(controllers.Controller, identity.SecureResource):

     @turbogears.expose(html="mytemplate")
     def myFunction(self):
         if not ("admin" in identity.current.groups or
                 "super" in identity.current.groups):
             raise identity.GroupMembershipRequiredException(("admin", "super"))

This will work because SecureResource? wraps all exposed methods with code that checks permissions and traps IdentityExceptions?. So if your code, raises an IdentityException?, everything will be handled correctly.

Of course, you can then pull your authorisation logic out into a function that you call rather than copying and pasting it into each function that requires it.

You can also use the identity predicates in your own code:

     if identity.in_group("admin") and identity.has_permission("edit"):
         pass
     else:
         pass

Writing your own decorator function

This is not for the faint at heart, but it does give you absolute flexibility.

Take a look at the decorators in turbogears/identity/conditions.py. They'll give you a head-start on what you'll have to do.

Also, to make your life easier, remember to base you decorator on turbogears.decorator. It will give you a solid basis and is used in other default decorators as well.

Customizing the Identity Model

You can customize your own classes for users, groups, and/or permissions -- to add a few attributes to the user class, say an image of the user and a phone number -- or even a complete replacement.

However, identity references the automatically-generated classes and their elements by name, so you should not remove or rename them.

Authenticating against an external password source

In some cases, you may want to authenticate against an external password source, while still using the standard Identity models for storing everything except passwords.

For example, authenticating against an existing Windows/Samba? domain controller means that your users can use the same password for your TurboGears project as they use to log in to Windows. (The same concept applies to LDAP, etc.)

For an example of how to do this, see the attached file "sosmbprovider.py", which subclasses the SQLObject provider, but validates user names and passwords against a Windows domain. It's not a pure SMB provider (only the user names and passwords are checked against the domain controller), so you still have to add users and groups (etc.) to the Identity tables.

For an example of how to validate user names and passwords against an LDAP directory, see "soldapprovider.py".

As of r1512 (branch 1.0) and r1513 (trunk), a validate_password() method was added to the "SqlObjectIdentityProvider?" object in soprovider.py, making it much simpler to subclass and create your own provider. So if you're running 0.9a6 or before, you'll still need to use "sosmbprovider.py", but if you're running a later release (or svn r1512 or later), you can use "sosmbprovider-(after_r1512).py". (It works the same, but the code is shorter and clearer.)

Attachments