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.

Version 20 (modified by msarahan@…, 9 years ago) (diff)

put in a bit on kid templates

Identity Management

This was ripped from  Jeff Watkins' blog.

...with a little changes to play nice with latest svn [as of 9th Jan 2006]

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.

Quick start example

Step 1 - Create new project

$ tg-admin quickstart 

Name your project idtest and set the dburi in dev.cfg to point to a server and database you want to use.

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

db_module=idtest.model,  turbogears.identity.soprovider

Step 3 - Create login.kid template

The login template and the controllers.py file are already created with up-to-date SVN quickstarts. This was instituted sometime around SVN 485. Here's the code in case you don't have it.

    <!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>

Step 4 - Create secured.kid template

<!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>

Step 5 - edit controllers.py

Ad this code to the top of the file:

from turbogears import identity
import cherrypy

Then add this inside the model class:

@turbogears.expose( html="idtest.templates.login" )
def login( self, *args, **kw ):
    if hasattr(cherrypy.request,"identity_errors"):
        msg= str(cherrypy.request.identity_errors)
    else:
        msg= "Please log in"
    cherrypy.response.status=403
    return dict( message=msg, previous_url=cherrypy.request.path )
    #to preserve the session you may want to return this instead:
    #return dict( message = msg, previous_url = turbogears.url( cherrypy.request.path, cherrypy.request.paramMap ) )

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

Note: You may need to revise the above code for the @identity.require decorator. In a  recent mailing list post, Jeff Watkins writes the following.

n the past you decorated your methods as such:

     @turbogears.expose()
     @identity.require( group="admin", permission="foo,bar" )

The require decorator checked whether the visitor was a member of the  
admin group AND had the permission foo AND had the permission bar.

Many people wanted something more flexible, and with revision 400,  
any of the following are valid require decorators:

     @identity.require( in_group( "admin" ) )
     @identity.require( in_all_groups( "admin", "editor" ) )
     @identity.require( in_any_group( "admin", "editor" ) )
     @identity.require( has_permission( "edit" ) )
     @identity.require( has_all_permissions( "edit", "delete",  
"update" ) )
     @identity.require( has_any_permission( "edit", "delete",  
"update" ) )

But most importantly, you can use decorators like theses:

     @identity.require( Any( in_group( "admin" ), has_permission
( "edit" ) ) )
     @identity.require( All( from_host( "127.0.0.1" ), has_permission
( "edit" ) ) )
     @identity.require( All( from_any_host( "127.0.0.1", "10.0.0.1" ),
                        in_group( "editor" ) ) )

You can also use these same predicates in your own code:

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

I still haven't addressed the need for something like `is_owner`,  
because that seems *so* model specific. 

However, you may need to use the in_group, in_all_groups, etc. functions in the identity namespace. For example:

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

changes to

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

Step 6 - Turn on Identity management

Edit dev.cfg. Under the "IDENTITY" heading (around line 68), uncomment and edit the following to turn on identity management. Edit the failure url as well.

identity.on=True
identity.failure_url="/login"

Step 7 - Create the database

$ tg-admin sql create

Step 8 - Create a user and group

Using Catwalk is probably the easiest way to create user/group/permissions. Use this method if you can't get Catwalk set up.

$ 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.soprovider import *
>>> hub.begin()
>>> u=TG_User( userId="jeff", emailAddress="jeff@metrocat.org",
            displayName="Jeff Watkins", password="xxxxx" )
>>> g=TG_Group( groupId="admin", displayName="Administrators" )
>>> hub.commit()
>>>

Step 9 - Testing the login

Start the project:

$ ./idtest-start.py

and visit secured page  http://localhost:8080/secured and login with the username and password you just created. It should fail with the message:

Not member of group: admin

Step 10 - Add the 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=TG_User.get(1)
>>> g=TG_Group.get(1)
>>> u.addTG_Group(g)
>>> hub.commit()
>>>

Step 11 - Revisit secured page and login

Browse to  http://localhost:8080/secured again and login, this time you should see the content of secured.kid


Restricting Access to whole Subdirectory

(from mailing list)

You should be able to restrict access to a subdirectory by subclassing SecureResource? in your descendent objects. So you might have the following:

class Toxicologia(controller.Controller, identity.SecureResource):
     required_permissions= ["write"]
     required_groups= ["admin"]
     allowed_hosts= ["127.0.0.1"]
     identity_required= True

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


Specifying an 'or' type for group Access

There are two ways to handle this:

1 Derive your Controller from SecureResource? (in addition to Controller) and check the permissions explicitly. For example:

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 throws, er, 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 into each function that requires it.

2 Write your own decorator function. This is not for the faint at heart. But it gives you absolute flexibility. Take a look at the two decorators in turbogears/identity/conditions.py. They'll give you a head-start on what you'll have to do.


Identity and Kid templates

In addition to restricting access to methods in controller files, identity checks can also be used to limit what links(or any other element, for that matter) show up in kid templates. This is done using py:if="" statements, like so:

<a py:if="'admin' in turbogears.identity.current.groups" href="/test">This is a test</a>
<a py:if="'write' in turbogears.identity.current.permissions" href="/test">This is a test</a>

Make sure you import turbogears somewhere in your template for those identity checks to work.

<?python import turbogears ?>

or, to save on typing,

<?python from turbogears import identity ?>

and omit the "turbogears" part of the py:if statement.


Applying security settings, not from source code, but from configuration data

You should be able to specify security settings not only from source code but via some other means. The goal is to allow an administrator to set the security policy, not the programmer.


FAQ's

How do I retrieve the userId in my application code?

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 I wanted to access the users’ group info, how would I do that?

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.


Attachments