Author: chrisz
Date: Mon Jan 17 06:36:05 2011
New Revision: 7205
URL: http://trac.turbogears.org/changeset/7205
Log:
Added default identity model classes/tables to the SQLAlchemy Identity provider
(ticket #1453).
Modified:
branches/1.5/turbogears/identity/saprovider.py
branches/1.5/turbogears/identity/soprovider.py
branches/1.5/turbogears/qstemplates/quickstart/+package+/json.py_tmpl
branches/1.5/turbogears/qstemplates/quickstart/+package+/model.py_tmpl
branches/1.5/turbogears/visit/savisit.py
Modified: branches/1.5/turbogears/identity/saprovider.py
==============================================================================
--- branches/1.5/turbogears/identity/saprovider.py Mon Jan 17 03:33:27
2011 (r7204)
+++ branches/1.5/turbogears/identity/saprovider.py Mon Jan 17 06:36:05
2011 (r7205)
@@ -1,16 +1,24 @@
import logging
+from datetime import datetime
from turbogears import config, identity
-from turbogears.database import bind_metadata, session
+from turbogears.database import bind_metadata, metadata, session
from turbogears.util import load_class
+from turbojson.jsonify import jsonify_saobject, jsonify
-from sqlalchemy.orm import class_mapper
+from sqlalchemy import (Table, Column, ForeignKey,
+ String, Unicode, Integer, DateTime)
+from sqlalchemy.orm import class_mapper, mapper, relation
try:
from sqlalchemy.exc import IntegrityError
except ImportError: # SQLAlchemy < 0.5
from sqlalchemy.exceptions import IntegrityError
+try:
+ from sqlalchemy.orm.exc import UnmappedClassError
+except ImportError: # SQLAlchemy < 0.5
+ from sqlalchemy.exceptions import InvalidRequestError as UnmappedClassError
-log = logging.getLogger("turbogears.identity.saprovider")
+log = logging.getLogger('turbogears.identity.saprovider')
# Global class references --
@@ -157,10 +165,15 @@
for classname in ('user', 'group', 'permission', 'visit'):
default_classname = '.TG_' + (classname == 'visit'
and 'VisitIdentity' or classname.capitalize())
- class_path = config.get("identity.saprovider.model.%s" % classname,
+ class_path = config.get('identity.saprovider.model.%s' % classname,
__name__ + default_classname)
class_ = load_class(class_path)
if class_:
+ if class_path == __name__ + default_classname:
+ try:
+ class_mapper(class_)
+ except UnmappedClassError:
+ class_._map()
log.info('Successfully loaded "%s".', class_path)
glob_ns['%s_class' % classname] = class_
else:
@@ -169,7 +182,7 @@
def encrypt_password(self, password):
# Default encryption algorithm is to use plain text passwords
- algorithm = config.get("identity.saprovider.encryption_algorithm",
None)
+ algorithm = config.get('identity.saprovider.encryption_algorithm',
None)
return identity.encrypt_pw_with_algorithm(algorithm, password)
def create_provider_model(self):
@@ -177,7 +190,11 @@
bind_metadata()
class_mapper(user_class).local_table.create(checkfirst=True)
class_mapper(group_class).local_table.create(checkfirst=True)
+ if group_class is TG_Group:
+ group_class._user_association_table.create(checkfirst=True)
class_mapper(permission_class).local_table.create(checkfirst=True)
+ if permission_class is TG_Permission:
+ permission_class._group_association_table.create(checkfirst=True)
class_mapper(visit_class).local_table.create(checkfirst=True)
def validate_identity(self, user_name, password, visit_key):
@@ -245,3 +262,190 @@
def authenticated_identity(self, user):
"""Construct Identity object for users with no visit_key."""
return SqlAlchemyIdentity(user=user)
+
+
+# default identity model classes
+
+
+class TG_User(object):
+ """Reasonably basic User definition."""
+
+ def __repr__(self):
+ return '<User: name="%s", email="%s", display name="%s">' % (
+ self.user_name, self.email_address, self.display_name)
+
+ def __unicode__(self):
+ return self.display_name or self.user_name
+
+ @property
+ def permissions(self):
+ """Return all permissions of all groups the user belongs to."""
+ p = set()
+ for g in self.groups:
+ p |= set(g.permissions)
+ return p
+
+ @classmethod
+ def by_email_address(cls, email_address):
+ return
session.query(cls).filter_by(email_address=email_address).first()
+
+ @classmethod
+ def by_user_name(cls, user_name):
+ return session.query(cls).filter_by(user_name=user_name).first()
+ by_name = by_user_name
+
+ def _set_password(self, password):
+ """Run cleartext password through the hash algorithm before saving."""
+ try:
+ hash =
identity.current_provider.encrypt_password(cleartext_password)
+ except identity.exceptions.IdentityManagementNotEnabledException:
+ # Creating identity provider just to encrypt password
+ # (so we don't reimplement the encryption step).
+ ip = SqlAlchemyIdentityProvider()
+ hash = ip.encrypt_password(cleartext_password)
+ if hash == cleartext_password:
+ log.info("Identity provider not enabled,"
+ " and no encryption algorithm specified in config."
+ " Setting password as plaintext.")
+ self._password = hash
+
+ def _get_password(self):
+ """Returns password."""
+ return self._password
+
+ password = property(_get_password, _set_password)
+
+ @classmethod
+ def _map(cls):
+ cls._table = Table('tg_user', metadata,
+ Column('user_id', Integer, primary_key=True),
+ Column('user_name', Unicode(16), unique=True, nullable=False),
+ Column('email_address', Unicode(255), unique=True),
+ Column('display_name', Unicode(255)),
+ Column('password', Unicode(40)),
+ Column('created', DateTime, default=datetime.now))
+ cls._mapper = mapper(cls, cls._table,
+ properties=dict(_password=cls._table.c.password))
+
[email protected]('isinstance(obj, TG_User)')
+def jsonify_user(obj):
+ """Convert user to JSON."""
+ result = jsonify_saobject(obj)
+ result.pop('password', None)
+ result.pop('_password', None)
+ result['groups'] = [g.group_name for g in obj.groups]
+ result['permissions'] = [p.permission_name for p in obj.permissions]
+ return result
+
+
+class TG_Group(object):
+ """An ultra-simple Group definition."""
+
+ def __repr__(self):
+ return '<Group: name="%s", display_name="%s">' % (
+ self.group_name, self.display_name)
+
+ def __unicode__(self):
+ return self.display_name or self.group_name
+
+ @classmethod
+ def by_group_name(cls, group_name):
+ """Look up Group by given group name."""
+ return session.query(cls).filter_by(group_name=group_name).first()
+ by_name = by_group_name
+
+ @classmethod
+ def _map(cls):
+ cls._table = Table('tg_group', metadata,
+ Column('group_id', Integer, primary_key=True),
+ Column('group_name', Unicode(16), unique=True, nullable=False),
+ Column('display_name', Unicode(255)),
+ Column('created', DateTime, default=datetime.now))
+ cls._user_association_table = Table('user_group', metadata,
+ Column('user_id', Integer, ForeignKey('tg_user.user_id',
+ onupdate='CASCADE', ondelete='CASCADE'), primary_key=True),
+ Column('group_id', Integer, ForeignKey('tg_group.group_id',
+ onupdate='CASCADE', ondelete='CASCADE'), primary_key=True))
+ cls._mapper = mapper(cls, cls._table,
+ properties=dict(users=relation(TG_User,
+ secondary=cls._user_association_table, backref='groups')))
+
[email protected]('isinstance(obj, TG_Group)')
+def jsonify_group(obj):
+ """Convert group to JSON."""
+ result = jsonify_saobject(obj)
+ result['users'] = [u.user_name for u in obj.users]
+ result['permissions'] = [p.permission_name for p in obj.permissions]
+ return result
+
+
+class TG_Permission(object):
+ """A relationship that determines what each Group can do."""
+
+ def __repr__(self):
+ return '<Permission: name="%s">' % self.permission_name
+
+ def __unicode__(self):
+ return self.permission_name
+
+ @classmethod
+ def by_permission_name(cls, permission_name):
+ """Look up Permission by given permission name."""
+ return
session.query(cls).filter_by(permission_name=permission_name).first()
+ by_name = by_permission_name
+
+ @classmethod
+ def _map(cls):
+ cls._table = Table('permission', metadata,
+ Column('permission_id', Integer, primary_key=True),
+ Column('permission_name', Unicode(16), unique=True,
nullable=False),
+ Column('description', Unicode(255)))
+ cls._group_association_table = Table('group_permission', metadata,
+ Column('group_id', Integer, ForeignKey('tg_group.group_id',
+ onupdate='CASCADE', ondelete='CASCADE'), primary_key=True),
+ Column('permission_id',
+ Integer, ForeignKey('permission.permission_id',
+ onupdate='CASCADE', ondelete='CASCADE'), primary_key=True))
+ cls._mapper = mapper(cls, cls._table,
+ properties=dict(groups=relation(TG_Group,
+ secondary=cls._group_association_table,
backref='permissions')))
+
[email protected]('isinstance(obj, TG_Permission)')
+def jsonify_permission(obj):
+ """Convert permissions to JSON."""
+ result = jsonify_saobject(obj)
+ result['groups'] = [g.group_name for g in obj.groups]
+ return result
+
+
+class TG_VisitIdentity(object):
+ """A Visit that is linked to a User object."""
+
+ @classmethod
+ def by_visit_key(cls, visit_key):
+ """Look up VisitIdentity by given visit key."""
+ return session.query(cls).get(visit_key)
+
+ @classmethod
+ def _map(cls):
+ cls._table = Table('visit_identity', metadata,
+ Column('visit_key', String(40), primary_key=True),
+ Column('user_id', Integer,
+ ForeignKey('tg_user.user_id'), index=True))
+ cls._mapper = mapper(cls, cls._table,
+ properties=dict(user=relation(TG_User, backref='visit_identity')))
+
+
+def encrypt_password(cleartext_password):
+ """Encrypt given cleartext password."""
+ try:
+ hash = identity.current_provider.encrypt_password(cleartext_password)
+ except identity.exceptions.RequestRequiredException:
+ # Creating identity provider just to encrypt password
+ # (so we don't reimplement the encryption step).
+ ip = SqlAlchemyIdentityProvider()
+ hash = ip.encrypt_password(cleartext_password)
+ if hash == cleartext_password:
+ log.info("Identity provider not enabled, and no encryption "
+ "algorithm specified in config. Setting password as
plaintext.")
+ return hash
Modified: branches/1.5/turbogears/identity/soprovider.py
==============================================================================
--- branches/1.5/turbogears/identity/soprovider.py Mon Jan 17 03:33:27
2011 (r7204)
+++ branches/1.5/turbogears/identity/soprovider.py Mon Jan 17 06:36:05
2011 (r7205)
@@ -11,9 +11,9 @@
from sqlobject.dberrors import DuplicateEntryError
-log = logging.getLogger("turbogears.identity.soprovider")
+log = logging.getLogger('turbogears.identity.soprovider')
-hub = PackageHub("turbogears.identity")
+hub = PackageHub('turbogears.identity')
__connection__ = hub
@@ -177,7 +177,7 @@
for classname in ('user', 'group', 'permission', 'visit'):
default_classname = '.TG_' + (classname == 'visit'
and 'VisitIdentity' or classname.capitalize())
- class_path = config.get("identity.soprovider.model.%s" % classname,
+ class_path = config.get('identity.soprovider.model.%s' % classname,
__name__ + default_classname)
class_ = load_class(class_path)
if class_:
@@ -194,7 +194,7 @@
def encrypt_password(self, password):
# Default encryption algorithm is to use plain text passwords
- algorithm = config.get("identity.soprovider.encryption_algorithm",
None)
+ algorithm = config.get('identity.soprovider.encryption_algorithm',
None)
return identity.encrypt_pw_with_algorithm(algorithm, password)
def create_provider_model(self):
@@ -279,57 +279,20 @@
return SqlObjectIdentity(user=user)
-class TG_VisitIdentity(SQLObject):
- """A visit to your website."""
-
- class sqlmeta:
- table = "visit_identity"
-
- visit_key = StringCol(length=40, alternateID=True,
- alternateMethodName="by_visit_key")
- user_id = IntCol()
-
-
-class TG_Group(SQLObject):
- """An ultra-simple group definition."""
-
- 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("TG_User", intermediateTable="user_group",
- joinColumn="group_id", otherColumn="user_id")
-
- # collection of all permissions for this group
- permissions = RelatedJoin("TG_Permission", joinColumn="group_id",
- intermediateTable="group_permission",
- otherColumn="permission_id")
-
[email protected]('isinstance(obj, TG_Group)')
-def jsonify_group(obj):
- """Convert group to JSON."""
- result = jsonify_sqlobject(obj)
- result["users"] = [u.user_name for u in obj.users]
- result["permissions"] = [p.permission_name for p in obj.permissions]
- return result
-
-
class TG_User(SQLObject):
"""Reasonably basic User definition."""
user_name = UnicodeCol(length=16, alternateID=True,
- alternateMethodName="by_user_name")
+ alternateMethodName='by_user_name')
email_address = UnicodeCol(length=255, alternateID=True,
- alternateMethodName="by_email_address")
+ 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("TG_Group", intermediateTable="user_group",
- joinColumn="user_id", otherColumn="group_id")
+ groups = RelatedJoin('TG_Group', intermediateTable='user_group',
+ joinColumn='user_id', otherColumn='group_id')
def _get_permissions(self):
perms = set()
@@ -360,9 +323,35 @@
def jsonify_user(obj):
"""Convert user to JSON."""
result = jsonify_sqlobject(obj)
- del result['password']
- result["groups"] = [g.group_name for g in obj.groups]
- result["permissions"] = [p.permission_name for p in obj.permissions]
+ result.pop('password', None)
+ result['groups'] = [g.group_name for g in obj.groups]
+ result['permissions'] = [p.permission_name for p in obj.permissions]
+ return result
+
+
+class TG_Group(SQLObject):
+ """An ultra-simple group definition."""
+
+ 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('TG_User', intermediateTable='user_group',
+ joinColumn='group_id', otherColumn='user_id')
+
+ # collection of all permissions for this group
+ permissions = RelatedJoin('TG_Permission', joinColumn='group_id',
+ intermediateTable='group_permission',
+ otherColumn='permission_id')
+
[email protected]('isinstance(obj, TG_Group)')
+def jsonify_group(obj):
+ """Convert group to JSON."""
+ result = jsonify_sqlobject(obj)
+ result['users'] = [u.user_name for u in obj.users]
+ result['permissions'] = [p.permission_name for p in obj.permissions]
return result
@@ -370,23 +359,34 @@
"""Permissions for a given group."""
class sqlmeta:
- table = "permission"
+ table = 'permission'
permission_name = UnicodeCol(length=16, alternateID=True,
- alternateMethodName="by_permission_name")
+ alternateMethodName='by_permission_name')
description = UnicodeCol(length=255)
- groups = RelatedJoin("TG_Group", intermediateTable="group_permission",
- joinColumn="permission_id", otherColumn="group_id")
+ groups = RelatedJoin('TG_Group', intermediateTable='group_permission',
+ joinColumn='permission_id', otherColumn='group_id')
@jsonify.when('isinstance(obj, TG_Permission)')
def jsonify_permission(obj):
"""Convert permissions to JSON."""
result = jsonify_sqlobject(obj)
- result["groups"] = [g.group_name for g in obj.groups]
+ result['groups'] = [g.group_name for g in obj.groups]
return result
+class TG_VisitIdentity(SQLObject):
+ """A visit to your website."""
+
+ class sqlmeta:
+ table = 'visit_identity'
+
+ visit_key = StringCol(length=40, alternateID=True,
+ alternateMethodName='by_visit_key')
+ user_id = IntCol()
+
+
def encrypt_password(cleartext_password):
"""Encrypt given cleartext password."""
try:
Modified: branches/1.5/turbogears/qstemplates/quickstart/+package+/json.py_tmpl
==============================================================================
--- branches/1.5/turbogears/qstemplates/quickstart/+package+/json.py_tmpl
Mon Jan 17 03:33:27 2011 (r7204)
+++ branches/1.5/turbogears/qstemplates/quickstart/+package+/json.py_tmpl
Mon Jan 17 06:36:05 2011 (r7205)
@@ -32,8 +32,8 @@
#elif $identity == "sqlalchemy"
result = jsonify_saobject(obj)
#end if
- result["users"] = [u.user_name for u in obj.users]
- result["permissions"] = [p.permission_name for p in obj.permissions]
+ result['users'] = [u.user_name for u in obj.users]
+ result['permissions'] = [p.permission_name for p in obj.permissions]
return result
@jsonify.when('isinstance(obj, User)')
@@ -43,14 +43,12 @@
#elif $identity == "sqlalchemy"
result = jsonify_saobject(obj)
#end if
- try: del result['password']
- except KeyError: pass
+ result.pop('password', None)
#if $identity == "sqlalchemy"
- try: del result['_password']
- except KeyError: pass
+ result.pop('_password', None)
#end if
- result["groups"] = [g.group_name for g in obj.groups]
- result["permissions"] = [p.permission_name for p in obj.permissions]
+ result['groups'] = [g.group_name for g in obj.groups]
+ result['permissions'] = [p.permission_name for p in obj.permissions]
return result
@jsonify.when('isinstance(obj, Permission)')
@@ -60,6 +58,6 @@
#elif $identity == "sqlalchemy"
result = jsonify_saobject(obj)
#end if
- result["groups"] = [g.group_name for g in obj.groups]
+ result['groups'] = [g.group_name for g in obj.groups]
return result
#end if
Modified: branches/1.5/turbogears/qstemplates/quickstart/+package+/model.py_tmpl
==============================================================================
--- branches/1.5/turbogears/qstemplates/quickstart/+package+/model.py_tmpl
Mon Jan 17 03:33:27 2011 (r7204)
+++ branches/1.5/turbogears/qstemplates/quickstart/+package+/model.py_tmpl
Mon Jan 17 06:36:05 2011 (r7205)
@@ -130,7 +130,7 @@
class Group(SQLObject):
- """An ultra-simple group definition."""
+ """An ultra-simple Group definition."""
# names like "user" and "group" are reserved words in SQL
# so we set the name to something safe for SQL
@@ -258,7 +258,7 @@
class Group(Entity):
- """An ultra-simple group definition."""
+ """An ultra-simple Group definition."""
using_options(tablename='tg_group')
@@ -439,7 +439,7 @@
class Group(object):
- """An ultra-simple group definition."""
+ """An ultra-simple Group definition."""
def __repr__(self):
return '<Group: name="%s", display_name="%s">' % (
Modified: branches/1.5/turbogears/visit/savisit.py
==============================================================================
--- branches/1.5/turbogears/visit/savisit.py Mon Jan 17 03:33:27 2011
(r7204)
+++ branches/1.5/turbogears/visit/savisit.py Mon Jan 17 06:36:05 2011
(r7205)
@@ -31,16 +31,13 @@
log.error(msg)
else:
log.info("Successfully loaded '%s'", visit_class_path)
-
if visit_class is TG_Visit:
- # Handle it gracefully when TG_Visit is already mapped.
- # May happen, when the visit manager is shutdown and started again
try:
class_mapper(visit_class)
except UnmappedClassError:
- mapper(visit_class, visits_table)
- # base-class' __init__ triggers self.create_model, so mappers need to
- # be initialized before.
+ visit_class._map()
+ # base-class' __init__ triggers self.create_model,
+ # so mappers need to be initialized before.
super(SqlAlchemyVisitManager, self).__init__(timeout)
def create_model(self):
@@ -91,17 +88,18 @@
values=dict(expiry=expiry)))
-# The Visit table
-
-visits_table = Table('tg_visit', metadata,
- Column('visit_key', String(40), primary_key=True),
- Column('created', DateTime, nullable=False, default=datetime.now),
- Column('expiry', DateTime)
-)
-
+# The default Visit model class
class TG_Visit(object):
@classmethod
def lookup_visit(cls, visit_key):
return session.query(cls).get(visit_key)
+
+ @classmethod
+ def _map(cls):
+ cls._table = Table('visit', metadata,
+ Column('visit_key', String(40), primary_key=True),
+ Column('created', DateTime, nullable=False, default=datetime.now),
+ Column('expiry', DateTime))
+ cls._mapper = mapper(cls, cls._table)