You are not logged in Log in Join
You are here: Home » Members » dlpierson » LoginManager with SQL and Skinscript » View Document

Log in
Name

Password

 

LoginManager with SQL and Skinscript

Integrating LoginManager with an SQL database is generally done using the GenericUserSource that comes with LoginManager. This was produced in the early LoginManager days as a clone of the Generic User Folder (GUF) interface. ZPatterns has evolved since GenericUserSource was created and some of the design decisions made in the GUF days don't fit the current ZPatterns model as well as they might.

User Authentication with LoginManager and GenericUserSource

With GenericUserSource all the action happens in SQL and DTML methods in the user folder. User information is passed in as arguments. Thus, the Rack knows too much about the application for my tastes. It doesn't know how authentication is done, but it does know that it has to handle it by calling a method in the specialist. This leads to the following flow of method calls:

  ZPublisher calls
          |
          |
          V
  LoginManager.LoginManager.validate() is called by Zope
          |
          | (pass self as manager)
          V
    LoginMethods.<method>.findLogin()
          |
          | manager.getItem() looks up user object which
          | is a subclass of LoginManager.LoginUser
          |
          V
      user.authenticate()
          |
          | LoginManager.LoginUser.authenticate is just a trampoline to
          |
          V
      GenericUserSource.authenticateUser()
          |
          | calls DTML/Python method written as part of GUS integration
          V
      userAuthenticate()

User Authentication with LoginManager and SkinScript

Applying the ZPatterns model as I understand it:

  • the user object should know how to authenticate itself,
  • the Specialist (LoginManager) should know how to find user objects and ask them to do things like authenticate themselves, and
  • the Rack (UserSource) should know almost nothing but how to get and maintain data.

SkinScript claims to make all of this very simple, so I decided to try it out. The following call flow is simpler, because a lot is happening magically in the ZPatterns core:

  ZPublisher calls
          |
          |
          V
  LoginManager.LoginManager.validate() is called by Zope
          |
          | (pass self as manager)
          V
    LoginMethods.<method>.findLogin()
          |
          | manager.getItem() looks up user object
          |   User ZClass instance with LoginManager.LoginUser as a 
          |     base classs is created.
          |   SkinScript method is called to get data from SQL when
          |     ZClass's member_id attribute is referenced.
          |
          V
      user.authenticate()
          |
          | Is a PythonMethod of the ZClass that does the
          | authorization directly.

Simple Example

The good news is that it really is simple. To keep it simple this example has minimal features and stores unencrypted passwords on the extreme programming principle of "Do the simplest thing that could possible work." I don't expect it to be hard to add the needed additional features once the basic questions are worked out.

Required Components

  • LoginManager 0.8.8b1 with ZPatterns upgraded to the 0.4.3b2 patch.
  • Postgresql (or modify the SQL for your database)
  • ZPyGreSQLDA (or whatever your database needs)
  • PythonMethods

Database

This initial design is intended to experiment with:

  • using SkinScript to access and cache pieces of the database in separate methods,
  • accessing pieces of the database as pseudo-property sheets (implemented as separate virtual ZClasses?), and
  • our future need to segment our member data for other reasons.

Member ids are Zope ids and most easily managed as immutable. Also, most web sites that I've encountered treat member ids as immutable (and disposable). For ease of debugging, I decided to use member ids and role names directly as keys instead of indirecting with a numeric key.

Here's the SQL to set up the database:

     CREATE TABLE "member_authorization" (
       "member_id" text PRIMARY KEY,
       "password" text,
       "challenge_question" text,
       "challenge_answer" text,
       "admin_email" text NOT NULL);

     CREATE TABLE "roles" (
       "role_name" text PRIMARY KEY);

     CREATE TABLE "member_role_map" (
       "member_id" text REFERENCES member_authorization,
       "role_name" text REFERENCES roles);

     CREATE TABLE "member_domain_map" (
       "member_id" text REFERENCES member_authorization,
       "domain" text);

     CREATE TABLE "member_statistics" (
       "member_id" text REFERENCES member_authorization,
       "login_time" timestamp,
       "last_login_time" timestamp);

     CREATE TABLE "member_profile" (
       "member_id" text REFERENCES member_authorization,
       "full_name" text,
       "public_email" text,
       "chrome" text,
       "listed" bool);

Create this database with the following commands:

     # createdb members
     # psql members <members.sql

Now create a Z PyGreSQL Database Connection with database connection string:

     members your-postgresql-user-name

Open this connection and use it when defining all the following ZSQL Methods.

LoginManager Setup

Now create a LoginManager instance in the folder you want to manage. Select UserSource as your UserSource type. This will give you a BasicUserSource.

ZSQL Methods

We now need a few methods to access the raw data. Make all of these methods of the Specialist (LoginManager) by defining them in the acl_users folder.

SQL_findMember(member_id)

This gets the basic member data needed for authorization:

      select member_id, password, 
             challenge_question, challenge_answer,
             admin_email
      from member_authorization
      where member_authorization.member_id = 
          <dtml-sqlvar member_id type="string">

