Skip to content

Commit

Permalink
Add a user database interface and implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
dnouri committed Feb 15, 2011
1 parent bfd9ac0 commit ac07484
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 6 deletions.
10 changes: 10 additions & 0 deletions README.rst
Expand Up @@ -95,6 +95,16 @@ The ``kotti.session_factory`` configuration variable allows the
overriding of the default session factory, which is
`pyramid.session.UnencryptedCookieSessionFactoryConfig`_.

*kotti.users*
-------------

Kotti comes with a default user database implementation in
``kotti.security.users``. You can use the ``kotti.users``
configuration variable to override the implementation used. The
default looks like this::

kotti.users = kotti.security.users

*kotti.templates.master_view* and *kotti.templates.master_edit*
---------------------------------------------------------------

Expand Down
8 changes: 6 additions & 2 deletions kotti/__init__.py
@@ -1,5 +1,3 @@
from kotti.security import list_groups_callback

from sqlalchemy import engine_from_config
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
Expand Down Expand Up @@ -29,7 +27,11 @@ def __getitem__(self, key):
else:
return value

def __getattr__(self, key):
return self[key]

def authtkt_factory(**kwargs):
from kotti.security import list_groups_callback
kwargs.setdefault('callback', list_groups_callback)
return AuthTktAuthenticationPolicy(**kwargs)

Expand All @@ -56,6 +58,7 @@ def none_factory(**kwargs):
'kotti.authentication_policy_factory': 'kotti.authtkt_factory',
'kotti.authorization_policy_factory': 'kotti.acl_factory',
'kotti.session_factory': 'kotti.cookie_session_factory',
'kotti.users': 'kotti.security.users',
},
dotted_names=set([
'kotti.configurators',
Expand All @@ -64,6 +67,7 @@ def none_factory(**kwargs):
'kotti.authentication_policy_factory',
'kotti.authorization_policy_factory',
'kotti.session_factory',
'kotti.users',
]),
)

Expand Down
6 changes: 3 additions & 3 deletions kotti/resources.py
Expand Up @@ -23,12 +23,12 @@
from pyramid.traversal import resource_path
from pyramid.security import view_execution_permitted

from kotti.util import JsonType
from kotti.security import ACL

metadata = MetaData()
DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))

from kotti.util import JsonType
from kotti.security import ACL

