I've got something that seems to mostly work.  Here's my caveats:

* I had to make minor modifications to savisit.py and saprovider.py in
order to make this work.  If no one speaks up and tells me how I can
avoid this, I'll submit these to trac.

* SqlAlchemyIdentityProvider() assumes that it needs to load User,
Group, and Permission tables in its __init__().  This means I either
have to forgo invoking the __init__() for the super class or fill the
config variables with fake class-table mappings.  I've done the latter
for now but I'm not sure if there's a third way that's cleaner.

* I'm having difficulties with logout.  When logging out and logging
back in I sometimes get a Flush error from SQLAlchemy:

  File "/usr/lib/python2.4/site-packages/sqlalchemy/orm/mapper.py", line
840, in save_obj
    raise exceptions.FlushError("New instance %s with identity key %s
conflicts with persistent instance %s" % (mapperutil.instance_str(obj),
str(instance_key), mapperutil.instance_str(existing)))
FlushError: New instance [EMAIL PROTECTED] with identity key
(<class 'fedora.accounts.tgfas.VisitIdentity'>,
('1a980e3f8e5f203d310290d7ffc176d6818ab905',), None) conflicts with
persistent instance [EMAIL PROTECTED]

I haven't traced this down yet as it's a transient error.  I'm tempted
to think I'm encountering a race condition somewhere caused by logging
out and then back in too rapidly but I don't have any supporting
evidence yet.

Here's the files I'm using to achieve this:

* savisit.py.diff and saprovider.py.diff: Changes to these files so
subclassing them with the alternative database session works.
* tgfas.py: The visit model.  The User and Group information will be
pulled from ldap.
* safasvisit.py: My visit manager
* sabzprovider.py: My teammate pulled together an identity provider that
authenticates against bugzilla until we code up something that targets
our account system.  I further hacked it to not need a User table to
store information in.  This isn't production quality -- just something
to work the kinks out of the identity-visit connection until we get our
account system hooked up.

-Toshio
--- /usr/lib/python2.4/site-packages/turbogears/visit/savisit.py	2006-11-13 15:09:48.000000000 -0800
+++ savisit.py	2007-01-12 16:34:14.000000000 -0800
@@ -29,7 +29,7 @@
     def new_visit_with_key(self, visit_key):
         visit = visit_class(visit_key=visit_key,
                         expiry=datetime.now()+self.timeout)
-        session.save(visit)
+        visit_class.mapper.get_session().save(visit)
         return Visit(visit_key, True)
         
     def visit_for_key(self, visit_key):
@@ -50,13 +50,14 @@
         
     def update_queued_visits(self, queue):
         # TODO this should be made transactional
-        table = class_mapper(visit_class).mapped_table
+        #table = class_mapper(visit_class).mapped_table
         # Now update each of the visits with the most recent expiry
         for visit_key,expiry in queue.items():
             log.info("updating visit (%s) to expire at %s", visit_key,
                       expiry)
-            get_engine().execute(table.update(table.c.visit_key==visit_key,
-                              values={'expiry': expiry}))
+            visit = visit_class.lookup_visit(visit_key)
+            visit.expiry = expiry
+            visit_class.mapper.get_session().flush()
 
 class TG_Visit(ActiveMapper):
   class mapping:
--- /usr/lib/python2.4/site-packages/turbogears/identity/saprovider.py	2006-09-06 13:48:39.000000000 -0700
+++ saprovider.py	2007-01-12 18:27:42.000000000 -0800
@@ -38,11 +38,11 @@
             pass
         # Attempt to load the user. After this code executes, there *WILL* be
         # a _user attribute, even if the value is None.
-        visit = session.query(visit_class).get_by(visit_key = self.visit_key)
+        visit = visit_class.get_by(visit_key = self.visit_key)
         if not visit:
             self._user = None
             return None
-        self._user = session.query(user_class).get(visit.user_id)
+        self._user = user_class.get(visit.user_id)
         return self._user
     user = property(_get_user)
     
