Section 22.1. Basic AuthenticationAuthorization with Identity


22.1. Basic Authentication/Authorization with Identity

The TurboGears Identity framework defaults to using your application's database to store usernames, privilege information, and passwords. For most stand-alone web applications this is what you want; but it isn't at all hard to validate against an external provider.

As we saw briefly in the WhatWhat Status application, Identity provides a simple API for defining access restrictions to individual pages, page sections, or whole "directory trees." While you can go back in later and add Identity to an existing project, it's easier to install Identity when you create the project by answering Yes when tg-admin quickstart asks if you want Identity. This automatically generates the model classes you need and adds them to your fresh model.py file. Not only does it add everything you need for a login page, it also adds login and logout methods to controller.py and a login.kid file to your templates directory.

Once you have a project with Identity up and running, you can decorate any controller with the require decorator, adding whatever restrictions you need. So, to restrict access to the DashboardController index method to only logged-in users, you could write something like this:

@tg.expose(template="securesite.admin") @require(identity.in_group("admin") def siteadmin(self):     pass


As we saw in Chapter 7, "Controllers, Views, and JavaScript in the WhatWhat Status," you can also secure a whole directory tree by creating a controller class that inherits from SecureResources. You can then override the SecureResources require method with whatever restrictions you need. We'll explore the details of Identity's require syntax in Section 22.2, "Validating User Access with Identity." First let's take a look at the autogenerated code.

We created a new project called securesite, with the tg-admin command, and included Identity. Let's take a quick look at some of the code it generates, before we get started creating secure page. Here's the login method from a freshly quickstarted site:

@expose(template="securesite.templates.login") def login(self, forward_url=None, previous_url=None, *args, **kw):    if not identity.current.anonymous \        and identity.was_login_attempted() \        and not identity.get_identity_errors():        raise redirect(forward_url)    forward_url=None    previous_url= cherrypy.request.path    if identity.was_login_attempted():        msg=_("The credentials you supplied were not correct or "               "did not grant access to this resource.")    elif identity.get_identity_errors():        msg=_("You must provide your credentials before accessing "               "this resource.")    else:        msg=_("Please log in.")        forward_url= cherrypy.request.headers.get("Referer", "/")    cherrypy.response.status=403    return dict(message=msg, previous_url=previous_url, logging_in=True,                original_parameters=cherrypy.request.params,                forward_url=forward_url)


There's quite a bit going on here. This page will be called whenever a user who is not logged in tries to access a secured resource. Because we want to forward the user back to the page they were trying to access originally, the default login controller does quite a bit of work to grab the URL from cherrypy.request.headers, and store it in forward_url.

If you want to rename the login page, you can do it easily by editing this method's name, going to app.cfg, and updating this line of code, to match your new method name.

identity.failure_url="/login"


Other than that, the login controller should be pretty self-explanatory. It checks to see if you are logged in and forwards you to the correct place if you are logged in and there are no errors. If there are any errors, it sends you out to the form with an appropriate message.

The login.kid form is found in login.kid and can be customized to your heart's content. It includes some CSS, which is not shown here, to create a nicer appearance, and you will very likely want to update that code so the login form matches the overall look and feel of your site:

<body>     <div >         <h1>Login</h1>         <p>${message}</p>         <form action="${previous_url}" method="POST">             <table>                 <tr>                     <td >                         <label for="user_name">User Name:</label>                     </td>                     <td >                         <input type="text"  name="user_name"/>                     </td>                 </tr>                 <tr>                     <td >                         <label for="password">Password:</label>                     </td>                     <td >                         <input type="password"  name="password"/>                     </td>                 </tr>                 <tr>                     <td colspan="2" >                         <input type="submit" name="login" value="Login"/>                     </td>                 </tr>             </table>             <input py:if="forward_url" type="hidden" name="forward_url"                 value="${forward_url}"/>             <input py:for="name,value in original_parameters.items()"                 type="hidden" name="${name}" value="${value}"/>         </form>     </div> </body>


As you can see there's nothing new in login.kid. It's just a manually-generated form that will be submitted back to the page that originally created the identity verification request. The require decorator will handle verifying the login and either grant or deny authorization for the login.

It's not nearly as interesting, but the default quickstarted project also includes a logout controller class, which logs the current user out and redirects to the index page.

@expose() def logout(self):     identity.current.logout()     raise redirect("/")


With the default Identity setup, all of your user and permission information will be stored in the database. To this end there are five model classes added to the database.

In model.py we find:

class Visit(SQLObject):     class sqlmeta:         table="visit"     visit_key= StringCol( length=40, alternateID=True,                           alternateMethodName="by_visit_key" )     created= DateTimeCol( default=datetime.now )     expiry= DateTimeCol()     def lookup_visit( cls, visit_key ):         try:             return cls.by_visit_key( visit_key )         except SQLObjectNotFound:             return None     lookup_visit= classmethod(lookup_visit) class VisitIdentity(SQLObject):     visit_key = StringCol(length=40, alternateID=True,                           alternateMethodName="by_visit_key")     user_id = IntCol()


Visit maintains information on a special "visit" cookie that connects the user to a particular server context. The cookie is used by Identity to keep track of which user is making a particular request. The visit_key is a unique hash that is stored in a cookie and is then connected to the user_id in the user class in VisitIdentity.

That brings us to the most important and complex of the Identity related classes in model.py, the User class:

class User(SQLObject):     """     Reasonably basic User definition. Probably would want additional attributes.     """     # names like "Group", "Order" and "User" are reserved words in SQL     # so we set the name to something safe for SQL     class sqlmeta:         table="tg_user"     user_name = UnicodeCol(length=16, alternateID=True,                            alternateMethodName="by_user_name")     email_address = UnicodeCol(length=255, alternateID=True,                                alternateMethodName="by_email_address")     display_name = UnicodeCol(length=255)     password = UnicodeCol(length=40)     created = DateTimeCol(default=datetime.now)     # groups this user belongs to     groups = RelatedJoin("Group", intermediateTable="user_group",                          joinColumn="user_id", otherColumn="group_id")     def _get_permissions(self):         perms = set()         for g in self.groups:             perms = perms | set(g.permissions)         return perms     def _set_password(self, cleartext_password):         "Runs cleartext_password through the hash algorithm before saving."         hash = identity.encrypt_password(cleartext_password)         self._SO_set_password(hash)     def set_password_raw(self, password):         "Saves the password as-is to the database."         self._SO_set_password(password)


The User class uses the special _get and _set tricks SQLObject provides to allow you to overide attribute access and creation. We looked at the _get and _set techniques in depth in Chapter 12, "Customizing SQLObject Behavior." Basically, whenever you set the password attribute, _set_password() will be called instead. Likewise when you access the permissions attribute, which is magically created by SQLObject, you'll get all the permissions for all of the groups of which a particular user is a member.

You can easily add attributes to your User class. For example, you may want to store the user's email address or first and last name. However, the Identity module relies on the existing attributes and methods, so you'll have to be careful when modifying pre-existing attributes.

Part of the permissions model built into Identity is that Users can belong to any number of Groups. In many cases this is all you need to secure your application. You can just check if a user is a member of the "admin" or "editor" or "superhero" groups and grant them access to the proper resources appropriately.

Here's the code for the Group class:

class Group(SQLObject):    """    An ultra-simple group definition.    """    # names like "Group", "Order" and "User" are reserved words in SQL    # so we set the name to something safe for SQL    class sqlmeta:        table="tg_group"    group_name = UnicodeCol(length=16, alternateID=True,                            alternateMethodName="by_group_name")    display_name = UnicodeCol(length=255)    created = DateTimeCol(default=datetime.now)    # collection of all users belonging to this group    users = RelatedJoin("User", intermediateTable="user_group",                        joinColumn="group_id", otherColumn="user_id")    # collection of all permissions for this group    permissions = RelatedJoin("Permission", joinColumn="group_id",                              intermediateTable="group_permission",                              otherColumn="permission_id")


The Group class has a many-to-many relationship with both Users and Permissions. This allows you to create more fine-grained access control. You can say that the "can_add_user" permission is available to members of the "admin" group, as well as to the "hr_manager" group. Again, if you want to store some group-related table, there's no problem. Just add columns to your table as you need them.

That leaves the Permission class:

class Permission(SQLObject):     permission_name = UnicodeCol(length=16, alternateID=True,                                  alternateMethodName="by_permission_name")     description = UnicodeCol(length=255)     groups = RelatedJoin("Group",                          intermediateTable="group_permission",                          joinColumn="permission_id",                          otherColumn="group_id")


You can define users, groups, and passwords using standard SQLObject syntax or by using CatWalk. Passwords are automatically hashed before they go into the database by the _set_password method. So, administering Users, Groups, and Permissions is as painless as updating any other table in your database.




Rapid Web Applications with TurboGears(c) Using Python to Create Ajax-Powered Sites
Rapid Web Applications with TurboGears: Using Python to Create Ajax-Powered Sites
ISBN: 0132433885
EAN: 2147483647
Year: 2006
Pages: 202

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net