summary | shortlog | log | commit | commitdiff | tree
raw | patch | inline | side by side (parent: 6a810f3)
raw | patch | inline | side by side (parent: 6a810f3)
author | richard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2> | |
Thu, 25 Jul 2002 07:14:06 +0000 (07:14 +0000) | ||
committer | richard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2> | |
Thu, 25 Jul 2002 07:14:06 +0000 (07:14 +0000) |
Still to do:
. call the security funcs from cgi and mailgw
. change shipped templates to include correct initialisation and remove
the old config vars
... that seems like a lot. The bulk of the work has been done though. Honest :)
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@918 57a73879-2fb5-44c3-a270-3262357dd7e2
. call the security funcs from cgi and mailgw
. change shipped templates to include correct initialisation and remove
the old config vars
... that seems like a lot. The bulk of the work has been done though. Honest :)
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@918 57a73879-2fb5-44c3-a270-3262357dd7e2
15 files changed:
COPYING.txt | [new file with mode: 0644] | patch | blob |
TODO.txt | patch | blob | history | |
doc/Makefile | patch | blob | history | |
doc/default.css | patch | blob | history | |
doc/security.txt | patch | blob | history | |
roundup/admin.py | patch | blob | history | |
roundup/backends/back_anydbm.py | patch | blob | history | |
roundup/cgi_client.py | patch | blob | history | |
roundup/htmltemplate.py | patch | blob | history | |
roundup/mailgw.py | patch | blob | history | |
roundup/security.py | [new file with mode: 0644] | patch | blob |
roundup/volatiledb.py | [new file with mode: 0644] | patch | blob |
test/test_db.py | patch | blob | history | |
test/test_htmltemplate.py | patch | blob | history | |
test/test_security.py | [new file with mode: 0644] | patch | blob |
diff --git a/COPYING.txt b/COPYING.txt
--- /dev/null
+++ b/COPYING.txt
@@ -0,0 +1,23 @@
+
+Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
+Copyright (c) 2002 eKit.com Inc (http://www.ekit.com/)
+
+This module is free software, and you may redistribute it and/or modify
+under the same terms as Python, so long as this copyright message and
+disclaimer are retained in their original form.
+
+IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
+DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OF THIS CODE, EVEN IF BIZAR SOFTWARE PTY LTD HAS BEEN
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
+BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+
+
+The stylesheet included with this package has been copied from the Zope
+management interface and presumably belongs to Digital Creations.
+
diff --git a/TODO.txt b/TODO.txt
index e41bb1e1128f0e0287c2b585881f30afd730bbf6..b5889345504b49ef35836daaeed84f53129d93fb 100644 (file)
--- a/TODO.txt
+++ b/TODO.txt
comparison functions: lt, le, eq, ge, gt. eq and
[value, value, ...] implies "in"
pending hyperdb: make creator, creation and activity available pre-commit
+pending hyperdb: migrate "id" property to be Number type
pending instance: including much simpler upgrade path and the use of
non-Python configuration files (ConfigParser)
pending instance: cleanup to support config (feature request #498658)
roundup: "|roundup-mailgw /instances/dev"
vmbugs: "|roundup-mailgw /instances/dev component=voicemail"
pending project: switch to a Roundup instance for Roundup bug/feature tracking
-active security: finish doc/security.txt (RJ)
+active security: finish doc/security.txt
pending security: implement and use the new logical control mechanisms
pending security: at least an LDAP user database implementation
pending security: authenticate over a secure connection
description of the property. Combine with help for the actual
form element too, eg. how to use the nosy list edit box.
pending web: feature request #507842
-active web: saving of named queries (GM)
ongoing any bugs
done hyperdb: add Boolean and Number types (GM)
done mailgw: better help message (feature request #558562) (RJ)
done security: switch to sessions for web authentication (RJ)
+done web: saving of named queries (GM)
diff --git a/doc/Makefile b/doc/Makefile
index 1f22b0ea466fba622ec2345c470e1f49fc61360b..1141e1432e97b0431a77afc32ef53eb5cfa3b087 100644 (file)
--- a/doc/Makefile
+++ b/doc/Makefile
all: ${COMPILED}
%.html: %.txt
- ${PYTHON} ${STXTOHTML} -d -v $< $@
+ ${PYTHON} ${STXTOHTML} -d $< $@
diff --git a/doc/default.css b/doc/default.css
index e2bfb08eb18f91c2bdb5499ffab59e53504161a5..2495eed223e4c9b42a8f968dcdef2b1d5dd0a929 100644 (file)
--- a/doc/default.css
+++ b/doc/default.css
/*
:Author: David Goodger
:Contact: goodger@users.sourceforge.net
-:date: $Date: 2002-06-24 00:57:23 $
-:version: $Revision: 1.5 $
+:date: $Date: 2002-07-25 07:14:05 $
+:version: $Revision: 1.6 $
:copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
table.footnote {
border-left: solid thin black ;
padding-left: 0.5ex }
+
+@media print {
+ h1 {page-break-before: always; }
+ h1, h2, h3, h4, h5, h6 { page-break-after: avoid; page-break-inside: avoid; }
+ blockquote, pre { page-break-inside: avoid; }
+ ul, ol, dl { page-break-before: avoid; }
+}
+
diff --git a/doc/security.txt b/doc/security.txt
index 4cdb3b0ef6737fbf4b2dfdc60877ffbc1db6ca89..2d3748a77cd188222108714d379994344504868e 100644 (file)
--- a/doc/security.txt
+++ b/doc/security.txt
Security Mechanisms
===================
-:Version: $Revision: 1.11 $
+:Version: $Revision: 1.12 $
Current situation
=================
than the From address. Support for strong identification through digital
signatures should be added.
5. The command-line tool has no logical controls.
+6. The anonymous control needs revising - there should only be one way to be
+ an anonymous user, not two (currently there is user==None and
+ user=='anonymous).
Possible approaches
A security module defines::
- class InMemoryImmutableClass(hyperdb.Class):
- ''' Don't allow changes to this class's nodes.
+ class InMemoryClass(hyperdb.Class):
+ ''' Just be an in-memory class
'''
def __init__(self, db, classname, **properties):
''' Set up an in-memory store for the nodes of this class
'''
def set(self, *args):
- raise ValueError, "%s are immutable"%self.__class__.__name__
+ ''' Set values on the node
+ '''
- class PermissionClass(InMemoryImmutableClass):
+ class PermissionClass(InMemoryClass):
''' Include the default attributes:
- name (String)
- - classname (String)
+ - klass (String)
- description (String)
- The classname may be unset, indicating that this permission is not
+ The klass may be unset, indicating that this permission is not
locked to a particular class. That means there may be multiple
Permissions for the same name for different classes.
'''
- class RoleClass(InMemoryImmutableClass):
+ class RoleClass(InMemoryClass):
''' Include the default attributes:
- name (String, key)
- description (String)
''' Initialise the permission and role classes, and add in the
base roles (for admin user).
'''
- # use a weak ref to avoid circularity
- self.db = weakref.proxy(db)
-
- # create the permission class instance (we only need one))
- self.permission = PermissionClass(db, "permission")
-
- # create the role class instance (we only need one)
- self.role = RoleClass(db, "role")
-
- # the default Roles
- self.addRole(name="User", description="A regular user, no privs")
- self.addRole(name="Admin", description="An admin user, full privs")
- self.addRole(name="No Rego",
- description="A user who can't register")
-
- ee = self.addPermission(name="Edit",
- description="User may edit everthing")
- self.addPermissionToRole('Admin', ee)
- ae = self.addPermission(name="Assign",
- description="User may be assigned to anything")
- self.addPermissionToRole('Admin', ae)
-
- # initialise the permissions and roles needed for the UIs
- from roundup import cgi_client, mailgw
- cgi_client.initialiseSecurity(self)
- mailgw.initialiseSecurity(self)
def hasClassPermission(self, db, classname, permission, userid):
''' Look through all the Roles, and hence Permissions, and see if
'''
- def hasNodePermission(self, db, classname, nodeid, userid, properties):
+ def hasNodePermission(self, db, classname, nodeid, **propspec):
''' Check the named properties of the given node to see if the
userid appears in them. If it does, then the user is granted
this permission check.
- 'propspec' consists of a list of property names. The property
- names must be the name of a property of classname, or a
- KeyError is raised. That property must be a Link or Multilink
- property, or a TypeError is raised.
+ 'propspec' consists of a set of properties and values that
+ must be present on the given node for access to be granted.
- If the property is a Link, the userid must match the property
- value. If the property is a Multilink, the userid must appear
+ If a property is a Link, the value must match the property
+ value. If a property is a Multilink, the value must appear
in the Multilink list.
'''
This function is directly invoked by security.Security.__init__()
as a part of the Security object instantiation.
'''
- newid = security.addPermission(name="Web Access",
- description="User may use the web interface")
- security.addToRole('User', newid)
- security.addToRole('No Rego', newid)
newid = security.addPermission(name="Web Registration",
- description="User may register through the web")
- security.addToRole('User', newid)
+ description="Anonymous users may register through the web")
+ security.addToRole('Anonymous', newid)
The instance dbinit module then has in ``open()``::
db = Database(instance_config, name)
# add some extra permissions and associate them with roles
- ei = db.security.addPermission(name="Edit", classname="issue",
+ ei = db.security.addPermission(name="Edit", klass="issue",
description="User is allowed to edit issues")
db.security.addPermissionToRole('User', ei)
- ai = db.security.addPermission(name="Assign", classname="issue",
+ ai = db.security.addPermission(name="Assign", klass="issue",
description="User may be assigned to issues")
db.security.addPermissionToRole('User', ei)
r = db.getclass('role').lookup('User')
user.create(username="anonymous", roles=[r])
-Then in the code that matters, calls to ``hasPermission`` are made to
-determine if the user has permission to perform some action::
+Then in the code that matters, calls to ``hasClassPermission`` and
+``hasNodePermission`` are made to determine if the user has permission
+to perform some action::
- if db.security.hasClassPermission('issue', 'Edit', self.user):
+ if db.security.hasClassPermission('issue', 'Edit', userid):
# all ok
- if db.security.hasNodePermission('issue', nodeid, self.user,
- ['assignedto']):
+ if db.security.hasNodePermission('issue', nodeid, assignedto=userid):
# all ok
-The htmltemplate will implement a new tag, <permission> which has the form::
+Code in the core will make use of these methods, as should code in auditors in
+custom templates. The htmltemplate will implement a new tag, ``<require>``
+which has the form::
- <permission require=name,name,name node=assignedto>
+ <require permission="name,name,name" assignedto="$userid" status="open">
HTML to display if the user has the permission.
<else>
HTML to display if the user does not have the permission.
- </permission>
+ </require>
+
+where:
+
+- the permission attribute gives a comma-separated list of permission names.
+ These are checked in turn using ``hasClassPermission`` and requires one to
+ be OK.
+- the other attributes are lookups on the node using ``hasNodePermission``. If
+ the attribute value is "$userid" then the current user's userid is tested.
+
+Any of these tests must pass or the ``<require>`` check will fail. The section
+of html within the side of the ``<else>`` that fails is remove from processing.
+
+Implementation as shipped
+-------------------------
+
+A set of Permissions are built in to the security module by default:
-where the require attribute gives a comma-separated list of permission names
-which are required, and the node attribute gives a comma-separated list of
-node properties whose value must match the current user's id. Either of these
-tests must pass or the permission check will fail. The section of html within
-the side of the ``<else>`` that fails is remove from processing.
+- Edit (everything)
+- Access (everything)
+- Assign (everything)
+
+The default interfaces define:
+
+- Web Registration
+- Email Registration
+
+These are hooked into the default Roles:
+
+- Admin (Edit everything, Access everything, Assign everything)
+- User ()
+- Anonymous (Web Registration, Email Registration)
+
+And finally, the "admin" user gets the "Admin" Role, and the "anonymous" user
+gets the "Anonymous" assigned when the database is initialised on installation.
+The two default schemas then define:
+
+- Edit issue, Access issue (both)
+- Edit support, Access support (extended only)
+
+and assign those Permissions to the "User" Role.
Authentication of Users
who supply their signature to an admin for submission to their user details.
+Anonymous Users
+---------------
+
+The "anonymous" user must always exist, and defines the access permissions for
+anonymous users. The three ANONYMOUS_ configuration variables are subsumed by
+this new functionality.
+
+
Action
======
diff --git a/roundup/admin.py b/roundup/admin.py
index b213c14d70624d8cee19dcd3cfc038ae3694fa89..305da4e355e1e5f49fffc5715299356b207fd29e 100644 (file)
--- a/roundup/admin.py
+++ b/roundup/admin.py
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-# $Id: admin.py,v 1.18 2002-07-18 11:17:30 gmcm Exp $
+# $Id: admin.py,v 1.19 2002-07-25 07:14:05 richard Exp $
import sys, os, getpass, getopt, re, UserDict, shlex, shutil
try:
for designator in designators:
# decode the node designator
try:
- classname, nodeid = roundupdb.splitDesignator(designator)
- except roundupdb.DesignatorError, message:
+ classname, nodeid = hyperdb.splitDesignator(designator)
+ except hyperdb.DesignatorError, message:
raise UsageError, message
# get the class
for designator in designators:
# decode the node designator
try:
- classname, nodeid = roundupdb.splitDesignator(designator)
- except roundupdb.DesignatorError, message:
+ classname, nodeid = hyperdb.splitDesignator(designator)
+ except hyperdb.DesignatorError, message:
raise UsageError, message
# get the class
# decode the node designator
try:
- classname, nodeid = roundupdb.splitDesignator(args[0])
- except roundupdb.DesignatorError, message:
+ classname, nodeid = hyperdb.splitDesignator(args[0])
+ except hyperdb.DesignatorError, message:
raise UsageError, message
# get the class
if len(args) < 1:
raise UsageError, _('Not enough arguments supplied')
try:
- classname, nodeid = roundupdb.splitDesignator(args[0])
- except roundupdb.DesignatorError, message:
+ classname, nodeid = hyperdb.splitDesignator(args[0])
+ except hyperdb.DesignatorError, message:
raise UsageError, message
try:
designators = args[0].split(',')
for designator in designators:
try:
- classname, nodeid = roundupdb.splitDesignator(designator)
- except roundupdb.DesignatorError, message:
+ classname, nodeid = hyperdb.splitDesignator(designator)
+ except hyperdb.DesignatorError, message:
raise UsageError, message
try:
self.db.getclass(classname).retire(nodeid)
#
# $Log: not supported by cvs2svn $
+# Revision 1.18 2002/07/18 11:17:30 gmcm
+# Add Number and Boolean types to hyperdb.
+# Add conversion cases to web, mail & admin interfaces.
+# Add storage/serialization cases to back_anydbm & back_metakit.
+#
# Revision 1.17 2002/07/14 06:05:50 richard
# . fixed the date module so that Date(". - 2d") works
#
index 933738dbc6d6a436c398502f4d574fb6fefc4a9c..128453ff67575d67f11b1ded6dfd1912d8d3759a 100644 (file)
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-#$Id: back_anydbm.py,v 1.52 2002-07-19 03:36:34 richard Exp $
+#$Id: back_anydbm.py,v 1.53 2002-07-25 07:14:06 richard Exp $
'''
This module defines a backend that saves the hyperdatabase in a database
chosen by anydbm. It is guaranteed to always be available in python
'''
import whichdb, anydbm, os, marshal, re, weakref, string, copy
-from roundup import hyperdb, date, password, roundupdb
+from roundup import hyperdb, date, password, roundupdb, security
from blobfiles import FileStorage
from roundup.indexer import Indexer
from locking import acquire_lock, release_lock
self.destroyednodes = {}# keep track of the destroyed nodes by class
self.transactions = []
self.indexer = Indexer(self.dir)
+ self.security = security.Security(self)
# ensure files are group readable and writable
os.umask(0002)
except (TypeError, KeyError):
raise IndexError, 'new property "%s": %s not a %s'%(
key, value, link_class)
- elif not self.db.hasnode(link_class, value):
+ elif not self.db.getclass(link_class).hasnode(value):
raise IndexError, '%s has no node %s'%(link_class, value)
# save off the value
propvalues[key] = value
# handle additions
- for id in value:
- if not self.db.hasnode(link_class, id):
- raise IndexError, '%s has no node %s'%(link_class, id)
+ for nodeid in value:
+ if not self.db.getclass(link_class).hasnode(nodeid):
+ raise IndexError, '%s has no node %s'%(link_class,
+ nodeid)
# register the link with the newly linked node
if self.do_journal and self.properties[key].do_journal:
- self.db.addjournal(link_class, id, 'link',
+ self.db.addjournal(link_class, nodeid, 'link',
(self.classname, newid, key))
elif isinstance(prop, String):
raise IndexError, 'new property "%s": %s not a %s'%(
propname, value, self.properties[propname].classname)
- if not self.db.hasnode(link_class, value):
+ if not self.db.getclass(link_class).hasnode(value):
raise IndexError, '%s has no node %s'%(link_class, value)
if self.do_journal and self.properties[propname].do_journal:
# handle additions
for id in value:
- if not self.db.hasnode(link_class, id):
+ if not self.db.getclass(link_class).hasnode(id):
raise IndexError, '%s has no node %s'%(link_class, id)
if id in l:
continue
prop = self.properties[propname]
if not isinstance(prop, Link) and not isinstance(prop, Multilink):
raise TypeError, "'%s' not a Link/Multilink property"%propname
- #XXX edit is expensive and of questionable use
- #for nodeid in nodeids:
- # if not self.db.hasnode(prop.classname, nodeid):
- # raise ValueError, '%s has no node %s'%(prop.classname, nodeid)
# ok, now do the find
cldb = self.db.getclassdb(self.classname)
#
#$Log: not supported by cvs2svn $
+#Revision 1.52 2002/07/19 03:36:34 richard
+#Implemented the destroy() method needed by the session database (and possibly
+#others). At the same time, I removed the leading underscores from the hyperdb
+#methods that Really Didn't Need Them.
+#The journal also raises IndexError now for all situations where there is a
+#request for the journal of a node that doesn't have one. It used to return
+#[] in _some_ situations, but not all. This _may_ break code, but the tests
+#pass...
+#
#Revision 1.51 2002/07/18 23:07:08 richard
#Unit tests and a few fixes.
#
diff --git a/roundup/cgi_client.py b/roundup/cgi_client.py
index f45667373c2afeb88ba536845a725f820e1e3fb1..9574d95690cf0df269752160c3af5fc5e4974c41 100644 (file)
--- a/roundup/cgi_client.py
+++ b/roundup/cgi_client.py
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-# $Id: cgi_client.py,v 1.143 2002-07-20 19:29:10 gmcm Exp $
+# $Id: cgi_client.py,v 1.144 2002-07-25 07:14:05 richard Exp $
__doc__ = """
WWW request handler (also used in the stand-alone server).
class NotFound(ValueError):
pass
+def initialiseSecurity(security):
+ ''' Create some Permissions and Roles on the security object
+
+ This function is directly invoked by security.Security.__init__()
+ as a part of the Security object instantiation.
+ '''
+ newid = security.addPermission(name="Web Registration",
+ description="User may register through the web")
+ security.addPermissionToRole('Anonymous', newid)
+
class Client:
'''
A note about login
#
# $Log: not supported by cvs2svn $
+# Revision 1.143 2002/07/20 19:29:10 gmcm
+# Fixes/improvements to the search form & saved queries.
+#
# Revision 1.142 2002/07/18 11:17:30 gmcm
# Add Number and Boolean types to hyperdb.
# Add conversion cases to web, mail & admin interfaces.
index 2dc9af7ea6d11aa3e146ba92d8811216d80d2bb1..2558f3750f42edae51cc119c319903534433fbc8 100644 (file)
--- a/roundup/htmltemplate.py
+++ b/roundup/htmltemplate.py
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-# $Id: htmltemplate.py,v 1.103 2002-07-20 19:29:10 gmcm Exp $
+# $Id: htmltemplate.py,v 1.104 2002-07-25 07:14:05 richard Exp $
__doc__ = """
Template engine.
+
+Three types of template files exist:
+ .index used by IndexTemplate
+ .item used by ItemTemplate and NewItemTemplate
+ .filter used by IndexTemplate
+
+Templating works by instantiating one of the *Template classes above,
+passing in a handle to the cgi client, identifying the class and the
+template source directory.
+
+The *Template class reads in the appropriate template text, and when the
+render() method is called, the template text is fed to an re.sub which
+calls the subfunc and then all the funky do_* methods as required.
+
+Templating is tested by the test_htmltemplate unit test suite. If you add
+a template function, add a test for all data types or the angry pink bunny
+will hunt you down.
"""
import os, re, StringIO, urllib, cgi, errno, types, urllib
if k[0] != ':':
filterspec[k] = v
ixtmplt = IndexTemplate(self.client, self.templates, classname)
- qform = '<form onSubmit="return submit_once()" action="%s%s">\n'%(self.classname,self.nodeid)
+ qform = '<form onSubmit="return submit_once()" action="%s%s">\n'%(
+ self.classname,self.nodeid)
qform += ixtmplt.filter_form(query.get('search_text', ''),
query.get(':filter', []),
query.get(':columns', []),
pagesize)
ixtmplt.clear()
return qform + '</table>\n'
-
+
+ #
+ # templating subtitution methods
+ #
+ def execute_template(self, text):
+ ''' do the replacement of the template stuff with useful
+ information
+ '''
+ replace = re.compile(
+ r'((<require\s+(?P<cond>.+?)>(?P<ok>.+?)'
+ r'(<else>(?P<fail>.*?))?</require>)|'
+ r'(<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
+ r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
+ return replace.sub(self.subfunc, text)
+
+ #
+ # secutiry <require> tag handling
+ #
+ condre = re.compile('(\w+?)\s*=\s*"([^"]+?)"')
+ def handle_require(self, condition, ok, fail):
+ userid = self.db.user.lookup(self.client.user)
+ security = self.db.security
+
+ # get the conditions
+ l = self.condre.findall(condition)
+ d = {}
+ for k,v in l:
+ d[k] = v
+
+ # see if one of the permissions are available
+ if d.has_key('permission'):
+ l.remove(('permission', d['permission']))
+ for value in d['permission'].split(','):
+ if security.hasClassPermission(self.classname, value, userid):
+ # just passing the permission is OK
+ return self.execute_template(ok)
+
+ # try the attr conditions until one is met
+ for propname, value in d.items():
+ if propname == 'permission':
+ continue
+ if not security.hasNodePermission(self.classname, self.nodeid,
+ **{value: userid}):
+ break
+ else:
+ if l:
+ # there were tests, and we didn't fail any of them so we're OK
+ return self.execute_template(ok)
+
+ # nope, fail
+ return self.execute_template(fail)
#
# INDEX TEMPLATES
#
-class IndexTemplateReplace:
- '''Regular-expression based parser that turns the template into HTML.
- '''
- def __init__(self, globals, locals, props):
- self.globals = globals
- self.locals = locals
- self.props = props
-
- replace=re.compile(
- r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
- r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
- def go(self, text):
- newtext = self.replace.sub(self, text)
- self.locals = self.globals = None
- return newtext
-
- def __call__(self, m, search_text=None, filter=None, columns=None,
- sort=None, group=None):
- if m.group('name'):
- if m.group('name') in self.props:
- text = m.group('text')
- replace = self.__class__(self.globals, {}, self.props)
- return replace.go(text)
- else:
- return ''
- if m.group('display'):
- command = m.group('command')
- return eval(command, self.globals, self.locals)
- return '*** unhandled match: %s'%str(m.groupdict())
-
class IndexTemplate(TemplateFunctions):
'''Templating functionality specifically for index pages
'''
def __init__(self, client, templates, classname):
TemplateFunctions.__init__(self)
+ self.globals['handle_require'] = self.handle_require
self.client = client
self.instance = client.instance
self.templates = templates
def clear(self):
self.db = self.cl = self.properties = None
TemplateFunctions.clear(self)
-
+
def buildurl(self, filterspec, search_text, filter, columns, sort, group, pagesize):
d = {'pagesize':pagesize, 'pagesize':pagesize, 'classname':self.classname}
d['filter'] = ','.join(map(urllib.quote,filter))
l.append(name)
columns = l
+ # TODO this is for the RE replacer func, and could probably be done
+ # better
+ self.props = columns
+
# display the filter section
if (show_display_form and
self.instance.FILTER_POSITION in ('top and bottom', 'top')):
w('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
- self.filter_section(search_text, filter, columns, group, all_columns, sort, filterspec,
- pagesize, startwith)
+ self.filter_section(search_text, filter, columns, group,
+ all_columns, sort, filterspec, pagesize, startwith)
# now display the index section
w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
for name in columns:
cname = name.capitalize()
if show_display_form:
- sb = self.sortby(name, filterspec, columns, filter, group, sort, pagesize, startwith)
+ sb = self.sortby(name, filterspec, columns, filter, group,
+ sort, pagesize, startwith)
anchor = "%s?%s"%(self.classname, sb)
- w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
- anchor, cname))
+ w('<td><span class="list-header"><a href="%s">%s</a>'
+ '</span></td>\n'%(anchor, cname))
else:
w('<td><span class="list-header">%s</span></td>\n'%cname)
w('</tr>\n')
old_group = this_group
# display this node's row
- replace = IndexTemplateReplace(self.globals, locals(), columns)
- self.nodeid = nodeid
- w(replace.go(template))
+ w(replace.execute_template(template))
if matches:
self.node_matches(matches[nodeid], len(columns))
self.nodeid = None
w('</table>\n')
# the previous and next links
if nodeids:
- baseurl = self.buildurl(filterspec, search_text, filter, columns, sort, group, pagesize)
+ baseurl = self.buildurl(filterspec, search_text, filter,
+ columns, sort, group, pagesize)
if startwith > 0:
- prevurl = '<a href="%s&:startwith=%s"><< Previous page</a>' % \
- (baseurl, max(0, startwith-pagesize))
+ prevurl = '<a href="%s&:startwith=%s"><< '\
+ 'Previous page</a>'%(baseurl, max(0, startwith-pagesize))
else:
prevurl = ""
if startwith + pagesize < len(nodeids):
- nexturl = '<a href="%s&:startwith=%s">Next page >></a>' % (baseurl, startwith+pagesize)
+ nexturl = '<a href="%s&:startwith=%s">Next page '\
+ '>></a>'%(baseurl, startwith+pagesize)
else:
nexturl = ""
if prevurl or nexturl:
- w('<table width="100%%"><tr><td width="50%%" align="center">%s</td><td width="50%%" align="center">%s</td></tr></table>\n' % (prevurl, nexturl))
+ w('''<table width="100%%"><tr>
+ <td width="50%%" align="center">%s</td>
+ <td width="50%%" align="center">%s</td>
+ </tr></table>\n'''%(prevurl, nexturl))
# display the filter section
if (show_display_form and hasattr(self.instance, 'FILTER_POSITION') and
self.instance.FILTER_POSITION in ('top and bottom', 'bottom')):
- w('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
- self.filter_section(search_text, filter, columns, group, all_columns, sort, filterspec,
- pagesize, startwith)
-
+ w('<form onSubmit="return submit_once()" action="%s">\n'%
+ self.classname)
+ self.filter_section(search_text, filter, columns, group,
+ all_columns, sort, filterspec, pagesize, startwith)
self.clear()
+ def subfunc(self, m, search_text=None, filter=None, columns=None,
+ sort=None, group=None):
+ ''' called as part of the template replacement
+ '''
+ if m.group('cond'):
+ # call the template handler for require
+ require = self.globals['handle_require']
+ return self.handle_require(m.group('cond'), m.group('ok'),
+ m.group('fail'))
+ if m.group('name'):
+ if m.group('name') in self.props:
+ text = m.group('text')
+ return self.execute_template(text)
+ else:
+ return ''
+ if m.group('display'):
+ command = m.group('command')
+ return eval(command, self.globals, {})
+ return '*** unhandled match: %s'%str(m.groupdict())
+
def node_matches(self, match, colspan):
''' display the files and messages for a node that matched a
full text search
' Matched files: %s</td></tr>\n')%(
colspan, ', '.join(file_links)))
- def filter_form(self, search_text, filter, columns, group, all_columns, sort, filterspec,
- pagesize):
-
+ def filter_form(self, search_text, filter, columns, group, all_columns,
+ sort, filterspec, pagesize):
sortspec = {}
for i in range(len(sort)):
mod = ''
return '\n'.join(rslt)
- def filter_section(self, search_text, filter, columns, group, all_columns, sort, filterspec,
- pagesize, startwith):
-
+ def filter_section(self, search_text, filter, columns, group, all_columns,
+ sort, filterspec, pagesize, startwith):
w = self.client.write
w(self.filter_form(search_text, filter, columns, group, all_columns,
sort, filterspec, pagesize))
w(':sort=%s'%','.join(m[:2]))
return '&'.join(l)
-#
-# ITEM TEMPLATES
-#
-class ItemTemplateReplace:
- '''Regular-expression based parser that turns the template into HTML.
- '''
- def __init__(self, globals, locals, cl, nodeid):
- self.globals = globals
- self.locals = locals
- self.cl = cl
- self.nodeid = nodeid
-
- replace=re.compile(
- r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
- r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
- def go(self, text):
- newtext = self.replace.sub(self, text)
- self.globals = self.locals = self.cl = None
- return newtext
-
- def __call__(self, m, filter=None, columns=None, sort=None, group=None):
- if m.group('name'):
- if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
- replace = ItemTemplateReplace(self.globals, {}, self.cl,
- self.nodeid)
- return replace.go(m.group('text'))
- else:
- return ''
- if m.group('display'):
- command = m.group('command')
- return eval(command, self.globals, self.locals)
- return '*** unhandled match: %s'%str(m.groupdict())
-
-
class ItemTemplate(TemplateFunctions):
'''Templating functionality specifically for item (node) display
'''
def __init__(self, client, templates, classname):
TemplateFunctions.__init__(self)
+ self.globals['handle_require'] = self.handle_require
self.client = client
self.instance = client.instance
self.templates = templates
w('<form onSubmit="return submit_once()" action="%s%s" method="POST" enctype="multipart/form-data">'%(
self.classname, nodeid))
s = open(os.path.join(self.templates, self.classname+'.item')).read()
- replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
- w(replace.go(s))
+ w(self.execute_template(s))
w('</form>')
self.clear()
+ def subfunc(self, m, search_text=None, filter=None, columns=None,
+ sort=None, group=None):
+ ''' called as part of the template replacement
+ '''
+ if m.group('cond'):
+ # call the template handler for require
+ require = self.globals['handle_require']
+ return self.handle_require(m.group('cond'), m.group('ok'),
+ m.group('fail'))
+ if m.group('name'):
+ if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
+ return self.execute_template(m.group('text'))
+ else:
+ return ''
+ if m.group('display'):
+ command = m.group('command')
+ return eval(command, self.globals, {})
+ return '*** unhandled match: %s'%str(m.groupdict())
-class NewItemTemplate(TemplateFunctions):
+class NewItemTemplate(ItemTemplate):
'''Templating functionality specifically for NEW item (node) display
'''
def __init__(self, client, templates, classname):
TemplateFunctions.__init__(self)
+ self.globals['handle_require'] = self.handle_require
self.client = client
self.instance = client.instance
self.templates = templates
if type(value) != type([]): value = [value]
for value in value:
w('<input type="hidden" name="%s" value="%s">'%(key, value))
- replace = ItemTemplateReplace(self.globals, locals(), None, None)
- w(replace.go(s))
+ w(self.execute_template(s))
w('</form>')
self.clear()
#
# $Log: not supported by cvs2svn $
+# Revision 1.103 2002/07/20 19:29:10 gmcm
+# Fixes/improvements to the search form & saved queries.
+#
# Revision 1.102 2002/07/18 23:07:08 richard
# Unit tests and a few fixes.
#
diff --git a/roundup/mailgw.py b/roundup/mailgw.py
index b5d802d150b6b2af0db135c5cd50c5c3536c30be..88868fd39cfa789d5333183e0de60eed58013f59 100644 (file)
--- a/roundup/mailgw.py
+++ b/roundup/mailgw.py
an exception, the original message is bounced back to the sender with the
explanatory message given in the exception.
-$Id: mailgw.py,v 1.77 2002-07-18 11:17:31 gmcm Exp $
+$Id: mailgw.py,v 1.78 2002-07-25 07:14:06 richard Exp $
'''
class UnAuthorized(Exception):
""" Access denied """
+def initialiseSecurity(security):
+ ''' Create some Permissions and Roles on the security object
+
+ This function is directly invoked by security.Security.__init__()
+ as a part of the Security object instantiation.
+ '''
+ newid = security.addPermission(name="Email Registration",
+ description="Anonymous may register through e-mail")
+ security.addPermissionToRole('Anonymous', newid)
+
class Message(mimetools.Message):
''' subclass mimetools.Message so we can retrieve the parts of the
message...
#
# $Log: not supported by cvs2svn $
+# Revision 1.77 2002/07/18 11:17:31 gmcm
+# Add Number and Boolean types to hyperdb.
+# Add conversion cases to web, mail & admin interfaces.
+# Add storage/serialization cases to back_anydbm & back_metakit.
+#
# Revision 1.76 2002/07/10 06:39:37 richard
# . made mailgw handle set and modify operations on multilinks (bug #579094)
#
diff --git a/roundup/security.py b/roundup/security.py
--- /dev/null
+++ b/roundup/security.py
@@ -0,0 +1,143 @@
+import weakref
+
+from roundup import hyperdb, volatiledb
+
+class PermissionClass(volatiledb.VolatileClass):
+ ''' Include the default attributes:
+ - name (String)
+ - classname (String)
+ - description (String)
+
+ The classname may be unset, indicating that this permission is not
+ locked to a particular class. That means there may be multiple
+ Permissions for the same name for different classes.
+ '''
+ def __init__(self, db, classname, **properties):
+ """ set up the default properties
+ """
+ if not properties.has_key('name'):
+ properties['name'] = hyperdb.String()
+ if not properties.has_key('klass'):
+ properties['klass'] = hyperdb.String()
+ if not properties.has_key('description'):
+ properties['description'] = hyperdb.String()
+ volatiledb.VolatileClass.__init__(self, db, classname, **properties)
+
+class RoleClass(volatiledb.VolatileClass):
+ ''' Include the default attributes:
+ - name (String, key)
+ - description (String)
+ - permissions (PermissionClass Multilink)
+ '''
+ def __init__(self, db, classname, **properties):
+ """ set up the default properties
+ """
+ if not properties.has_key('name'):
+ properties['name'] = hyperdb.String()
+ if not properties.has_key('description'):
+ properties['description'] = hyperdb.String()
+ if not properties.has_key('permissions'):
+ properties['permissions'] = hyperdb.Multilink('permission')
+ volatiledb.VolatileClass.__init__(self, db, classname, **properties)
+ self.setkey('name')
+
+class Security:
+ def __init__(self, db):
+ ''' Initialise the permission and role classes, and add in the
+ base roles (for admin user).
+ '''
+ # use a weak ref to avoid circularity
+ self.db = weakref.proxy(db)
+
+ # create the permission class instance (we only need one))
+ self.permission = PermissionClass(db, "permission")
+
+ # create the role class instance (we only need one)
+ self.role = RoleClass(db, "role")
+
+ # the default Roles
+ self.addRole(name="User", description="A regular user, no privs")
+ self.addRole(name="Admin", description="An admin user, full privs")
+ self.addRole(name="Anonymous", description="An anonymous user")
+
+ ee = self.addPermission(name="Edit",
+ description="User may edit everthing")
+ self.addPermissionToRole('Admin', ee)
+ ae = self.addPermission(name="Access",
+ description="User may access everything")
+ self.addPermissionToRole('Admin', ae)
+ ae = self.addPermission(name="Assign",
+ description="User may be assigned to anything")
+ self.addPermissionToRole('Admin', ae)
+ reg = self.addPermission(name="Register Web",
+ description="User may register through the web")
+ self.addPermissionToRole('Anonymous', reg)
+ reg = self.addPermission(name="Register Email",
+ description="User may register through the email")
+ self.addPermissionToRole('Anonymous', reg)
+
+ # initialise the permissions and roles needed for the UIs
+ from roundup import cgi_client, mailgw
+ cgi_client.initialiseSecurity(self)
+ mailgw.initialiseSecurity(self)
+
+ def hasClassPermission(self, classname, permission, userid):
+ ''' Look through all the Roles, and hence Permissions, and see if
+ "permission" is there for the specified classname.
+
+ '''
+ roles = self.db.user.get(userid, 'roles')
+ for roleid in roles:
+ for permissionid in self.db.role.get(roleid, 'permissions'):
+ if self.db.permission.get(permissionid, 'name') != permission:
+ continue
+ klass = self.db.permission.get(permissionid, 'klass')
+ if klass is None or klass == classname:
+ return 1
+ return 0
+
+ def hasNodePermission(self, classname, nodeid, **propspec):
+ ''' Check the named properties of the given node to see if the
+ userid appears in them. If it does, then the user is granted
+ this permission check.
+
+ 'propspec' consists of a set of properties and values that
+ must be present on the given node for access to be granted.
+
+ If a property is a Link, the value must match the property
+ value. If a property is a Multilink, the value must appear
+ in the Multilink list.
+ '''
+ klass = self.db.getclass(classname)
+ properties = klass.getprops()
+ for k,v in propspec.items():
+ value = klass.get(nodeid, k)
+ if isinstance(properties[k], hyperdb.Multilink):
+ if v not in value:
+ return 0
+ else:
+ if v != value:
+ return 0
+ return 1
+
+ def addPermission(self, **propspec):
+ ''' Create a new Permission with the properties defined in
+ 'propspec'
+ '''
+ return self.db.permission.create(**propspec)
+
+ def addRole(self, **propspec):
+ ''' Create a new Role with the properties defined in 'propspec'
+ '''
+ return self.db.role.create(**propspec)
+
+ def addPermissionToRole(self, rolename, permissionid):
+ ''' Add the permission to the role's permission list.
+
+ 'rolename' is the name of the role to add 'permissionid'.
+ '''
+ roleid = self.db.role.lookup(rolename)
+ permissions = self.db.role.get(roleid, 'permissions')
+ permissions.append(permissionid)
+ self.db.role.set(roleid, permissions=permissions)
+
diff --git a/roundup/volatiledb.py b/roundup/volatiledb.py
--- /dev/null
+++ b/roundup/volatiledb.py
@@ -0,0 +1,274 @@
+import weakref, re
+
+from roundup import hyperdb
+from roundup.hyperdb import String, Password, Date, Interval, Link, \
+ Multilink, DatabaseError, Boolean, Number
+
+class VolatileClass(hyperdb.Class):
+ ''' This is a class that just sits in memory, no saving to disk.
+ It has no journal.
+ '''
+ def __init__(self, db, classname, **properties):
+ ''' Set up an in-memory store for the nodes of this class
+ '''
+ self.db = weakref.proxy(db) # use a weak ref to avoid circularity
+ self.classname = classname
+ self.properties = properties
+ self.id_counter = 1
+ self.store = {}
+ self.by_key = {}
+ self.key = ''
+ db.addclass(self)
+
+ def setkey(self, propname):
+ prop = self.getprops()[propname]
+ if not isinstance(prop, String):
+ raise TypeError, 'key properties must be String'
+ self.key = propname
+
+ def getprops(self, protected=1):
+ d = self.properties.copy()
+ if protected:
+ d['id'] = String()
+ return d
+
+ def create(self, **propvalues):
+ ''' Create a new node in the in-memory store
+ '''
+ if propvalues.has_key('id'):
+ raise KeyError, '"id" is reserved'
+ newid = str(self.id_counter)
+ self.id_counter += 1
+
+ # get the key value, validate it
+ if self.key:
+ keyvalue = propvalues[self.key]
+ try:
+ self.lookup(keyvalue)
+ except KeyError:
+ pass
+ else:
+ raise ValueError, 'node with key "%s" exists'%keyvalue
+ self.by_key[keyvalue] = newid
+
+ # validate propvalues
+ num_re = re.compile('^\d+$')
+
+ for key, value in propvalues.items():
+
+ # try to handle this property
+ try:
+ prop = self.properties[key]
+ except KeyError:
+ raise KeyError, '"%s" has no property "%s"'%(self.classname,
+ key)
+
+ if isinstance(prop, Link):
+ if type(value) != type(''):
+ raise ValueError, 'link value must be String'
+ link_class = self.properties[key].classname
+ # if it isn't a number, it's a key
+ if not num_re.match(value):
+ try:
+ value = self.db.classes[link_class].lookup(value)
+ except (TypeError, KeyError):
+ raise IndexError, 'new property "%s": %s not a %s'%(
+ key, value, link_class)
+ elif not self.db.hasnode(link_class, value):
+ raise IndexError, '%s has no node %s'%(link_class, value)
+
+ # save off the value
+ propvalues[key] = value
+
+ elif isinstance(prop, Multilink):
+ if type(value) != type([]):
+ raise TypeError, 'new property "%s" not a list of ids'%key
+
+ # clean up and validate the list of links
+ link_class = self.properties[key].classname
+ l = []
+ for entry in value:
+ if type(entry) != type(''):
+ raise ValueError, '"%s" link value (%s) must be '\
+ 'String'%(key, value)
+ # if it isn't a number, it's a key
+ if not num_re.match(entry):
+ try:
+ entry = self.db.classes[link_class].lookup(entry)
+ except (TypeError, KeyError):
+ raise IndexError, 'new property "%s": %s not a %s'%(
+ key, entry, self.properties[key].classname)
+ l.append(entry)
+ value = l
+ propvalues[key] = value
+
+ # handle additions
+ for id in value:
+ if not self.db.hasnode(link_class, id):
+ raise IndexError, '%s has no node %s'%(link_class, id)
+
+ elif isinstance(prop, String):
+ if type(value) != type(''):
+ raise TypeError, 'new property "%s" not a string'%key
+
+ elif isinstance(prop, Password):
+ if not isinstance(value, password.Password):
+ raise TypeError, 'new property "%s" not a Password'%key
+
+ elif isinstance(prop, Date):
+ if value is not None and not isinstance(value, date.Date):
+ raise TypeError, 'new property "%s" not a Date'%key
+
+ elif isinstance(prop, Interval):
+ if value is not None and not isinstance(value, date.Interval):
+ raise TypeError, 'new property "%s" not an Interval'%key
+
+ # make sure there's data where there needs to be
+ for key, prop in self.properties.items():
+ if propvalues.has_key(key):
+ continue
+ if key == self.key:
+ raise ValueError, 'key property "%s" is required'%key
+ if isinstance(prop, Multilink):
+ propvalues[key] = []
+ else:
+ propvalues[key] = None
+
+ # done
+ self.store[newid] = propvalues
+
+ return newid
+
+ _marker = []
+ def get(self, nodeid, propname, default=_marker, cache=1):
+ ''' Get the node from the in-memory store
+ '''
+ if propname == 'id':
+ return nodeid
+ return self.store[nodeid][propname]
+
+ def set(self, nodeid, **propvalues):
+ ''' Set properties on the node in the in-memory store
+ '''
+ if not propvalues:
+ return
+
+ if propvalues.has_key('id'):
+ raise KeyError, '"id" is reserved'
+
+ node = self.store[nodeid]
+ num_re = re.compile('^\d+$')
+
+ for propname, value in propvalues.items():
+ # check to make sure we're not duplicating an existing key
+ if propname == self.key and node[propname] != value:
+ try:
+ self.lookup(value)
+ except KeyError:
+ pass
+ else:
+ raise ValueError, 'node with key "%s" exists'%value
+
+ # this will raise the KeyError if the property isn't valid
+ # ... we don't use getprops() here because we only care about
+ # the writeable properties.
+ prop = self.properties[propname]
+
+ # if the value's the same as the existing value, no sense in
+ # doing anything
+ if node.has_key(propname) and value == node[propname]:
+ del propvalues[propname]
+ continue
+
+ # do stuff based on the prop type
+ if isinstance(prop, Link):
+ link_class = self.properties[propname].classname
+ # if it isn't a number, it's a key
+ if type(value) != type(''):
+ raise ValueError, 'link value must be String'
+ if not num_re.match(value):
+ try:
+ value = self.db.classes[link_class].lookup(value)
+ except (TypeError, KeyError):
+ raise IndexError, 'new property "%s": %s not a %s'%(
+ propname, value, self.properties[propname].classname)
+
+ if not self.db.hasnode(link_class, value):
+ raise IndexError, '%s has no node %s'%(link_class, value)
+
+ elif isinstance(prop, Multilink):
+ if type(value) != type([]):
+ raise TypeError, 'new property "%s" not a list of'\
+ ' ids'%propname
+ link_class = self.properties[propname].classname
+ l = []
+ for entry in value:
+ # if it isn't a number, it's a key
+ if type(entry) != type(''):
+ raise ValueError, 'new property "%s" link value ' \
+ 'must be a string'%propname
+ if not num_re.match(entry):
+ try:
+ entry = self.db.classes[link_class].lookup(entry)
+ except (TypeError, KeyError):
+ raise IndexError, 'new property "%s": %s not a %s'%(
+ propname, entry,
+ self.properties[propname].classname)
+ l.append(entry)
+ value = l
+ propvalues[propname] = value
+
+ elif isinstance(prop, String):
+ if value is not None and type(value) != type(''):
+ raise TypeError, 'new property "%s" not a string'%propname
+
+ elif isinstance(prop, Password):
+ if not isinstance(value, password.Password):
+ raise TypeError, 'new property "%s" not a Password'%propname
+ propvalues[propname] = value
+
+ elif value is not None and isinstance(prop, Date):
+ if not isinstance(value, date.Date):
+ raise TypeError, 'new property "%s" not a Date'% propname
+ propvalues[propname] = value
+
+ elif value is not None and isinstance(prop, Interval):
+ if not isinstance(value, date.Interval):
+ raise TypeError, 'new property "%s" not an '\
+ 'Interval'%propname
+ propvalues[propname] = value
+
+ elif value is not None and isinstance(prop, Number):
+ try:
+ float(value)
+ except ValueError:
+ raise TypeError, 'new property "%s" not numeric'%propname
+
+ elif value is not None and isinstance(prop, Boolean):
+ try:
+ int(value)
+ except ValueError:
+ raise TypeError, 'new property "%s" not boolean'%propname
+
+ node[propname] = value
+
+ # do the set
+ self.store[nodeid] = node
+
+ def lookup(self, keyvalue):
+ ''' look up the key node in the store
+ '''
+ return self.by_key[keyvalue]
+
+ def hasnode(self, nodeid):
+ nodeid = str(nodeid)
+ return self.store.has_key(nodeid)
+
+ def list(self):
+ l = self.store.keys()
+ l.sort()
+ return l
+
+ def index(self, nodeid):
+ pass
+
diff --git a/test/test_db.py b/test/test_db.py
index 47a1bbadb93bda06f8901be6b0d432ee5d050e34..ffc07c7c8e8414c96d7179d750f0171213ab1d3f 100644 (file)
--- a/test/test_db.py
+++ b/test/test_db.py
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-# $Id: test_db.py,v 1.36 2002-07-19 03:36:34 richard Exp $
+# $Id: test_db.py,v 1.37 2002-07-25 07:14:06 richard Exp $
import unittest, os, shutil, time
status = module.Class(db, "status", name=String())
status.setkey("name")
user = module.Class(db, "user", username=String(), password=Password(),
- assignable=Boolean(), age=Number())
+ assignable=Boolean(), age=Number(), roles=Multilink('role'))
+ user.setkey("username")
file = module.FileClass(db, "file", name=String(), type=String(),
comment=String(indexme="yes"))
issue = module.IssueClass(db, "issue", title=String(indexme="yes"),
status=Link("status"), nosy=Multilink("user"), deadline=Date(),
- foo=Interval(), files=Multilink("file"))
+ foo=Interval(), files=Multilink("file"), assignedto=Link('user'))
session = module.Class(db, 'session', title=String())
session.disableJournalling()
db.post_init()
self.assertNotEqual(self.db.issue.get('1', "foo"), a)
def testBooleanChange(self):
- self.db.user.create(username='foo', assignable=1)
- self.db.user.create(username='foo', assignable=0)
- a = self.db.user.get('1', 'assignable')
- self.db.user.set('1', assignable=0)
- self.assertNotEqual(self.db.user.get('1', 'assignable'), a)
- self.db.user.set('1', assignable=0)
- self.db.user.set('1', assignable=1)
+ userid = self.db.user.create(username='foo', assignable=1)
+ self.db.user.create(username='foo2', assignable=0)
+ a = self.db.user.get(userid, 'assignable')
+ self.db.user.set(userid, assignable=0)
+ self.assertNotEqual(self.db.user.get(userid, 'assignable'), a)
+ self.db.user.set(userid, assignable=0)
+ self.db.user.set(userid, assignable=1)
def testNumberChange(self):
self.db.user.create(username='foo', age='1')
self.db.user.set('1', age='1.0')
def testNewProperty(self):
- ' make sure a new property is added ok '
self.db.issue.create(title="spam", status='1')
self.db.issue.addprop(fixer=Link("user"))
props = self.db.issue.getprops()
keys = props.keys()
keys.sort()
- self.assertEqual(keys, ['activity', 'creation', 'creator', 'deadline',
- 'files', 'fixer', 'foo', 'id', 'messages', 'nosy', 'status',
- 'superseder', 'title'])
+ self.assertEqual(keys, ['activity', 'assignedto', 'creation',
+ 'creator', 'deadline', 'files', 'fixer', 'foo', 'id', 'messages',
+ 'nosy', 'status', 'superseder', 'title'])
self.assertEqual(self.db.issue.get('1', "fixer"), None)
def testRetire(self):
self.assertEqual(num_files2, self.db.numfiles())
def testDestroyNoJournalling(self):
- ' test destroy on a class with no journalling '
self.innerTestDestroy(klass=self.db.session)
def testDestroyJournalling(self):
- ' test destroy on a class with journalling '
self.innerTestDestroy(klass=self.db.issue)
def innerTestDestroy(self, klass):
self.assertEqual(action, 'create')
keys = params.keys()
keys.sort()
- self.assertEqual(keys, ['deadline', 'files', 'fixer', 'foo',
- 'messages', 'nosy', 'status', 'superseder', 'title'])
+ self.assertEqual(keys, ['assignedto', 'deadline', 'files', 'fixer',
+ 'foo', 'messages', 'nosy', 'status', 'superseder', 'title'])
self.assertEqual(None,params['deadline'])
self.assertEqual(None,params['fixer'])
self.assertEqual(None,params['foo'])
setupSchema(self.db2, 0, anydbm)
def testExceptions(self):
- ' make sure exceptions are raised on writes to a read-only db '
# this tests the exceptions that should be raised
ar = self.assertRaises
#
# $Log: not supported by cvs2svn $
+# Revision 1.36 2002/07/19 03:36:34 richard
+# Implemented the destroy() method needed by the session database (and possibly
+# others). At the same time, I removed the leading underscores from the hyperdb
+# methods that Really Didn't Need Them.
+# The journal also raises IndexError now for all situations where there is a
+# request for the journal of a node that doesn't have one. It used to return
+# [] in _some_ situations, but not all. This _may_ break code, but the tests
+# pass...
+#
# Revision 1.35 2002/07/18 23:07:08 richard
# Unit tests and a few fixes.
#
index 8461e7e67a01096581a3144185a98955e956047e..8b155fc2422845b87a5487696153426b03e54106 100644 (file)
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
-# $Id: test_htmltemplate.py,v 1.17 2002-07-18 23:07:07 richard Exp $
+# $Id: test_htmltemplate.py,v 1.18 2002-07-25 07:14:06 richard Exp $
-import unittest, cgi, time
+import unittest, cgi, time, os, shutil
from roundup import date, password
-from roundup.htmltemplate import TemplateFunctions
+from roundup.htmltemplate import TemplateFunctions, IndexTemplate, ItemTemplate
from roundup.i18n import _
from roundup.hyperdb import String, Password, Date, Interval, Link, \
Multilink, Boolean, Number
-class Class:
+class TestClass:
def get(self, nodeid, attribute, default=None):
if attribute == 'string':
return 'Node %s: I am a string'%nodeid
def labelprop(self, default_to_id=0):
return 'key'
-class Database:
- classes = {'other': Class()}
+class TestDatabase:
+ classes = {'other': TestClass()}
def getclass(self, name):
return Class()
def __getattr(self, name):
return Class()
-class Client:
- write = None
-
-class NodeCase(unittest.TestCase):
+class FunctionCase(unittest.TestCase):
def setUp(self):
''' Set up the harness for calling the individual tests
'''
self.tf = tf = TemplateFunctions()
tf.nodeid = '1'
- tf.cl = Class()
+ tf.cl = TestClass()
tf.classname = 'test_class'
tf.properties = tf.cl.getprops()
- tf.db = Database()
+ tf.db = TestDatabase()
# def do_plain(self, property, escape=0):
def testPlain_string(self):
'<a href="javascript:help_window(\'classhelp?classname=theclass'
'&properties=prop1,prop2\', \'400\', \'400\')"><b>(?)</b></a>')
-# def do_multiline(self, property, rows=5, cols=40)
+# def do_email(self, property, rows=5, cols=40)
def testEmail_string(self):
self.assertEqual(self.tf.do_email('email'), 'test at foo domain example')
@@ -414,12 +411,147 @@ the key2:<input type="checkbox" checked name="multilink" value="the key2">''')
self.assertEqual(self.tf.do_email('boolean'), s)
self.assertEqual(self.tf.do_email('number'), s)
+
+from test_db import setupSchema, MyTestCase, config
+
+class Client:
+ user = 'admin'
+
+class IndexTemplateCase(unittest.TestCase):
+ def setUp(self):
+ from roundup.backends import anydbm
+ # remove previous test, ignore errors
+ if os.path.exists(config.DATABASE):
+ shutil.rmtree(config.DATABASE)
+ os.makedirs(config.DATABASE + '/files')
+ self.db = anydbm.Database(config, 'test')
+ setupSchema(self.db, 1, anydbm)
+
+ client = Client()
+ client.db = self.db
+ client.instance = None
+ self.tf = tf = IndexTemplate(client, '', 'issue')
+ tf.props = ['title']
+
+ # admin user
+ r = str(self.db.role.lookup('Admin'))
+ self.db.user.create(username="admin", roles=[r])
+ r = str(self.db.role.lookup('User'))
+ self.db.user.create(username="anonymous", roles=[r])
+
+ def testBasic(self):
+ self.assertEqual(self.tf.execute_template('hello'), 'hello')
+
+ def testValue(self):
+ self.tf.nodeid = self.db.issue.create(title="spam", status='1')
+ self.assertEqual(self.tf.execute_template('<display call="plain(\'title\')">'), 'spam')
+
+ def testColumnSelection(self):
+ self.tf.nodeid = self.db.issue.create(title="spam", status='1')
+ self.assertEqual(self.tf.execute_template('<property name="title">'
+ '<display call="plain(\'title\')"></property>'
+ '<property name="bar">hello</property>'), 'spam')
+ self.tf.props = ['bar']
+ self.assertEqual(self.tf.execute_template('<property name="title">'
+ '<display call="plain(\'title\')"></property>'
+ '<property name="bar">hello</property>'), 'hello')
+
+ def testSecurityPass(self):
+ self.assertEqual(self.tf.execute_template(
+ '<require permission="Edit">hello<else>foo</require>'), 'hello')
+
+ def testSecurityPassValue(self):
+ self.tf.nodeid = self.db.issue.create(title="spam", status='1')
+ self.assertEqual(self.tf.execute_template(
+ '<require permission="Edit">'
+ '<display call="plain(\'title\')">'
+ '<else>not allowed</require>'), 'spam')
+
+ def testSecurityFail(self):
+ self.tf.client.user = 'anonymous'
+ self.assertEqual(self.tf.execute_template(
+ '<require permission="Edit">hello<else>foo</require>'), 'foo')
+
+ def testSecurityFailValue(self):
+ self.tf.nodeid = self.db.issue.create(title="spam", status='1')
+ self.tf.client.user = 'anonymous'
+ self.assertEqual(self.tf.execute_template(
+ '<require permission="Edit">allowed<else>'
+ '<display call="plain(\'title\')"></require>'), 'spam')
+
+ def tearDown(self):
+ if os.path.exists('_test_dir'):
+ shutil.rmtree('_test_dir')
+
+
+class ItemTemplateCase(unittest.TestCase):
+ def setUp(self):
+ ''' Set up the harness for calling the individual tests
+ '''
+ from roundup.backends import anydbm
+ # remove previous test, ignore errors
+ if os.path.exists(config.DATABASE):
+ shutil.rmtree(config.DATABASE)
+ os.makedirs(config.DATABASE + '/files')
+ self.db = anydbm.Database(config, 'test')
+ setupSchema(self.db, 1, anydbm)
+
+ client = Client()
+ client.db = self.db
+ client.instance = None
+ self.tf = tf = IndexTemplate(client, '', 'issue')
+ tf.nodeid = self.db.issue.create(title="spam", status='1')
+
+ # admin user
+ r = str(self.db.role.lookup('Admin'))
+ self.db.user.create(username="admin", roles=[r])
+ r = str(self.db.role.lookup('User'))
+ self.db.user.create(username="anonymous", roles=[r])
+
+ def testBasic(self):
+ self.assertEqual(self.tf.execute_template('hello'), 'hello')
+
+ def testValue(self):
+ self.assertEqual(self.tf.execute_template('<display call="plain(\'title\')">'), 'spam')
+
+ def testSecurityPass(self):
+ self.assertEqual(self.tf.execute_template(
+ '<require permission="Edit">hello<else>foo</require>'), 'hello')
+
+ def testSecurityPassValue(self):
+ self.assertEqual(self.tf.execute_template(
+ '<require permission="Edit">'
+ '<display call="plain(\'title\')">'
+ '<else>not allowed</require>'), 'spam')
+
+ def testSecurityFail(self):
+ self.tf.client.user = 'anonymous'
+ self.assertEqual(self.tf.execute_template(
+ '<require permission="Edit">hello<else>foo</require>'), 'foo')
+
+ def testSecurityFailValue(self):
+ self.tf.client.user = 'anonymous'
+ self.assertEqual(self.tf.execute_template(
+ '<require permission="Edit">allowed<else>'
+ '<display call="plain(\'title\')"></require>'), 'spam')
+
+ def tearDown(self):
+ if os.path.exists('_test_dir'):
+ shutil.rmtree('_test_dir')
+
def suite():
- return unittest.makeSuite(NodeCase, 'test')
+ return unittest.TestSuite([
+ unittest.makeSuite(FunctionCase, 'test'),
+ unittest.makeSuite(IndexTemplateCase, 'test'),
+ unittest.makeSuite(ItemTemplateCase, 'test'),
+ ])
#
# $Log: not supported by cvs2svn $
+# Revision 1.17 2002/07/18 23:07:07 richard
+# Unit tests and a few fixes.
+#
# Revision 1.16 2002/07/09 05:20:09 richard
# . added email display function - mangles email addrs so they're not so easily
# scraped from the web
diff --git a/test/test_security.py b/test/test_security.py
--- /dev/null
+++ b/test/test_security.py
@@ -0,0 +1,99 @@
+# Copyright (c) 2002 ekit.com Inc (http://www.ekit-inc.com/)
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+# $Id: test_security.py,v 1.1 2002-07-25 07:14:06 richard Exp $
+
+import os, unittest, shutil
+
+from roundup.password import Password
+from test_db import setupSchema, MyTestCase, config
+
+class PermissionTest(MyTestCase):
+ def setUp(self):
+ from roundup.backends import anydbm
+ # remove previous test, ignore errors
+ if os.path.exists(config.DATABASE):
+ shutil.rmtree(config.DATABASE)
+ os.makedirs(config.DATABASE + '/files')
+ self.db = anydbm.Database(config, 'test')
+ setupSchema(self.db, 1, anydbm)
+
+ def testInterfaceSecurity(self):
+ ' test that the CGI and mailgw have initialised security OK '
+ # TODO: some asserts
+
+ def testInitialiseSecurity(self):
+ ''' Create some Permissions and Roles on the security object
+
+ This function is directly invoked by security.Security.__init__()
+ as a part of the Security object instantiation.
+ '''
+ ei = self.db.security.addPermission(name="Edit", klass="issue",
+ description="User is allowed to edit issues")
+ self.db.security.addPermissionToRole('User', ei)
+ ai = self.db.security.addPermission(name="Assign", klass="issue",
+ description="User may be assigned to issues")
+ self.db.security.addPermissionToRole('User', ai)
+
+ def testDBinit(self):
+ r = str(self.db.role.lookup('Admin'))
+ self.db.user.create(username="admin", roles=[r])
+ r = str(self.db.role.lookup('User'))
+ self.db.user.create(username="anonymous", roles=[r])
+
+ def testAccess(self):
+ self.testDBinit()
+ self.testInitialiseSecurity()
+
+ # test class-level access
+ userid = self.db.user.lookup('admin')
+ self.assertEquals(self.db.security.hasClassPermission('issue',
+ 'Edit', userid), 1)
+ self.assertEquals(self.db.security.hasClassPermission('user',
+ 'Edit', userid), 1)
+ userid = self.db.user.lookup('anonymous')
+ self.assertEquals(self.db.security.hasClassPermission('issue',
+ 'Edit', userid), 1)
+ self.assertEquals(self.db.security.hasClassPermission('user',
+ 'Edit', userid), 0)
+
+ # test node-level access
+ issueid = self.db.issue.create(title='foo', assignedto='admin')
+ userid = self.db.user.lookup('admin')
+ self.assertEquals(self.db.security.hasNodePermission('issue',
+ issueid, assignedto=userid), 1)
+ self.assertEquals(self.db.security.hasNodePermission('issue',
+ issueid, nosy=userid), 0)
+ self.db.issue.set(issueid, nosy=[userid])
+ self.assertEquals(self.db.security.hasNodePermission('issue',
+ issueid, nosy=userid), 1)
+
+def suite():
+ return unittest.makeSuite(PermissionTest)
+
+
+#
+# $Log: not supported by cvs2svn $
+# Revision 1.1 2002/07/10 06:40:01 richard
+# ehem, forgot to add
+#
+#
+#
+# vim: set filetype=python ts=4 sw=4 et si