SQL_get_roles(member_id)

This returns a list of brains, each of which has the attribute 'role_name':

      select role_name
      from member_role_map
      where member_id = <dtml-sqlvar member_id type="string">

SQL_get_domains(member_id)

This returns a list of brains, each of which has the attribute 'domain':

      select domain
      from member_domain_map
      where member_id = <dtml-sqlvar member_id type="string">

SQL_findMemberProfile(member_id)

This gets the member-alterable data. Note that this and the following method are optional, as are the associated tables and SkinScript methods:

      select full_name, public_email, chrome, listed
      from member_profile
      where member_profile.member_id = 
          <dtml-sqlvar member_id type="string">

SQL_findMemberStatistics(member_id)

This gets data that we automatically update with every login. Again, this is optional:

      select login_time, last_login_time
      from member_statistics
      where member_statistics.member_id =
          <dtml-sqlvar member_id type="string">

The following methods are trivial admin support to populate the minimal databases. They need to be replaced with additional Zope pages or other tools. In the meantime you can populate for simple tests just by using the test tab of the Z SQL Method.

SQL_add_role(role_name)

You'll need to use this to define at least one role for things to work:

      insert into roles
      values (<dtml-sqlvar role_name type="string">)

SQL_add_user(member_id, password, email)

The longest method in the whole example, this just fills in all the table entries for a new member, mostly with dummy values:

      insert into member_authorization
      values (<dtml-sqlvar member_id type="string">,
              <dtml-sqlvar password type="string">,
              NULL, NULL, <dtml-sqlvar email type="string">)

      <dtml-var sql_delimiter>

      insert into member_role_map 
      values (<dtml-sqlvar member_id type="string">, 'Member')

      <dtml-var sql_delimiter>

      insert into member_statistics 
      values (<dtml-sqlvar member_id type="string">, NULL, NULL)

      <dtml-var sql_delimiter>

      insert into member_profile
      values(<dtml-sqlvar member_id type="string">,
             NULL, NULL, NULL, 'false')

SkinScript

Select the User Sources tab, then click on your UserSource, then select the Data Plug-ins tab. Delete all but SpecialistPlugIns and add a SkinScript Method named member_storage (I don't think the name matters) with the following contents:

    WITH QUERY SQL_findMember(member_id=self.id) COMPUTE
    member_id, password, challenge_question, challenge_answer, admin_email

    WITH QUERY SQL_findMemberStatistics(member_id=self.id) COMPUTE
    login_time, last_login_time

    WITH QUERY SQL_findMemberProfile(member_id=self.id) COMPUTE
    full_name, public_email, chrome, listed

    WITH SQL_get_roles(member_id=self.id) COMPUTE
    role_list = RESULT

    WITH SQL_get_domains(member_id=self.id) COMPUTE
    domain_list = RESULT

Make this method the first data plug-in.

The Member ZClass

Go into Control Panel, then Products and create a new product to contain your ZClass.

Create a ZClass with LoginManager:LoginUser as a base class. Add the following PythonMethods to this ZClass (you could use DTML methods if you really insist).

authenticate(self, password, request):

    if password == self.password:
        return 1
    else:
        return 0

getDomains(self):

    result = []
    if self.domain_list:
        for d in self.domain_list:
            result.append(r.domain)
    return result

getRoles(self):

    result = []
    if self.role_list:
        for r in self.role_list:
            result.append(r.role_name)
    return result

The last two methods simply convert a list of brains with a single interesting value to a list of that value. It would be much nicer if this could be done in line in the SkinScript method as in:

    WITH SQL_get_domains(member_id=self.id) COMPUTE
    domains = map(lambda x: x.domain, RESULT)

Unfortunately, map isn't available in DTML which means that it isn't available in SkinScript. Maybe when we convert to Python 2.0 with list comprehensions the above can be written:

    WITH SQL_get_domains(member_id=self.id) COMPUTE
    domains = [d.domain for d in RESULT]

You're Done

Well almost :-). To test all this:

  • Use SQL_add_role to add the role Member to your database.
  • Use SQL_add_user to add a user.
  • Enable new member objects by selecting the User Sources tab, then clicking on your UserSource, then selecting the Storage tab. Change the Class to use for stored items to your new ZClass and change the setting for Objects are to be loaded by accessing attribute member_id.
  • Go to the Security tab of your test folder and:
    • Add the Member role as a User Defined Role.
    • Check the View permission for Member.
    • Then uncheck the "Acquire permission settings?" box on View.
    • Click Change.

Now start up a fresh browser and attempt to view the folder. You should get a login prompt and be able to successfully log in.

Comment

Discussion icon ZPyGreSQLDA

Posted by: dpwildboar at 2004-11-21

is ZPyGreSQLDA even being maintained any more? the download for it on this site has a 2000 date on it. the work that Federico Di Gregorio is doing with psycopg and ZPsycopgDA (see: http://www.initd.org/) seem like a much better place to refer people to...