class Container(object, DictMixin):
"""Containers form the API of a Node that's used for subitem
access and in traversal.
Expand Down
87 changes: 86 additions & 1 deletion kotti/security.py
@@ -1,7 +1,21 @@
from datetime import datetime
from UserDict import DictMixin

from sqlalchemy import Table
from sqlalchemy import Column
from sqlalchemy import Unicode
from sqlalchemy import DateTime
from sqlalchemy.orm import mapper
from sqlalchemy.orm.exc import NoResultFound
from pyramid.location import lineage
from pyramid.security import Allow
from pyramid.security import ALL_PERMISSIONS

from kotti import configuration
from kotti.resources import DBSession
from kotti.resources import metadata
from kotti.util import JsonType

ALL_PERMISSIONS_SERIALIZED = '__ALL_PERMISSIONS__'

class ACL(object):
Expand Down Expand Up @@ -56,9 +70,13 @@ def list_groups_raw(userid, context):
return set()

def list_groups(userid, context, _seen=None):
groups = set()
if _seen is None:
user = get_users().get(userid)
if user is not None:
groups.update(user.groups)
_seen = set()
groups = set()

for item in lineage(context):
groups.update(list_groups_raw(userid, item))

Expand All @@ -78,3 +96,70 @@ def set_groups(userid, context, groups_to_set):

def list_groups_callback(userid, request):
return list_groups(userid, request.context)

def get_users():
return configuration['kotti.users'][0]

class Users(DictMixin):
"""Kotti's default user database.
Promises dict-like access to user profiles, and a 'query' method
for finding users.
This is a default implementation that may be replaced by using the
'kotti.users' configuration variable.
"""
def __getitem__(self, key):
key = unicode(key)
session = DBSession()
try:
return session.query(User).filter(User.id==key).one()
except NoResultFound:
raise KeyError(key)

def __setitem__(self, key, user):
key = unicode(key)
session = DBSession()
if isinstance(user, dict):
profile = User(**user)
session.add(profile)

def __delitem__(self, key):
key = unicode(key)
session = DBSession()
try:
user = session.query(User).filter(User.id==key).one()
session.delete(user)
except NoResultFound:
raise KeyError(key)

def keys(self):
session = DBSession()
for (userid,) in session.query(User.id):
yield userid

def query(self, **kwargs):
session = DBSession()
query = session.query(User)
for key, value in kwargs.items():
attr = getattr(User, key)
query = query.filter(attr.like(value))
return query

class User(object):
def __init__(self, id, title=None, groups=()):
self.id = id
self.title = title
self.groups = groups
self.creation_date = datetime.now()

users = Users()

users_table = Table('users', metadata,
Column('id', Unicode(100), primary_key=True),
Column('title', Unicode(100)),
Column('groups', JsonType(), nullable=False),
Column('creation_date', DateTime(), nullable=False),
)

mapper(User, users_table)
60 changes: 60 additions & 0 deletions kotti/tests.py
Expand Up @@ -17,6 +17,7 @@
from kotti.security import list_groups_raw
from kotti.security import set_groups
from kotti.security import list_groups_callback
from kotti.security import get_users
from kotti import main

BASE_URL = 'http://localhost:6543'
Expand Down Expand Up @@ -260,6 +261,65 @@ def test_works_with_auth(self):
'bob', 'group:bobsgroup']
)

class TestUser(UnitTestBase):
def _make_bob(self):
users = get_users()
users['bob'] = dict(
id=u'bob', title=u'Bob Dabolina', groups=[u'group:bobsgroup'])

def _assert_is_bob(self, bob):
self.assertEqual(bob.id, u'bob')
self.assertEqual(bob.title, u'Bob Dabolina')
self.assertEqual(bob.groups, [u'group:bobsgroup'])

def test_users_empty(self):
users = get_users()
self.assertRaises(KeyError, users.__getitem__, u'bob')
self.assertRaises(KeyError, users.__delitem__, u'bob')
self.assertEqual(len(list(users.keys())), 0)
self.assertEqual(len(list(users.query())), 0)

def test_users_add_and_remove(self):
self._make_bob()
users = get_users()
self._assert_is_bob(users[u'bob'])
self.assertEqual(list(users.keys()), [u'bob'])

del users['bob']
self.assertRaises(KeyError, users.__getitem__, u'bob')
self.assertRaises(KeyError, users.__delitem__, u'bob')

def test_users_query(self):
users = get_users()
self.assertEqual(list(users.query(title=u"%Bob%")), [])
self._make_bob()
[bob] = list(users.query(id=u"bob"))
self._assert_is_bob(bob)
[bob] = list(users.query(title=u"%Bob%"))
self._assert_is_bob(bob)

def test_groups_from_users(self):
self._make_bob()

session = DBSession()
root = session.query(Node).get(1)
child = root[u'child'] = Node()
session.flush()

self.assertEqual(list_groups('bob', root), ['group:bobsgroup'])

set_groups('group:bobsgroup', root, ['group:editors'])
set_groups('group:editors', child, ['group:foogroup'])

self.assertEqual(
set(list_groups('bob', root)),
set(['group:bobsgroup', 'group:editors'])
)
self.assertEqual(
set(list_groups('bob', child)),
set(['group:bobsgroup', 'group:editors', 'group:foogroup'])
)

class TestEvents(UnitTestBase):
def setUp(self):
super(TestEvents, self).setUp()
Expand Down

0 comments on commit ac07484

Please sign in to comment.