# HG changeset patch # User Filip de Waard # Date 1261047880 -3600 # Node ID c6b7b50597dcd858f552a1473f37d057e1c8f268 # Parent 1be1a0a668cdf71d08acc68b1636659692df5e0d added User model object diff -r 1be1a0a668cdf71d08acc68b1636659692df5e0d -r c6b7b50597dcd858f552a1473f37d057e1c8f268 development.ini --- a/development.ini Tue Dec 15 09:58:02 2009 +0100 +++ b/development.ini Thu Dec 17 12:04:40 2009 +0100 @@ -35,6 +35,7 @@ # execute malicious code after an exception is raised. #set debug = false +couchdb_uri = http://localhost:5984/vix_dev # Logging configuration [loggers] diff -r 1be1a0a668cdf71d08acc68b1636659692df5e0d -r c6b7b50597dcd858f552a1473f37d057e1c8f268 test.ini --- a/test.ini Tue Dec 15 09:58:02 2009 +0100 +++ b/test.ini Thu Dec 17 12:04:40 2009 +0100 @@ -17,5 +17,6 @@ [app:main] use = config:development.ini +couchdb_server = http://localhost:5984/ # Add additional test specific configuration options as necessary. diff -r 1be1a0a668cdf71d08acc68b1636659692df5e0d -r c6b7b50597dcd858f552a1473f37d057e1c8f268 vix/config/environment.py --- a/vix/config/environment.py Tue Dec 15 09:58:02 2009 +0100 +++ b/vix/config/environment.py Thu Dec 17 12:04:40 2009 +0100 @@ -1,6 +1,7 @@ """Pylons environment configuration""" import os +from couchdb.client import Database from mako.lookup import TemplateLookup from pylons import config from pylons.error import handle_mako_error @@ -37,3 +38,6 @@ # CONFIGURATION OPTIONS HERE (note: all config options will override # any Pylons config options) + + #setup CouchDB connection configuration + config['vix.db'] = Database(config['couchdb_uri']) diff -r 1be1a0a668cdf71d08acc68b1636659692df5e0d -r c6b7b50597dcd858f552a1473f37d057e1c8f268 vix/model/__init__.py --- a/vix/model/__init__.py Tue Dec 15 09:58:02 2009 +0100 +++ b/vix/model/__init__.py Thu Dec 17 12:04:40 2009 +0100 @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- + +""" +vix/model/__init__.py: CouchDB models + +Copyright 2009, Net Collective. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +""" + +from datetime import datetime + +import couchdb.schema as schema + +class User(schema.Document): + """ + The User object is the model for a Vix user and includes both + login information (username and password) and other user details. + Apart from the user credentials the object also contains the + permissions dictionary that describes the permissions for this + user for each database. + + For applications that require more information about a user, like + a profile with a link to a homepage or an instant messenger contact, + these extra fields can be added on-the-fly (thanks to the fact + that CouchDB is a schema-less database). + + :param username: unique identifier for the user. + :type username: unicode + :param password: Bcrypt encoded password + :type password: unicode + :param mail: e-mail address. + :type mail: unicode + :param real_name: the real name of the user. + :type real_name: unicode + + """ + + type = schema.TextField(default="user") + created = schema.DateTimeField(default=datetime.utcnow) + + id = username = schema.TextField() + password = schema.TextField() + mail = schema.TextField() + real_name = schema.TextField() + + permissions = schema.ListField(schema.DictField(schema.Schema.build( + database = schema.TextField(), + feed = schema.TextField(), + admin = schema.BooleanField(default=False), + actions = schema.DictField(schema.Schema.build( + GET = schema.BooleanField(default=False), + POST = schema.BooleanField(default=False), + PUT = schema.BooleanField(default=False), + DELETE = schema.BooleanField(default=False) + )) + ))) + + def get_permissions(self, database, feed): + """Returns permission values for given feed and database. + + >>> user.load(database, u'fmw') + >>> user.get_permissions(database=u'vix_tests', feed=u'blog') + [0, {'database': 'vix_tests', 'feed': 'blog', 'admin': True, + 'actions': {'GET': True, 'POST': True, 'PUT': True, 'DELETE': True}}] + + :param database: CouchDB database name. + :type database: unicode + :param feed: Name of the feed you need permissions for. + :type feed: unicode + :return: List with the index of the requested permission as the + first value and a dictionary with the actual permissions as + the second value. + + """ + + for i in range(len(self.permissions)): + p = self.permissions[i] + + if p['database'] == database and p['feed'] == feed: + #FIXME: refactor ugly private attribute call + return [i,p._data] + + def set_permissions(self, database, feed, admin, actions): + """Saves a permission row to the user.permissions list. + + >>> user.load(database, u'fmw') + >>> perms = {'GET': True, 'POST': True, 'PUT': True, 'DELETE': True} + >>> user.set_permissions(database=u'vix_tests', feed=u'blog, + ... admin=True, actions=perms} + + :param database: CouchDB database name. + :type database: unicode + :param feed: Name of the feed where the permissions apply. + :type feed: unicode + :param admin: Determines is the user can modify documents posted + by others (default is False, meaning the user can only edit + his own documents). + :type admin: boolean + :param actions: Dictionary containing keys for every action, + 'GET', 'POST', 'PUT', and 'DELETE', with boolean values. + + """ + + if type(admin) != bool: + raise ValueError(u"The argument 'admin' needs to be a Boolean.") + elif type(actions) != dict: + raise ValueError(u"The argument 'actions' needs to be a dict.") + else: + #FIXME: Refactor actions dictionary validation (this is dirty). + for key in ['GET', 'POST', 'PUT', 'DELETE']: + try: + if type(actions[key]) != bool: + raise ValueError + except Exception: + #raised if KeyError occurs or the type check above + #fails and the ValueError is triggered. + raise ValueError("The argument 'actions' needs to be" + + "a dictionary with 'GET', 'POST', 'PUT' and " + + "'DELETE' as keys and Boolean values.") + + permissions = self.get_permissions(database=database, feed=feed) + + if permissions == None: + self.permissions.append({'database': database, + 'feed': feed, 'admin': admin, 'actions': actions}) + else: + permissions[1]['admin'] = admin + permissions[1]['actions'] = actions + + def delete_permissions(self, database, feed): + """Deletes permissions for given database and feed. + + Note that method should only be used when a feed or database + is removed. Just set all actions to False to remove permissions + to an existing feed. + + :param database: database name. + :type database: unicode + :param feed: feed name. + :type feed: unicode + + """ + + p = self.get_permissions(database, feed) + del self.permissions[p[0]] diff -r 1be1a0a668cdf71d08acc68b1636659692df5e0d -r c6b7b50597dcd858f552a1473f37d057e1c8f268 vix/tests/test_models.py --- a/vix/tests/test_models.py Tue Dec 15 09:58:02 2009 +0100 +++ b/vix/tests/test_models.py Thu Dec 17 12:04:40 2009 +0100 @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- + +""" +vix/tests/test_models.py: Tests for model code. + +Copyright 2009, Net Collective. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +""" + +from datetime import datetime +from unittest import TestCase + +import couchdb + +import vix.model as model + +class TestModel(TestCase): + + def setUp(self): + """Create new test database for every test.""" + + from pylons import config + + self.server = couchdb.client.Server(config['couchdb_server']) + self.db = self.server.create('vix_tests') + + def tearDown(self): + """Delete test database.""" + + try: + del self.server['vix_tests'] + except couchdb.client.ResourceNotFound: + pass + + def test_User(self): + """General test for the User model object.""" + + p = u'$2a$10$zovtWSOSm0PTsiuovPOxC.uEJxsEzVf0AlswKvgT/jtxMqf44.Kpi' + + user = model.User(username=u'fmw', password=p, + mail=u'fmw@vix.io', real_name=u'Filip de Waard') + + #note that there is a known bug in httplib2 0.5 that causes + #conflicts here (because it submits the PUT request twice): + #see: http://code.google.com/p/couchdb-python/issues/detail?id=85 + # and http://code.google.com/p/httplib2/issues/detail?id=67 + #updating httplib2 to the latest development version or any + #version after 11/15/2009 fixes this issue. + user.store(self.db) + + #make sure the data is reloaded straight out of the database + user = model.User() + user = user.load(self.db, u'fmw') + + #check if the basic values contain what we expect: + self.assertEquals(user.id, u'fmw') + self.assertEquals(user.mail, u'fmw@vix.io') + self.assertEquals(user.type, u'user') + self.assertEquals(user.password, + u'$2a$10$zovtWSOSm0PTsiuovPOxC.uEJxsEzVf0AlswKvgT/jtxMqf44.Kpi') + + assert user.id == user.username + + #max difference between current time and .created is μs 9,999,999 μs + timedelta = datetime.utcnow() - user.created + self.assertTrue(timedelta.seconds < 1) + + #validate (password length, username length, required fields) + #duplication + #encoding + + def test_User_permissions(self): + """Test permission handling in the User model object.""" + + p = u'$2a$10$zovtWSOSm0PTsiuovPOxC.uEJxsEzVf0AlswKvgT/jtxMqf44.Kpi' + + user = model.User(username=u'fmw', password=p, + mail=u'fmw@vix.io', real_name=u'Filip de Waard') + + #test adding permissions + perm_blog = {'GET': True, 'POST': True, 'PUT': True, 'DELETE': True} + perm_news = {'GET': True, 'POST': True, 'PUT': True, 'DELETE': False} + + user.set_permissions(database='vix_tests', feed='blog', admin=True, + actions=perm_blog) + + user.set_permissions(database='vix_tests', feed='news', admin=False, + actions=perm_news) + + user.store(self.db) + + user = model.User() + user = user.load(self.db, u'fmw') + + self.assertEquals(user.get_permissions( + database='vix_tests', feed='blog'), [0, { + 'database': 'vix_tests', 'feed': 'blog', 'admin': True, + 'actions': perm_blog}]) + + self.assertEquals(user.get_permissions( + database='vix_tests', feed='news'), [1, { + 'database': 'vix_tests', 'feed': 'news', 'admin': False, + 'actions': perm_news}]) + + #test updating permissions + user.set_permissions(database='vix_tests', feed='news', admin=True, + actions=perm_blog) + + user.store(self.db) + + user = model.User() + user = user.load(self.db, u'fmw') + + self.assertEquals(user.get_permissions( + database='vix_tests', feed='news'), [1, { + 'database': 'vix_tests', 'feed': 'news', 'admin': True, + 'actions': perm_blog}]) + + #test what happens if we provid invalid input + + #invalid admin value (should be a boolean) + self.assertRaises(ValueError, + user.set_permissions, 'vix_tests', 'news', u'invalid', perm_blog) + + #invalid action: non-Boolean value for one of the elements + perm_news['DELETE'] = u"foo" + + self.assertRaises(ValueError, + user.set_permissions, 'vix_tests', 'news', False, perm_news) + + #invalid action: missing value for one of the elements + del perm_news['DELETE'] + + self.assertRaises(ValueError, + user.set_permissions, 'vix_tests', 'news', False, perm_news) + + #invalid action: not a dictionary + self.assertRaises(ValueError, + user.set_permissions, 'vix_tests', 'news', False, u"unicode") + + #test deleting permissions: + user.delete_permissions(database='vix_tests', feed='news') + user.store(self.db) + + user = model.User() + user = user.load(self.db, u'fmw') + + self.assertEquals(user.get_permissions('vix_tests', 'news'), None)