From 55124e861faa6b3c51194898143751b09f670fc0 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Thu, 5 Jun 2008 16:24:43 +0000 Subject: [PATCH] * infrastructure for authkit * menu generation code * improved templates * database and basic role setup in websetup.py --- development.ini | 9 +- gnuviechadminweb/config/middleware.py | 3 + gnuviechadminweb/controllers/login.py | 22 ++ gnuviechadminweb/controllers/menu.py | 13 ++ gnuviechadminweb/lib/base.py | 6 +- gnuviechadminweb/model/GVAUsers.py | 200 ++++++++++++++++++ gnuviechadminweb/model/__init__.py | 5 +- gnuviechadminweb/model/menu.py | 88 +++----- gnuviechadminweb/model/tables.py | 34 +++ gnuviechadminweb/model/user.py | 30 +++ gnuviechadminweb/public/stylesheets/gva.css | 10 + gnuviechadminweb/templates/loginform.mako | 11 + gnuviechadminweb/templates/main.mako | 4 +- gnuviechadminweb/templates/menuindex.mako | 6 + gnuviechadminweb/templates/site.mako | 21 +- .../tests/functional/test_auth.py | 7 + .../tests/functional/test_login.py | 7 + .../tests/functional/test_menu.py | 7 + gnuviechadminweb/websetup.py | 14 +- setup.py | 2 +- 20 files changed, 431 insertions(+), 68 deletions(-) create mode 100644 gnuviechadminweb/controllers/login.py create mode 100644 gnuviechadminweb/controllers/menu.py create mode 100644 gnuviechadminweb/model/GVAUsers.py create mode 100644 gnuviechadminweb/model/tables.py create mode 100644 gnuviechadminweb/model/user.py create mode 100644 gnuviechadminweb/public/stylesheets/gva.css create mode 100644 gnuviechadminweb/templates/loginform.mako create mode 100644 gnuviechadminweb/templates/menuindex.mako create mode 100644 gnuviechadminweb/tests/functional/test_auth.py create mode 100644 gnuviechadminweb/tests/functional/test_login.py create mode 100644 gnuviechadminweb/tests/functional/test_menu.py diff --git a/development.ini b/development.ini index 3bd319d..0eb1c99 100644 --- a/development.ini +++ b/development.ini @@ -20,7 +20,7 @@ use = egg:gnuviechadminweb full_stack = true cache_dir = %(here)s/data beaker.session.key = gnuviechadminweb -beaker.session.secret = somesecret +beaker.session.secret = realy secret data # If you'd like to fine-tune the individual locations of the cache data dirs # for the Cache data, or the Session saves, un-comment the desired settings @@ -35,6 +35,13 @@ beaker.session.secret = somesecret sqlalchemy.url = sqlite:///%(here)s/gvaweb.sqlite +authkit.setup.method = form, cookie +authkit.form.authenticate.user.type = gnuviechadminweb.model:GVAUsers +authkit.form.authenticate.user.data = gnuviechadminweb.model.meta +authkit.form.authenticate.user.encrypt = authkit.users:md5 +authkit.cookie.secret = really secret data +authkit.cookie.signoutpath = /login/logout + # Logging configuration [loggers] diff --git a/gnuviechadminweb/config/middleware.py b/gnuviechadminweb/config/middleware.py index 148c3b2..b515b85 100644 --- a/gnuviechadminweb/config/middleware.py +++ b/gnuviechadminweb/config/middleware.py @@ -10,6 +10,8 @@ from pylons.middleware import error_mapper, ErrorDocuments, ErrorHandler, \ StaticJavascripts from pylons.wsgiapp import PylonsApp +import authkit.authenticate + from gnuviechadminweb.config.environment import load_environment def make_app(global_conf, full_stack=True, **app_conf): @@ -43,6 +45,7 @@ def make_app(global_conf, full_stack=True, **app_conf): app = ErrorHandler(app, global_conf, error_template=error_template, **config['pylons.errorware']) + app = authkit.authenticate.middleware(app, app_conf) # Display error documents for 401, 403, 404 status codes (and # 500 when debug is disabled) app = ErrorDocuments(app, global_conf, mapper=error_mapper, **app_conf) diff --git a/gnuviechadminweb/controllers/login.py b/gnuviechadminweb/controllers/login.py new file mode 100644 index 0000000..f8163fb --- /dev/null +++ b/gnuviechadminweb/controllers/login.py @@ -0,0 +1,22 @@ +import logging + +from gnuviechadminweb.lib.base import * +from authkit.permissions import ValidAuthKitUser +from authkit.authorize.pylons_adaptors import authorize + +log = logging.getLogger(__name__) + +class LoginController(BaseController): + + def index(self): + # Return a rendered template + return render('/loginform.mako') + + @authorize(ValidAuthKitUser()) + def login(self): + c.messages['messages'].append('Successfully logged in.') + return render('/main.mako') + + def logout(self): + c.messages['messages'].append('You are logged out.') + return render('/main.mako') diff --git a/gnuviechadminweb/controllers/menu.py b/gnuviechadminweb/controllers/menu.py new file mode 100644 index 0000000..799bdee --- /dev/null +++ b/gnuviechadminweb/controllers/menu.py @@ -0,0 +1,13 @@ +import logging + +from gnuviechadminweb.lib.base import * +from authkit.permissions import HasAuthKitRole +from authkit.authorize.pylons_adaptors import authorize + +log = logging.getLogger(__name__) + +class MenuController(BaseController): + @authorize(HasAuthKitRole(["menuedit"])) + def index(self): + # Return a rendered template + return render('/menuindex.mako') diff --git a/gnuviechadminweb/lib/base.py b/gnuviechadminweb/lib/base.py index 8b3b5bc..65315a3 100644 --- a/gnuviechadminweb/lib/base.py +++ b/gnuviechadminweb/lib/base.py @@ -20,8 +20,10 @@ class BaseController(WSGIController): """Invoke the Controller""" conn = meta.engine.connect() meta.Session.configure(bind=conn) - c.menu = model.Menu.allowed(session['user'] if 'user' in session \ - else None) + c.menu = model.Menu.allowed(environ['authkit.users'].user_roles( + environ['REMOTE_USER']) if 'REMOTE_USER' in environ else []) + c.messages = {'errors' : [], + 'messages' : []} try: # WSGIController.__call__ dispatches to the Controller method # the request is routed to. This routing information is diff --git a/gnuviechadminweb/model/GVAUsers.py b/gnuviechadminweb/model/GVAUsers.py new file mode 100644 index 0000000..1e4f558 --- /dev/null +++ b/gnuviechadminweb/model/GVAUsers.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +from authkit.users import Users +from gnuviechadminweb.model.user import Group, Role, User +from paste.util.import_string import eval_import +import logging + +log = logging.getLogger(__name__) + +def needsConnection(func): + def wrapper(*__args, **__kw): + from sqlalchemy.orm import create_session + engine = __args[0].meta.engine + conn = engine.contextual_connect() + if conn.closed: + conn = engine.connect() + __args[0].session = create_session(bind=conn) + try: + return func(*__args, **__kw) + finally: + conn.close() + else: + __args[0].session = create_session(bind=conn) + return func(*__args, **__kw) + return wrapper + +class GVAUsers(Users): + def __init__(self, data, encrypt = None): + Users.__init__(self, data, encrypt) + log.debug("in __init__") + self.meta = eval_import(self.data) + self.session = self.meta.Session + + @needsConnection + def _getSession(self): + return self.session + + def _get_group(self, groupname): + return self._getSession().query(Group).filter_by(name=groupname).one() + + def _get_user(self, username): + return self._getSession().query(User).filter_by(name=username).one() + + def _get_role(self, rolename): + return self._getSession().query(Role).filter_by(name=rolename).one() + + # Create Methods + def user_create(self, username, password, group=None): + n_user = User() + n_user.name = username + n_user.password = self.encrypt(password) + if group: + n_user.group = self._get_group(group) + self._getSession().save(n_user) + self._getSession().commit() + + def role_create(self, role): + n_role = Role() + n_role.name = role + self._getSession().save(n_role) + self._getSession().commit() + + def group_create(self, group): + n_group = Group() + n_group.name = group + self._getSession().save(n_group) + self._getSession().commit() + + # Delete Methods + def user_delete(self, username): + self._getSession().delete(self._get_user(username)) + self._getSession().commit() + + def role_delete(self, role): + self._getSession().delete(self._get_role(role)) + self._getSession().commit() + + def group_delete(self, group): + self._getSession().delete(self._get_group()) + self._getSession().commit() + + # Delete Cascade Methods + def role_delete_cascade(self, role): + n_role = self._get_role(role) + for user in self._getSession().query(User).roles.any(name=role).all(): + del user.roles[n_role.id] + self._getSession().delete(n_role) + self._getSession().commit() + + def group_delete_cascade(self, group): + n_group = self._get_group(group) + self._getSession().delete(self._getSession().query(User).filter_by( + User.group==n_group)) + self._getSession().delete(n_group) + self._getSession().commit() + + # Existence Methods + def user_exists(self, username): + return self._getSession().query(User).filter_by( + name=username).count() == 1 + + def role_exists(self, role): + return self._getSession().query(Role).filter_by( + name=role).count() == 1 + + def group_exists(self, group): + return self._getSession().query(Group).filter_by( + name=group).count() == 1 + + # List Methods + def list_roles(self): + return [role.name.lower() for role in self._getSession().query( + Role).all()] + + def list_users(self): + return [user.name.lower() for user in self._getSession().query( + User).all()] + + def list_groups(self): + return [group.name.lower() for group in self._getSession().query( + Group).all()] + + # User Methods + def user(self, username): + user = self._get_user(username) + roles = [role.name.lower() for role in user.roles] + roles.sort() + return { + 'username' : user.name, + 'group' : None if user.group is None else user.group.name, + 'password' : user.password, + 'roles' : roles + } + + def user_roles(self, username): + user = self._get_user(username) + roles = [role.name.lower() for role in user.roles] + roles.sort() + return roles + + def user_group(self, username): + user = self._get_user(username) + return None if user.group is None else user.group.name + + def user_password(self, username): + user = self._get_user(username) + return user.password + + def user_has_role(self, username, role): + user = self._get_user(username) + return role in [role.name for role in user.roles] + + def user_has_group(self, username, group): + user = self._get_user(username) + return (group is None and user.group is None) or \ + (group is not None and user.group is not None and \ + group == user.group.name) + + def user_has_password(self, username, password): + user = self._get_user(username) + return user.password == self.encrypt(password) + + def user_set_username(self, username, new_username): + user = self._get_user(username) + user.name = new_username + self._getSession().update(user) + self._getSession().commit() + + def user_set_group(self, username, group, add_if_necessary=False): + if add_if_necessary and self._getSession().query(Group).filter_by( + name=group).count() == 0: + self.group_create(group) + groupobj = self._get_group(group) + user = self._get_user(user) + user.group = groupobj + self._getSession().update(user) + self._getSession().commit() + + def user_add_role(self, username, role, add_if_necessary=False): + if add_if_necessary and self._getSession().query(Role).filter_by( + name=role).count() == 0: + self.role_create(role) + roleobj = self._get_role(role) + user = self._get_user(username) + if not roleobj in user.roles: + user.roles.append(roleobj) + self._getSession().update(user) + self._getSession().commit() + + def user_remove_role(self, username, role): + roleobj = self._get_role(role) + user = self._get_user(username) + if roleobj in user.roles: + del user.roles[roleobj.id] + self._getSession().commit() + + def user_remove_group(self, username): + user = self._get_user(username) + user.group = None + self._getSession().update(user) + self._getSession().commit() diff --git a/gnuviechadminweb/model/__init__.py b/gnuviechadminweb/model/__init__.py index 9ec32d0..c05d1a1 100644 --- a/gnuviechadminweb/model/__init__.py +++ b/gnuviechadminweb/model/__init__.py @@ -11,5 +11,6 @@ def init_model(engine): meta.engine = engine meta.Session = orm.scoped_session(sm) -from gnuviechadminweb.model import menu -from gnuviechadminweb.model.menu import Menu, User, Role +from gnuviechadminweb.model.user import Group, Role, User +from gnuviechadminweb.model.GVAUsers import GVAUsers +from gnuviechadminweb.model.menu import Menu diff --git a/gnuviechadminweb/model/menu.py b/gnuviechadminweb/model/menu.py index f39ac5e..557df55 100644 --- a/gnuviechadminweb/model/menu.py +++ b/gnuviechadminweb/model/menu.py @@ -1,64 +1,36 @@ -# -*- python -*- # -*- coding: utf-8 -*- -import sqlalchemy as sa -from sqlalchemy import orm - -from gnuviechadminweb.model import meta - -t_menu = \ - sa.Table("menu", meta.metadata, - sa.Column("id", sa.types.Integer, primary_key=True), - sa.Column("title", sa.types.String(40), nullable=False), - sa.Column("controller", sa.types.String(40), nullable=False), - sa.Column("action", sa.types.String(40), nullable=False) - ) - -t_user = \ - sa.Table("user", meta.metadata, - sa.Column("id", sa.types.Integer, primary_key=True), - sa.Column("name", sa.types.String(40), nullable=False), - sa.Column("password", sa.types.String(128), nullable=False) - ) - -t_role = \ - sa.Table("role", meta.metadata, - sa.Column("id", sa.types.Integer, primary_key=True), - sa.Column("name", sa.types.String(40), nullable=False) - ) - -t_menu_role = \ - sa.Table("menu_role", meta.metadata, - sa.Column("id", sa.types.Integer, primary_key=True), - sa.Column("menu_id", sa.types.Integer, sa.ForeignKey(t_menu.c.id)), - sa.Column("role_id", sa.types.Integer, sa.ForeignKey(t_role.c.id)) - ) - -t_user_role = \ - sa.Table("user_role", meta.metadata, - sa.Column("id", sa.types.Integer, primary_key=True), - sa.Column("user_id", sa.types.Integer, sa.ForeignKey(t_user.c.id)), - sa.Column("role_id", sa.types.Integer, sa.ForeignKey(t_role.c.id)) - ) class Menu(object): - @classmethod - def allowed(cls, user=None): - menu_q = meta.Session.query(cls) - return menu_q.all() + menuitems = [ + ('Main page', 'gva', 'index', None), + ('Login', 'login', 'login', []), + ('Logout', 'login', 'logout', ['*']), + ('Admin', 'gva', 'index', ['admin']) + ] -class User(object): - pass + @classmethod + def allowed(cls, roles=[]): + items = [] + for item in cls.menuitems: + additem = False + if item[3] is None: + additem = True + elif len(item[3]) == 0 and len(roles) == 0: + additem = True + elif len(roles) > 0: + for role in item[3]: + if role in roles or role == '*': + additem = True + break + if additem: + items.append(Menu(item[0], item[1], item[2])) + return items -class Role(object): - pass + def __init__(self, title, controller, action): + self.title = title + self.controller = controller + self.action = action -orm.mapper(Menu, t_menu, { - 'roles' : orm.relation(Role, secondary = t_menu_role), - }) -orm.mapper(Role, t_role, properties = { - 'users' : orm.relation(User, secondary = t_user_role), - 'menus' : orm.relation(Menu, secondary = t_menu_role), - }) -orm.mapper(User, t_role, properties = { - 'roles' : orm.relation(User, secondary = t_user_role) - }) + + + diff --git a/gnuviechadminweb/model/tables.py b/gnuviechadminweb/model/tables.py new file mode 100644 index 0000000..9b5625b --- /dev/null +++ b/gnuviechadminweb/model/tables.py @@ -0,0 +1,34 @@ +# -*- python -*- +# -*- coding: utf-8 -*- +import sqlalchemy as sa + +from gnuviechadminweb.model import meta + +t_group = \ + sa.Table("group", meta.metadata, + sa.Column("id", sa.types.Integer, primary_key=True), + sa.Column("name", sa.types.String(40), nullable=False, + unique=True), + ) + +t_role = \ + sa.Table("role", meta.metadata, + sa.Column("id", sa.types.Integer, primary_key=True), + sa.Column("name", sa.types.String(40), nullable=False) + ) + +t_user = \ + sa.Table("user", meta.metadata, + sa.Column("id", sa.types.Integer, primary_key=True), + sa.Column("name", sa.types.String(40), nullable=False), + sa.Column("password", sa.types.String(128), nullable=False), + sa.Column("group_id", sa.types.Integer, + sa.ForeignKey(t_group.c.id)) + ) + +t_user_role = \ + sa.Table("user_role", meta.metadata, + sa.Column("id", sa.types.Integer, primary_key=True), + sa.Column("user_id", sa.types.Integer, sa.ForeignKey(t_user.c.id)), + sa.Column("role_id", sa.types.Integer, sa.ForeignKey(t_role.c.id)) + ) diff --git a/gnuviechadminweb/model/user.py b/gnuviechadminweb/model/user.py new file mode 100644 index 0000000..bd6f2f5 --- /dev/null +++ b/gnuviechadminweb/model/user.py @@ -0,0 +1,30 @@ +# -*- python -*- +# -*- coding: utf-8 -*- +from sqlalchemy import orm + +from gnuviechadminweb.model.tables import * +import logging + +log = logging.getLogger(__name__) + +class Group(object): + pass + +class Role(object): + pass + +class User(object): + pass + +orm.mapper(Group, t_group, { + 'users' : orm.relation(User), + }) + +orm.mapper(Role, t_role, properties = { + 'users' : orm.relation(User, secondary = t_user_role), + }) + +orm.mapper(User, t_user, properties = { + 'roles' : orm.relation(Role, secondary = t_user_role), + 'group' : orm.relation(Group) + }) diff --git a/gnuviechadminweb/public/stylesheets/gva.css b/gnuviechadminweb/public/stylesheets/gva.css new file mode 100644 index 0000000..aa95828 --- /dev/null +++ b/gnuviechadminweb/public/stylesheets/gva.css @@ -0,0 +1,10 @@ +html { + font-family:sans; +} + +#menu { + float:left; + padding-right:10px; + border-right:1px solid black; + margin-right:10px; +} \ No newline at end of file diff --git a/gnuviechadminweb/templates/loginform.mako b/gnuviechadminweb/templates/loginform.mako new file mode 100644 index 0000000..f7946a1 --- /dev/null +++ b/gnuviechadminweb/templates/loginform.mako @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +<%inherit file="site.mako" /> +