@@ -89,15 +89,15 @@
         if not self.visit_key:
             return
         try:
-            visit = session.query(visit_class).get_by(visit_key=self.visit_key)
-            session.delete(visit)
+            visit = visit_class.get_by(visit_key=self.visit_key)
+            visit_class.mapper.get_session().delete(visit)
             # Clear the current identity
             anon = SqlAlchemyIdentity(None,None)
             identity.set_current_identity(anon)
         except:
             pass
         else:
-            session.flush()
+            visit_class.mapper.get_session().flush()
 
 
 class SqlAlchemyIdentityProvider(object):
import turbogears
from datetime import *
from sqlalchemy import *
from sqlalchemy.ext.sessioncontext import SessionContext
from sqlalchemy.ext.assignmapper import assign_mapper

# The identity schema.

### FIXME:
# Steps to abstracting this:
# [x] Move this to its own model.py
# [x] in app.cfg, set the config to point to a different model.py
# [x] Move the visit/identity model.py into fedora-accounts.

### FIXME: Construct the default dburi from the /etc/sysconfig file like
# website.py does

#dburi = turbogears.config.get('fedora.fasdburi', 'sqlite://')
dburi = 'sqlite:////var/tmp/fasdb.sqlite'
engine = create_engine(dburi)
metadata = DynamicMetaData()
metadata.connect(dburi)

context = SessionContext(lambda:create_session(bind_to=engine))

visits_table = Table('visit', metadata,
    Column('visit_key', String(40), primary_key=True),
    Column('created', DateTime, nullable=False, default=datetime.now),
    Column('expiry', DateTime)
)

visit_identity_table = Table('visit_identity', metadata,
    Column('visit_key', String(40), primary_key=True),
    Column('user_id', Integer, index=True)
)
'''
visit_identity_table = Table('visit_identity', metadata,
    Column('visit_key', String(40), primary_key=True),
    Column('user_id', Integer, ForeignKey('tg_user.user_id'), index=True)
)

groups_table = Table('tg_group', metadata,
    Column('group_id', Integer, primary_key=True),
    Column('group_name', Unicode(16), unique=True),
    Column('display_name', Unicode(255)),
    Column('created', DateTime, default=datetime.now)
)

users_table = Table('tg_user', metadata,
    Column('user_id', Integer, primary_key=True),
    Column('user_name', Unicode(16), unique=True),
    Column('email_address', Unicode(255), unique=True),
    Column('display_name', Unicode(255)),
    Column('password', Unicode(40)),
    Column('created', DateTime, default=datetime.now)
)

permissions_table = Table('permission', metadata,
    Column('permission_id', Integer, primary_key=True),
    Column('permission_name', Unicode(16), unique=True),
    Column('description', Unicode(255))
)

user_group_table = Table('user_group', metadata,
    Column('user_id', Integer, ForeignKey('tg_user.user_id')),
    Column('group_id', Integer, ForeignKey('tg_group.group_id'))
)

group_permission_table = Table('group_permission', metadata,
    Column('group_id', Integer, ForeignKey('tg_group.group_id')),
    Column('permission_id', Integer, ForeignKey('permission.permission_id'))
)
'''

class Visit(object):
    def lookup_visit(cls, visit_key):
        return Visit.get(visit_key)
    lookup_visit = classmethod(lookup_visit)

class VisitIdentity(object):
    pass
'''
class Group(object):
    """
    An ultra-simple group definition.
    """
    pass

class User(object):
    """
    Reasonably basic User definition. Probably would want additional
    attributes.
    """
    def permissions(self):
        perms = set()
        for g in self.groups:
            perms = perms | set(g.permissions)
        return perms
    permissions = property(permissions)

class Permission(object):
    pass
'''
assign_mapper(context, Visit, visits_table)
assign_mapper(context, VisitIdentity, visit_identity_table)
'''
assign_mapper(context, VisitIdentity, visit_identity_table,
          properties=dict(users=relation(User, backref='visit_identity')))
assign_mapper(context, User, users_table)
assign_mapper(context, Group, groups_table,
          properties=dict(users=relation(User,secondary=user_group_table, backref='groups')))
assign_mapper(context, Permission, permissions_table,
          properties=dict(groups=relation(Group,secondary=group_permission_table, backref='permissions')))
'''
from turbogears.visit.savisit import SqlAlchemyVisitManager
from turbogears import config
from turbogears.util import load_class
from turbogears.database import bind_meta_data

