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
Memberto 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 itemsto your new ZClass and change the setting forObjects areto be loaded by accessing attributemember_id. - Go to the Security tab of your test folder and:
- Add the
Memberrole as a User Defined Role. - Check the
Viewpermission forMember. - Then uncheck the "Acquire permission settings?" box on
View. - Click
Change.
- Add the
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
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...