Login

+${h.form(h.url(action='login'), method='post')} +User name: ${h.text_field('username')} +Password: ${h.password_field('password')} +${h.submit('Login')} +${h.end_form()} +<%def name="headlines()"> + Login + \ No newline at end of file diff --git a/gnuviechadminweb/templates/main.mako b/gnuviechadminweb/templates/main.mako index 1d862da..f4ec6ee 100644 --- a/gnuviechadminweb/templates/main.mako +++ b/gnuviechadminweb/templates/main.mako @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- <%inherit file="site.mako" />

Main page

-<%def name="title()">Main page \ No newline at end of file +<%def name="headlines()"> + Main page + \ No newline at end of file diff --git a/gnuviechadminweb/templates/menuindex.mako b/gnuviechadminweb/templates/menuindex.mako new file mode 100644 index 0000000..19a5646 --- /dev/null +++ b/gnuviechadminweb/templates/menuindex.mako @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +<%inherit file="site.mako" /> +

Menu editor

+<%def name="headlines()"> + Menu editor + \ No newline at end of file diff --git a/gnuviechadminweb/templates/site.mako b/gnuviechadminweb/templates/site.mako index 7db9f30..290a62c 100644 --- a/gnuviechadminweb/templates/site.mako +++ b/gnuviechadminweb/templates/site.mako @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- -${self.title()} +${self.headlines()} ${h.javascript_include_tag('jquery.js')} +${h.stylesheet_link_tag('gva.css')} +% if c.messages['errors']: + +% endif +% if c.messages['messages']: + +% endif
${next.body()}
-<%def name="title()">Site \ No newline at end of file +<%def name="headlines()"> + Site + \ No newline at end of file diff --git a/gnuviechadminweb/tests/functional/test_auth.py b/gnuviechadminweb/tests/functional/test_auth.py new file mode 100644 index 0000000..4685fb4 --- /dev/null +++ b/gnuviechadminweb/tests/functional/test_auth.py @@ -0,0 +1,7 @@ +from gnuviechadminweb.tests import * + +class TestAuthController(TestController): + + def test_index(self): + response = self.app.get(url_for(controller='auth')) + # Test response... diff --git a/gnuviechadminweb/tests/functional/test_login.py b/gnuviechadminweb/tests/functional/test_login.py new file mode 100644 index 0000000..e1ccddb --- /dev/null +++ b/gnuviechadminweb/tests/functional/test_login.py @@ -0,0 +1,7 @@ +from gnuviechadminweb.tests import * + +class TestLoginController(TestController): + + def test_index(self): + response = self.app.get(url_for(controller='login')) + # Test response... diff --git a/gnuviechadminweb/tests/functional/test_menu.py b/gnuviechadminweb/tests/functional/test_menu.py new file mode 100644 index 0000000..78888de --- /dev/null +++ b/gnuviechadminweb/tests/functional/test_menu.py @@ -0,0 +1,7 @@ +from gnuviechadminweb.tests import * + +class TestMenuController(TestController): + + def test_index(self): + response = self.app.get(url_for(controller='menu')) + # Test response... diff --git a/gnuviechadminweb/websetup.py b/gnuviechadminweb/websetup.py index db29ed2..1ee2fe5 100644 --- a/gnuviechadminweb/websetup.py +++ b/gnuviechadminweb/websetup.py @@ -5,7 +5,9 @@ from paste.deploy import appconfig from pylons import config from gnuviechadminweb.config.environment import load_environment -from gnuviechadminweb.model import meta +from gnuviechadminweb.model import meta, User, Group, Role +from gnuviechadminweb.model.GVAUsers import GVAUsers +from authkit.users import md5 log = logging.getLogger(__name__) @@ -13,6 +15,16 @@ def setup_config(command, filename, section, vars): """Place any commands to setup gnuviechadminweb here""" conf = appconfig('config:' + filename) load_environment(conf.global_conf, conf.local_conf) + log.info("Creating tables") meta.metadata.create_all(bind=meta.engine) + + log.info("Creating default admin user, role and group") + users = GVAUsers('gnuviechadminweb.model:meta', md5) + users.role_create("admin") + users.group_create("administrators") + + users.user_create("admin", "admin", "administrators") + users.user_add_role("admin", "admin") + log.info("Successfully setup") diff --git a/setup.py b/setup.py index b937187..ba53588 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ setup( author='Jan Dittberner', author_email='jan@dittberner.info', url='http://www.gnuviech-server.de/projects/gnuviechadminweb', - install_requires=["Pylons>=0.9.6.2", "Elixir>=0.5.2"], + install_requires=["Pylons>=0.9.6.2", "SQLAlchemy>=0.4", "AuthKit>=0.4"], packages=find_packages(exclude=['ez_setup']), include_package_data=True, test_suite='nose.collector',