class SaFASVisitManager(SqlAlchemyVisitManager):
    '''Visit Manager that talks to the Fedora Account System'''
    def __init__(self, timeout):
        global visit_class
        super(SaFASVisitManager, self).__init__(timeout)
        visit_class_path = config.get('visit.safasprovider.model',
                'fedora.accounts.tgfas.Visit')
        visit_class = load_class(visit_class_path)
        bind_meta_data()
# $Id: sobzprovider.py,v 1.2 2007/01/03 21:21:18 lmacken Exp $
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 2 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

"""
This plugin provides authentication of passwords against Bugzilla via XML-RPC
"""

import logging
import xmlrpclib

from turbogears.identity.saprovider import *
from turbogears import config
from fedora.accounts.tgfas import VisitIdentity

log = logging.getLogger(__name__)
class FakeUser(object):
    def __init__(self, user_id, user_name, user, groups, permissions):
        self.user_id = user_id
        self.user_name = user_name
        self.user = user
        self.groups = groups
        self.permissions = permissions
        self.display_name = 'Toshio Kuratomi'

class FakeGroup(object):
    def __init__(self, gid):
        self.group_id = gid
        self.group_name = 'first' + str(gid)
        self.display_name = 'Group: #' + str(gid)

class SaBugzillaIdentity(SqlAlchemyIdentity):
    def _get_user(self):
        visit = visit_class.get_by(visit_key = self.visit_key)
        if not visit:
            self._user = None
            return None
        self._user = FakeUser(1, 'my fake user name', None, (FakeGroup(1), FakeGroup(2), FakeGroup(3)), None)
        return self._user
    user = property(_get_user)

class SaBugzillaIdentityProvider(SqlAlchemyIdentityProvider):
    """
    IdentityProvider that authenticates users against Bugzilla via XML-RPC
    """
    def __init__(self):
        super(SaBugzillaIdentityProvider, self).__init__()
        self.bz_server = config.get('identity.sabugzilla.bz_server')
        global visit_class
        visit_class_path = config.get("identity.saprovider.model.visit", None)
        log.info("Loading: %s", visit_class_path)
        visit_class = load_class(visit_class_path)

    def validate_identity(self, user_name, password, visit_key):
        user = FakeUser(1, user_name, None, (FakeGroup(1), FakeGroup(2), FakeGroup(3)), None)

        if not self.validate_password(user, user_name, password):
            log.warning("Invalid password for %s" % user_name)
            return None
        log.info("Login successful for %s" % user_name)

        link = visit_class.get_by(visit_key=visit_key)
        if not link:
            link = visit_class(visit_key=visit_key, user_id = user.user_id)
            visit_class.mapper.get_session().save(link)
        else:
            link.user_id = user.user_id
        visit_class.mapper.get_session().flush()
        return SaBugzillaIdentity(visit_key, user)

    def validate_password(self, user, user_name, password):
        """
        Complete hack, but it works.
        Request bug #1 with the given username and password.  If a Fault is
        thrown, the username/pass is invalid; else, we're good to go.
        """
        try:
            server = xmlrpclib.Server(self.bz_server)
            server.bugzilla.getBugSimple('1', user_name, password)
        except xmlrpclib.Fault:
            return False
        return True

    def load_identity(self, visit_key):
        return SaBugzillaIdentity(visit_key)

Attachment: signature.asc
Description: This is a digitally signed message part

Reply via email to