changeset 2649:1df7d4a41da4

Buncha stuff (sorry about the large checkin): - Permissions may now be defined on a per-property basis - added "Create" Permission. Replaces the "Web"- and "Email Registration" Permissions. - added option to turn off registration confirmation via email ("instant_registration" in config) Migrated the user edit/view permission to use check code. Fixed a buncha stuff in the default templates. Needs a thorough review though.
author Richard Jones <richard@users.sourceforge.net>
date Wed, 28 Jul 2004 02:29:46 +0000
parents fe71e108d998
children d68a444fcce3
files CHANGES.txt demo.py roundup/backends/back_anydbm.py roundup/backends/back_metakit.py roundup/backends/rdbms_common.py roundup/cgi/actions.py roundup/cgi/client.py roundup/cgi/templating.py roundup/configuration.py roundup/mailgw.py roundup/security.py templates/classic/config.ini templates/classic/html/page.html templates/classic/html/user.register.html templates/classic/schema.py
diffstat 15 files changed, 342 insertions(+), 308 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGES.txt	Tue Jul 27 11:36:01 2004 +0000
+++ b/CHANGES.txt	Wed Jul 28 02:29:46 2004 +0000
@@ -17,6 +17,13 @@
 - roundup-server options -g and -u accept both ids and names (sf bug 983769)
 - roundup-server now has a configuration file (-C option)
 - added mod_python interface (see installation.txt)
+- reorganised tracker configuration, using ConfigParser config, cleaned-up
+  schema definition and implementing easier extension writing (sf rfe 661301)
+- Permissions may now be defined on a per-property basis
+- added "Create" Permission. Replaces the "Web"- and "Email Registration"
+  Permissions.
+- added option to turn off registration confirmation via email
+  ("instant_registration" in config) (sf rfe 922209)
 
 
 2004-??-?? 0.7.7
--- a/demo.py	Tue Jul 27 11:36:01 2004 +0000
+++ b/demo.py	Wed Jul 28 02:29:46 2004 +0000
@@ -2,7 +2,7 @@
 #
 # Copyright (c) 2003 Richard Jones (richard@mechanicalcat.net)
 #
-# $Id: demo.py,v 1.15 2004-07-27 11:36:01 a1s Exp $
+# $Id: demo.py,v 1.16 2004-07-28 02:29:45 richard Exp $
 
 import sys, os, string, re, urlparse, ConfigParser
 import shutil, socket, errno, BaseHTTPServer
@@ -59,6 +59,7 @@
     config['TRACKER_WEB'] = 'http://%s:%s/demo/'%(hostname, port)
 
     # write the config
+    config['INSTANT_REGISTRATION'] = 1
     config.save()
 
     # open the tracker and initialise
--- a/roundup/backends/back_anydbm.py	Tue Jul 27 11:36:01 2004 +0000
+++ b/roundup/backends/back_anydbm.py	Wed Jul 28 02:29:46 2004 +0000
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-#$Id: back_anydbm.py,v 1.169 2004-07-27 04:28:39 richard Exp $
+#$Id: back_anydbm.py,v 1.170 2004-07-28 02:29:45 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
 versions >2.1.1 (the dumbdbm fallback in 2.1.1 and earlier has several
@@ -144,6 +144,8 @@
         self.classes[cn] = cl
 
         # add default Edit and View permissions
+        self.security.addPermission(name="Create", klass=cn,
+            description="User is allowed to create "+cn)
         self.security.addPermission(name="Edit", klass=cn,
             description="User is allowed to edit "+cn)
         self.security.addPermission(name="View", klass=cn,
--- a/roundup/backends/back_metakit.py	Tue Jul 27 11:36:01 2004 +0000
+++ b/roundup/backends/back_metakit.py	Wed Jul 28 02:29:46 2004 +0000
@@ -1,4 +1,4 @@
-# $Id: back_metakit.py,v 1.82 2004-07-27 00:57:18 richard Exp $
+# $Id: back_metakit.py,v 1.83 2004-07-28 02:29:45 richard Exp $
 '''Metakit backend for Roundup, originally by Gordon McMillan.
 
 Known Current Bugs:
@@ -186,6 +186,8 @@
             self.tables.append(name=cl.classname)
 
         # add default Edit and View permissions
+        self.security.addPermission(name="Create", klass=cn,
+            description="User is allowed to create "+cn)
         self.security.addPermission(name="Edit", klass=cl.classname,
             description="User is allowed to edit "+cl.classname)
         self.security.addPermission(name="View", klass=cl.classname,
--- a/roundup/backends/rdbms_common.py	Tue Jul 27 11:36:01 2004 +0000
+++ b/roundup/backends/rdbms_common.py	Wed Jul 28 02:29:46 2004 +0000
@@ -1,4 +1,4 @@
-# $Id: rdbms_common.py,v 1.126 2004-07-27 04:28:39 richard Exp $
+# $Id: rdbms_common.py,v 1.127 2004-07-28 02:29:45 richard Exp $
 ''' Relational database (SQL) backend common code.
 
 Basics:
@@ -607,6 +607,8 @@
         self.classes[cn] = cl
 
         # add default Edit and View permissions
+        self.security.addPermission(name="Create", klass=cn,
+            description="User is allowed to create "+cn)
         self.security.addPermission(name="Edit", klass=cn,
             description="User is allowed to edit "+cn)
         self.security.addPermission(name="View", klass=cn,
--- a/roundup/cgi/actions.py	Tue Jul 27 11:36:01 2004 +0000
+++ b/roundup/cgi/actions.py	Wed Jul 28 02:29:46 2004 +0000
@@ -1,4 +1,4 @@
-#$Id: actions.py,v 1.35 2004-07-20 02:07:58 richard Exp $
+#$Id: actions.py,v 1.36 2004-07-28 02:29:45 richard Exp $
 
 import re, cgi, StringIO, urllib, Cookie, time, random
 
@@ -53,10 +53,13 @@
             raise Unauthorised, self._('You do not have permission to '
                 '%(action)s the %(classname)s class.')%info
 
-    def hasPermission(self, permission):
+    _marker = []
+    def hasPermission(self, permission, classname=_marker):
         """Check whether the user has 'permission' on the current class."""
+        if classname is self._marker:
+            classname = self.client.classname
         return self.db.security.hasPermission(permission, self.client.userid,
-            self.client.classname)
+            classname)
 
     def gettext(self, msgid):
         """Return the localized translation of msgid"""
@@ -314,46 +317,8 @@
 
         self.client.ok_message.append(self._('Items edited OK'))
 
-class _EditAction(Action):
-    def isEditingSelf(self):
-        """Check whether a user is editing his/her own details."""
-        return (self.nodeid == self.userid
-                and self.db.user.get(self.nodeid, 'username') != 'anonymous')
-
-    def editItemPermission(self, props):
-        """Determine whether the user has permission to edit this item.
-
-        Base behaviour is to check the user can edit this class. If we're
-        editing the "user" class, users are allowed to edit their own details.
-        Unless it's the "roles" property, which requires the special Permission
-        "Web Roles".
-        """
-        if self.classname == 'user':
-            if props.has_key('roles') and not self.hasPermission('Web Roles'):
-                raise Unauthorised, self._(
-                    "You do not have permission to edit user roles")
-            if self.isEditingSelf():
-                return 1
-        if self.hasPermission('Edit'):
-            return 1
-        return 0
-
-    def newItemPermission(self, props):
-        """Determine whether the user has permission to create (edit) this item.
-
-        Base behaviour is to check the user can edit this class. No additional
-        property checks are made. Additionally, new user items may be created
-        if the user has the "Web Registration" Permission.
-
-        """
-        if (self.classname == 'user' and self.hasPermission('Web Registration')
-            or self.hasPermission('Edit')):
-            return 1
-        return 0
-
-    #
-    #  Utility methods for editing
-    #
+class EditCommon:
+    '''Utility methods for editing.'''
     def _editnodes(self, all_props, all_links, newids=None):
         ''' Use the props in all_props to perform edit and creation, then
             use the link specs in all_links to do linking.
@@ -475,7 +440,38 @@
         cl = self.db.classes[cn]
         return cl.create(**props)
 
-class EditItemAction(_EditAction):
+    def isEditingSelf(self):
+        """Check whether a user is editing his/her own details."""
+        return (self.nodeid == self.userid
+                and self.db.user.get(self.nodeid, 'username') != 'anonymous')
+
+    def editItemPermission(self, props):
+        """Determine whether the user has permission to edit this item.
+
+        Base behaviour is to check the user can edit this class. If we're
+        editing the "user" class, users are allowed to edit their own details.
+        Unless it's the "roles" property, which requires the special Permission
+        "Web Roles".
+        """
+        if self.classname == 'user':
+            if props.has_key('roles') and not self.hasPermission('Web Roles'):
+                raise Unauthorised, self._(
+                    "You do not have permission to edit user roles")
+            if self.isEditingSelf():
+                return 1
+        if self.hasPermission('Edit'):
+            return 1
+        return 0
+
+    def newItemPermission(self, props):
+        """Determine whether the user has permission to create this item.
+
+        Base behaviour is to check the user can edit this class. No additional
+        property checks are made.
+        """
+        return self.hasPermission('Create', self.classname)
+
+class EditItemAction(EditCommon, Action):
     def lastUserActivity(self):
         if self.form.has_key(':lastactivity'):
             d = date.Date(self.form[':lastactivity'].value)
@@ -539,7 +535,7 @@
             url += '&' + req.indexargs_href('', {})[1:]
         raise Redirect, url
 
-class NewItemAction(_EditAction):
+class NewItemAction(EditCommon, Action):
     def handle(self):
         ''' Add a new item to the database.
 
@@ -677,28 +673,21 @@
 
         self.client.ok_message.append(self._('Email sent to %s') % address)
 
-class ConfRegoAction(Action):
-    def handle(self):
-        """Grab the OTK, use it to load up the new user details."""
-        try:
-            # pull the rego information out of the otk database
-            self.userid = self.db.confirm_registration(self.form['otk'].value)
-        except (ValueError, KeyError), message:
-            self.client.error_message.append(str(message))
-            return
-
+class RegoCommon:
+    def finishRego(self):
         # log the new user in
-        self.client.user = self.db.user.get(self.userid, 'username')
+        self.client.userid = self.userid
+        user = self.client.user = self.db.user.get(self.userid, 'username')
         # re-open the database for real, using the user
-        self.client.opendb(self.client.user)
+        self.client.opendb(user)
 
         # if we have a session, update it
-        if hasattr(self, 'session'):
-            self.client.db.sessions.set(self.session, user=self.user,
-                last_use=time.time())
+        if hasattr(self.client, 'session'):
+            self.client.db.getSessionManager().set(self.client.session,
+                user=user, last_use=time.time())
         else:
             # new session cookie
-            self.client.set_cookie(self.user)
+            self.client.set_cookie(user)
 
         # nice message
         message = self._('You are now registered, welcome!')
@@ -713,9 +702,20 @@
             window.setTimeout('window.location = "%s"', 1000);
             </script>'''%(message, url, message, url)
 
-class RegisterAction(Action):
+class ConfRegoAction(RegoCommon, Action):
+    def handle(self):
+        """Grab the OTK, use it to load up the new user details."""
+        try:
+            # pull the rego information out of the otk database
+            self.userid = self.db.confirm_registration(self.form['otk'].value)
+        except (ValueError, KeyError), message:
+            self.client.error_message.append(str(message))
+            return
+        self.finishRego()
+
+class RegisterAction(RegoCommon, EditCommon, Action):
     name = 'register'
-    permissionType = 'Web Registration'
+    permissionType = 'Create'
 
     def handle(self):
         """Attempt to create a new user based on the contents of the form
@@ -723,38 +723,59 @@
 
         Return 1 on successful login.
         """
-        props = self.client.parsePropsFromForm(create=1)[0][('user', None)]
+        # parse the props from the form
+        try:
+            props, links = self.client.parsePropsFromForm(create=1)
+        except (ValueError, KeyError), message:
+            self.client.error_message.append(self._('Error: %s')
+                % str(message))
+            return
 
         # registration isn't allowed to supply roles
-        if props.has_key('roles'):
+        user_props = props[('user', None)]
+        if user_props.has_key('roles'):
             raise Unauthorised, self._(
                 "It is not permitted to supply roles at registration.")
 
-        username = props['username']
-        try:
-            self.db.user.lookup(username)
-            self.client.error_message.append(self._('Error: A user with the '
-                'username "%(username)s" already exists')%props)
-            return
-        except KeyError:
-            pass
+        # skip the confirmation step?
+        if self.db.config['INSTANT_REGISTRATION']:
+            # handle the create now
+            try:
+                # when it hits the None element, it'll set self.nodeid
+                messages = self._editnodes(props, links)
+            except (ValueError, KeyError, IndexError, exceptions.Reject), \
+                    message:
+                # these errors might just be indicative of user dumbness
+                self.client.error_message.append(_('Error: %s') % str(message))
+                return
+
+            # fix up the initial roles
+            self.db.user.set(self.nodeid,
+                roles=self.db.config['NEW_WEB_USER_ROLES'])
+
+            # commit now that all the tricky stuff is done
+            self.db.commit()
+
+            # finish off by logging the user in
+            self.userid = self.nodeid
+            return self.finishRego()
 
         # generate the one-time-key and store the props for later
         for propname, proptype in self.db.user.getprops().items():
-            value = props.get(propname, None)
+            value = user_props.get(propname, None)
             if value is None:
                 pass
             elif isinstance(proptype, hyperdb.Date):
-                props[propname] = str(value)
+                user_props[propname] = str(value)
             elif isinstance(proptype, hyperdb.Interval):
-                props[propname] = str(value)
+                user_props[propname] = str(value)
             elif isinstance(proptype, hyperdb.Password):
-                props[propname] = str(value)
+                user_props[propname] = str(value)
         otks = self.db.getOTKManager()
         otk = ''.join([random.choice(chars) for x in range(32)])
         while otks.exists(otk):
             otk = ''.join([random.choice(chars) for x in range(32)])
-        otks.set(otk, **props)
+        otks.set(otk, **user_props)
 
         # send the email
         tracker_name = self.db.config.TRACKER_NAME
@@ -771,9 +792,9 @@
 
 %(url)s?@action=confrego&otk=%(otk)s
 
-""" % {'name': props['username'], 'tracker': tracker_name, 'url': self.base,
-        'otk': otk, 'tracker_email': tracker_email}
-        if not self.client.standard_message([props['address']], subject,
+""" % {'name': user_props['username'], 'tracker': tracker_name,
+        'url': self.base, 'otk': otk, 'tracker_email': tracker_email}
+        if not self.client.standard_message([user_props['address']], subject,
                 body, (tracker_name, tracker_email)):
             return
 
--- a/roundup/cgi/client.py	Tue Jul 27 11:36:01 2004 +0000
+++ b/roundup/cgi/client.py	Wed Jul 28 02:29:46 2004 +0000
@@ -1,4 +1,4 @@
-# $Id: client.py,v 1.185 2004-07-27 02:30:31 richard Exp $
+# $Id: client.py,v 1.186 2004-07-28 02:29:45 richard Exp $
 
 """WWW request handler (also used in the stand-alone server).
 """
@@ -22,8 +22,6 @@
     This function is directly invoked by security.Security.__init__()
     as a part of the Security object instantiation.
     '''
-    security.addPermission(name="Web Registration",
-        description="User may register through the web")
     p = security.addPermission(name="Web Access",
         description="User may access the web interface")
     security.addPermissionToRole('Admin', p)
@@ -398,6 +396,9 @@
         # make sure the anonymous user is valid if we're using it
         if user == 'anonymous':
             self.make_user_anonymous()
+            if not self.db.security.hasPermission('Web Access', self.userid):
+                raise Unauthorised, self._("Anonymous users are not "
+                    "allowed to use the web interface")
         else:
             self.user = user
 
--- a/roundup/cgi/templating.py	Tue Jul 27 11:36:01 2004 +0000
+++ b/roundup/cgi/templating.py	Wed Jul 28 02:29:46 2004 +0000
@@ -262,17 +262,10 @@
     }
     # add in the item if there is one
     if client.nodeid:
-        if classname == 'user':
-            c['context'] = HTMLUser(client, classname, client.nodeid,
-                anonymous=1)
-        else:
-            c['context'] = HTMLItem(client, classname, client.nodeid,
-                anonymous=1)
+        c['context'] = HTMLItem(client, classname, client.nodeid,
+            anonymous=1)
     elif client.db.classes.has_key(classname):
-        if classname == 'user':
-            c['context'] = HTMLUserClass(client, classname, anonymous=1)
-        else:
-            c['context'] = HTMLClass(client, classname, anonymous=1)
+        c['context'] = HTMLClass(client, classname, anonymous=1)
     return c
 
 class RoundupPageTemplate(PageTemplate.PageTemplate):
@@ -328,15 +321,9 @@
         if m:
             cl = m.group('cl')
             self._client.db.getclass(cl)
-            if cl == 'user':
-                klass = HTMLUser
-            else:
-                klass = HTMLItem
-            return klass(self._client, cl, m.group('id'))
+            return HTMLItem(self._client, cl, m.group('id'))
         else:
             self._client.db.getclass(item)
-            if item == 'user':
-                return HTMLUserClass(self._client, item)
             return HTMLClass(self._client, item)
 
     def __getattr__(self, attr):
@@ -350,10 +337,7 @@
         l.sort()
         m = []
         for item in l:
-            if item == 'user':
-                m.append(HTMLUserClass(self._client, item))
-            else:
-                m.append(HTMLClass(self._client, item))
+            m.append(HTMLClass(self._client, item))
         return m
 
 def lookupIds(db, prop, ids, fail_ok=0, num_re=re.compile('^-?\d+$')):
@@ -386,42 +370,6 @@
             l.append(entry)
     return l
 
-class HTMLPermissions:
-    ''' Helpers that provide answers to commonly asked Permission questions.
-    '''
-    def is_edit_ok(self):
-        ''' Is the user allowed to Edit the current class?
-        '''
-        return self._db.security.hasPermission('Edit', self._client.userid,
-            self._classname)
-
-    def is_view_ok(self):
-        ''' Is the user allowed to View the current class?
-        '''
-        return self._db.security.hasPermission('View', self._client.userid,
-            self._classname)
-
-    def is_only_view_ok(self):
-        ''' Is the user only allowed to View (ie. not Edit) the current class?
-        '''
-        return self.is_view_ok() and not self.is_edit_ok()
-
-    def view_check(self):
-        ''' Raise the Unauthorised exception if the user's not permitted to
-            view this class.
-        '''
-        if not self.is_view_ok():
-            raise Unauthorised("view", self._classname,
-                translator=self._client.translator)
-
-    def edit_check(self):
-        ''' Raise the Unauthorised exception if the user's not permitted to
-            edit this class.
-        '''
-        if not self.is_edit_ok():
-            raise Unauthorised("edit", self._classname,
-                translator=self._client.translator)
-
 def input_html4(**attrs):
     """Generate an 'input' (html4) element with given attributes"""
     return '<input %s>'%' '.join(['%s="%s"'%item for item in attrs.items()])
@@ -453,6 +401,32 @@
 
     _ = gettext
 
+class HTMLPermissions:
+
+    def view_check(self):
+        ''' Raise the Unauthorised exception if the user's not permitted to
+            view this class.
+        '''
+        if not self.is_view_ok():
+            raise Unauthorised("view", self._classname,
+                translator=self._client.translator)
+
+    def create_check(self):
+        ''' Raise the Unauthorised exception if the user's not permitted to
+            create items of this class.
+        '''
+        if not self.is_create_ok():
+            raise Unauthorised("create", self._classname,
+                translator=self._client.translator)
+
+    def edit_check(self):
+        ''' Raise the Unauthorised exception if the user's not permitted to
+            edit items of this class.
+        '''
+        if not self.is_edit_ok():
+            raise Unauthorised("edit", self._classname,
+                translator=self._client.translator)
+
 class HTMLClass(HTMLInputMixin, HTMLPermissions):
     ''' Accesses through a class (either through *class* or *db.<classname>*)
     '''
@@ -469,6 +443,25 @@
 
         HTMLInputMixin.__init__(self)
 
+    def is_edit_ok(self):
+        ''' Is the user allowed to Create the current class?
+        '''
+        return self._db.security.hasPermission('Create', self._client.userid,
+            self._classname)
+
+    def is_view_ok(self):
+        ''' Is the user allowed to View the current class?
+        '''
+        if self._db.security.hasPermission('View', self._client.userid,
+                self._classname):
+            return 1
+        return self.is_create_ok()
+
+    def is_only_view_ok(self):
+        ''' Is the user only allowed to View (ie. not Create) the current class?
+        '''
+        return self.is_view_ok() and not self.is_create_ok()
+
     def __repr__(self):
         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
 
@@ -534,12 +527,7 @@
         if not isinstance(itemid, type(1)) and not num_re.match(itemid):
             itemid = self._klass.lookup(itemid)
 
-        if self.classname == 'user':
-            klass = HTMLUser
-        else:
-            klass = HTMLItem
-
-        return klass(self._client, self.classname, itemid)
+        return HTMLItem(self._client, self.classname, itemid)
 
     def properties(self, sort=1):
         ''' Return HTMLProperty for all of this class' properties.
@@ -561,17 +549,12 @@
     def list(self, sort_on=None):
         ''' List all items in this class.
         '''
-        if self.classname == 'user':
-            klass = HTMLUser
-        else:
-            klass = HTMLItem
-
         # get the list and sort it nicely
         l = self._klass.list()
         sortfunc = make_sort_function(self._db, self.classname, sort_on)
         l.sort(sortfunc)
 
-        l = [klass(self._client, self.classname, x) for x in l]
+        l = [HTMLItem(self._client, self.classname, x) for x in l]
         return l
 
     def csv(self):
@@ -615,11 +598,7 @@
             filterspec = request.filterspec
             sort = request.sort
             group = request.group
-        if self.classname == 'user':
-            klass = HTMLUser
-        else:
-            klass = HTMLItem
-        l = [klass(self._client, self.classname, x)
+        l = [HTMLItem(self._client, self.classname, x)
              for x in self._klass.filter(None, filterspec, sort, group)]
         return l
 
@@ -683,7 +662,7 @@
         }
         return pt.render(self._client, self.classname, req, **args)
 
-class HTMLItem(HTMLInputMixin, HTMLPermissions):
+class _HTMLItem(HTMLInputMixin, HTMLPermissions):
     ''' Accesses through an *item*
     '''
     def __init__(self, client, classname, nodeid, anonymous=0):
@@ -699,6 +678,25 @@
 
         HTMLInputMixin.__init__(self)
 
+    def is_edit_ok(self):
+        ''' Is the user allowed to Edit the current class?
+        '''
+        return self._db.security.hasPermission('Edit', self._client.userid,
+            self._classname, itemid=self._nodeid)
+
+    def is_view_ok(self):
+        ''' Is the user allowed to View the current class?
+        '''
+        if self._db.security.hasPermission('View', self._client.userid,
+                self._classname, itemid=self._nodeid):
+            return 1
+        return self.is_edit_ok()
+
+    def is_only_view_ok(self):
+        ''' Is the user only allowed to View (ie. not Edit) the current class?
+        '''
+        return self.is_view_ok() and not self.is_edit_ok()
+
     def __repr__(self):
         return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
             self._nodeid)
@@ -1010,66 +1008,26 @@
         url = '%s%s/%s'%(self._classname, self._nodeid, name)
         return urllib.quote(url)
 
-
-class HTMLUserPermission:
-
-    def is_edit_ok(self):
-        ''' Is the user allowed to Edit the current class?
-            Also check whether this is the current user's info.
-        '''
-        return self._user_perm_check('Edit')
-
-    def is_view_ok(self):
-        ''' Is the user allowed to View the current class?
-            Also check whether this is the current user's info.
-        '''
-        return self._user_perm_check('View')
-
-    def _user_perm_check(self, type):
-        # some users may view / edit all users
-        s = self._db.security
-        userid = self._client.userid
-        if s.hasPermission(type, userid, self._classname):
-            return 1
-
-        # users may view their own info
-        is_anonymous = self._db.user.get(userid, 'username') == 'anonymous'
-        if getattr(self, '_nodeid', None) == userid and not is_anonymous:
-            return 1
-
-        # may anonymous users register? (so, they need to be anonymous,
-        # need the Web Rego permission, and not trying to view an item)
-        rego = s.hasPermission('Web Registration', userid, self._classname)
-        rego = rego and self._client.template == 'register'
-        if is_anonymous and rego and getattr(self, '_nodeid', None) is None:
-            return 1
-
-        # nope, no access here
-        return 0
-
-class HTMLUserClass(HTMLUserPermission, HTMLClass):
-    pass
-
-class HTMLUser(HTMLUserPermission, HTMLItem):
-    ''' Accesses through the *user* (a special case of item)
+class _HTMLUser(_HTMLItem):
+    '''Add ability to check for permissions on users.
     '''
-    def __init__(self, client, classname, nodeid, anonymous=0):
-        HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
-        self._default_classname = client.classname
-
-        # used for security checks
-        self._security = client.db.security
-
     _marker = []
     def hasPermission(self, permission, classname=_marker):
-        ''' Determine if the user has the Permission.
+        '''Determine if the user has the Permission.
 
-            The class being tested defaults to the template's class, but may
-            be overidden for this test by suppling an alternate classname.
+        The class being tested defaults to the template's class, but may
+        be overidden for this test by suppling an alternate classname.
         '''
         if classname is self._marker:
-            classname = self._default_classname
-        return self._security.hasPermission(permission, self._nodeid, classname)
+            classname = self._client.classname
+        return self._client.db.security.hasPermission(permission,
+            self._nodeid, classname)
+
+def HTMLItem(client, classname, nodeid, anonymous=0):
+    if classname == 'user':
+        return _HTMLUser(client, classname, nodeid, anonymous)
+    else:
+        return _HTMLItem(client, classname, nodeid, anonymous)
 
 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
     ''' String, Number, Date, Interval HTMLProperty
@@ -1116,24 +1074,23 @@
         return self._value is not None
 
     def is_edit_ok(self):
-        ''' Is the user allowed to Edit the current class?
+        '''Should the user be allowed to use an edit form field for this
+        property. Check "Create" for new items, or "Edit" for existing
+        ones.
         '''
-        thing = HTMLDatabase(self._client)[self._classname]
         if self._nodeid:
-            # this is a special-case for the User class where permission's
-            # on a per-item basis :(
-            thing = thing.getItem(self._nodeid)
-        return thing.is_edit_ok()
+            return self._db.security.hasPermission('Edit', self._client.userid,
+                self._classname, self._name, self._nodeid)
+        return self._db.security.hasPermission('Create', self._client.userid,
+            self._classname, self._name)
 
     def is_view_ok(self):
         ''' Is the user allowed to View the current class?
         '''
-        thing = HTMLDatabase(self._client)[self._classname]
-        if self._nodeid:
-            # this is a special-case for the User class where permission's
-            # on a per-item basis :(
-            thing = thing.getItem(self._nodeid)
-        return thing.is_view_ok()
+        if self._db.security.hasPermission('View', self._client.userid,
+                self._classname, self._name, self._nodeid):
+            return 1
+        return self.is_edit_ok()
 
 class StringHTMLProperty(HTMLProperty):
     hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
@@ -1551,11 +1508,7 @@
        #print 'Link.getattr', (self, attr, self._value)
         if not self._value:
             raise AttributeError, "Can't access missing value"
-        if self._prop.classname == 'user':
-            klass = HTMLUser
-        else:
-            klass = HTMLItem
-        i = klass(self._client, self._prop.classname, self._value)
+        i = HTMLItem(self._client, self._prop.classname, self._value)
         return getattr(i, attr)
 
     def plain(self, escape=0):
@@ -1688,11 +1641,7 @@
         '''
        #print 'Multi.getitem', (self, num)
         value = self._value[num]
-        if self._prop.classname == 'user':
-            klass = HTMLUser
-        else:
-            klass = HTMLItem
-        return klass(self._client, self._prop.classname, value)
+        return HTMLItem(self._client, self._prop.classname, value)
 
     def __contains__(self, value):
         ''' Support the "in" operator. We have to make sure the passed-in
@@ -1709,11 +1658,8 @@
         '''
         l = self._value[:]
         l.reverse()
-        if self._prop.classname == 'user':
-            klass = HTMLUser
-        else:
-            klass = HTMLItem
-        return [klass(self._client, self._prop.classname, value) for value in l]
+        return [HTMLItem(self._client, self._prop.classname, value)
+            for value in l]
 
     def plain(self, escape=0):
         ''' Render a "plain" representation of the property
@@ -1865,7 +1811,7 @@
     - "form" the CGI form as a cgi.FieldStorage
     - "env" the CGI environment variables
     - "base" the base URL for this instance
-    - "user" a HTMLUser instance for this user
+    - "user" a HTMLItem instance for this user
     - "classname" the current classname (possibly None)
     - "template" the current template (suffix, also possibly None)
 
@@ -1888,7 +1834,7 @@
         self.form = client.form
         self.env = client.env
         self.base = client.base
-        self.user = HTMLUser(client, 'user', client.userid)
+        self.user = HTMLItem(client, 'user', client.userid)
 
         # store the current class name and action
         self.classname = client.classname
@@ -2220,10 +2166,7 @@
         item = self._sequence[index + self.first]
         if self.classname:
             # map the item ids to instances
-            if self.classname == 'user':
-                item = HTMLUser(self.client, self.classname, item)
-            else:
-                item = HTMLItem(self.client, self.classname, item)
+            item = HTMLItem(self.client, self.classname, item)
         self.current_item = item
         return item
 
--- a/roundup/configuration.py	Tue Jul 27 11:36:01 2004 +0000
+++ b/roundup/configuration.py	Wed Jul 28 02:29:46 2004 +0000
@@ -1,6 +1,6 @@
 # Roundup Issue Tracker configuration support
 #
-# $Id: configuration.py,v 1.14 2004-07-27 11:26:20 a1s Exp $
+# $Id: configuration.py,v 1.15 2004-07-28 02:29:45 richard Exp $
 #
 __docformat__ = "restructuredtext"
 
@@ -436,6 +436,9 @@
             "Numeric timezone offset used when users do not choose their own\n"
             "in their settings.",
             ["DEFAULT_TIMEZONE"]),
+        (BooleanOption, "instant_registration", "no",
+            "Register new users instantly, or require confirmation via\n"
+            "email?"),
     )),
     ("tracker", (
         (Option, "name", "Roundup issue tracker",
--- a/roundup/mailgw.py	Tue Jul 27 11:36:01 2004 +0000
+++ b/roundup/mailgw.py	Wed Jul 28 02:29:46 2004 +0000
@@ -74,7 +74,7 @@
 an exception, the original message is bounced back to the sender with the
 explanatory message given in the exception.
 
-$Id: mailgw.py,v 1.152 2004-07-26 09:29:22 a1s Exp $
+$Id: mailgw.py,v 1.153 2004-07-28 02:29:45 richard Exp $
 """
 __docformat__ = 'restructuredtext'
 
@@ -117,8 +117,6 @@
         This function is directly invoked by security.Security.__init__()
         as a part of the Security object instantiation.
     '''
-    security.addPermission(name="Email Registration",
-        description="Anonymous may register through e-mail")
     p = security.addPermission(name="Email Access",
         description="User may use the email interface")
     security.addPermissionToRole('Admin', p)
@@ -764,7 +762,7 @@
         # Don't create users if anonymous isn't allowed to register
         create = 1
         anonid = self.db.user.lookup('anonymous')
-        if not self.db.security.hasPermission('Email Registration', anonid):
+        if not self.db.security.hasPermission('Create', 'user', anonid):
             create = 0
 
         # ok, now figure out who the author is - create a new user if the
--- a/roundup/security.py	Tue Jul 27 11:36:01 2004 +0000
+++ b/roundup/security.py	Wed Jul 28 02:29:46 2004 +0000
@@ -16,13 +16,39 @@
         locked to a particular class. That means there may be multiple
         Permissions for the same name for different classes.
     '''
-    def __init__(self, name='', description='', klass=None):
+    def __init__(self, name='', description='', klass=None,
+            property=None, check=None):
         self.name = name
         self.description = description
         self.klass = klass
+        self.property = property
+        self.check = check
+
+    def test(self, db, permission, classname, property, userid, itemid):
+        if permission != self.name:
+            return 0
+
+        # are we checking the correct class
+        if (classname is not None and self.klass is not None
+                and self.klass != classname):
+            return 0
+
+        # what about property?
+        if (property is not None and self.property is not None
+                and self.property != property):
+            return 0
+
+        # check code
+        if self.check is not None:
+            if not self.check(db, userid, itemid):
+                return 0
+
+        # we have a winner
+        return 1
 
     def __repr__(self):
-        return '<Permission 0x%x %r,%r>'%(id(self), self.name, self.klass)
+        return '<Permission 0x%x %r,%r,%r,%r>'%(id(self), self.name,
+            self.klass, self.property, self.check)
 
 class Role:
     ''' Defines a Role with the attributes
@@ -95,24 +121,25 @@
         raise ValueError, 'No permission "%s" defined for "%s"'%(permission,
             classname)
 
-    def hasPermission(self, permission, userid, classname=None):
+    def hasPermission(self, permission, userid, classname=None,
+            property=None, itemid=None):
         ''' 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')
         if roles is None:
             return 0
+        if itemid is not None and classname is None:
+            raise ValueError, 'classname must accompany itemid'
         for rolename in [x.lower().strip() for x in roles.split(',')]:
             if not rolename or not self.role.has_key(rolename):
                 continue
             # for each of the user's Roles, check the permissions
             for perm in self.role[rolename].permissions:
                 # permission name match?
-                if perm.name == permission:
-                    # permission klass match?
-                    if perm.klass is None or perm.klass == classname:
-                        # we have a winner
-                        return 1
+                if perm.test(self.db, permission, classname, property,
+                        userid, itemid):
+                    return 1
         return 0
 
     def hasNodePermission(self, classname, nodeid, **propspec):
--- a/templates/classic/config.ini	Tue Jul 27 11:36:01 2004 +0000
+++ b/templates/classic/config.ini	Wed Jul 28 02:29:46 2004 +0000
@@ -12,7 +12,7 @@
 [tracker]
 name = Roundup issue tracker
 web = http://tracker.example/cgi-bin/roundup.cgi/bugs/
-emaiL = issue_tracker
+email = issue_tracker
 
 [logging]
 level = ERROR
--- a/templates/classic/html/page.html	Tue Jul 27 11:36:01 2004 +0000
+++ b/templates/classic/html/page.html	Wed Jul 28 02:29:46 2004 +0000
@@ -37,8 +37,9 @@
    <p class="classblock"
        tal:condition="python:request.user.hasPermission('View', 'issue')">
     <b i18n:translate="">Issues</b><br>
-    <a tal:condition="python:request.user.hasPermission('Edit', 'issue')"
-      href="issue?@template=item" i18n:translate="">Create New</a><br>
+    <span tal:condition="python:request.user.hasPermission('Create', 'issue')">
+      <a href="issue?@template=item" i18n:translate="">Create New</a><br>
+    </span>
     <a href="issue?@sort=-activity&@group=priority&@filter=status,assignedto&@columns=id,activity,title,creator,status&status=-1,1,2,3,4,5,6,7&assignedto=-1"
      i18n:translate="">Show Unassigned</a><br>
     <a href="issue?@sort=-activity&@group=priority&@filter=status&@columns=id,activity,title,creator,assignedto,status&status=-1,1,2,3,4,5,6,7"
@@ -52,22 +53,28 @@
   </form>
 
   <p class="classblock"
-     tal:condition="python:request.user.hasPermission('Edit', 'keyword')">
+     tal:condition="python:request.user.hasPermission('Edit', 'keyword')
+        or request.user.hasPermission('Create', 'keyword')">
    <b i18n:translate="">Keywords</b><br>
-   <a href="keyword?@template=item" i18n:translate="">Create New</a><br>
-   <a tal:condition="db/keyword/list"
-      href="keyword?@template=item" i18n:translate="">Edit Existing</a><br>
+   <span tal:condition="python:request.user.hasPermission('Create', 'keyword')">
+    <a href="keyword?@template=item" i18n:translate="">Create New</a><br>
+   </span>
+   <span tal:condition="python:db.keyword.list() and
+        request.user.hasPermission('Edit', 'keyword')">
+    <a href="keyword?@template=item" i18n:translate="">Edit Existing</a><br>
+   </span>
   </p>
 
   <p class="classblock"
        tal:condition="python:request.user.hasPermission('View', 'user')">
    <b i18n:translate="">Administration</b><br>
-   <tal:block tal:condition="python:request.user.hasPermission('Edit', None)">
+   <span tal:condition="python:request.user.hasPermission('Edit', None)">
     <a href="home?@template=classlist" i18n:translate="">Class List</a><br>
-   </tal:block>
-   <a tal:condition="python:request.user.hasPermission('View', 'user')
-                            or request.user.hasPermission('Edit', 'user')"
-      href="user"  i18n:translate="">User List</a><br>
+   </span>
+   <span tal:condition="python:request.user.hasPermission('View', 'user')
+                            or request.user.hasPermission('Edit', 'user')">
+    <a href="user"  i18n:translate="">User List</a><br>
+   </span>
    <a tal:condition="python:request.user.hasPermission('Edit', 'user')"
       href="user?@template=item" i18n:translate="">Add User</a>
   </p>
@@ -82,7 +89,7 @@
     <input type="submit" value="Login" i18n:attributes="value"><br>
     <span tal:replace="structure request/indexargs_form" />
     <a href="user?@template=register"
-       tal:condition="python:request.user.hasPermission('Web Registration')"
+       tal:condition="python:request.user.hasPermission('Create', 'user')"
      i18n:translate="">Register</a><br>
     <a href="user?@template=forgotten" i18n:translate="">Lost&nbsp;your&nbsp;login?</a><br>
    </p>
--- a/templates/classic/html/user.register.html	Tue Jul 27 11:36:01 2004 +0000
+++ b/templates/classic/html/user.register.html	Wed Jul 28 02:29:46 2004 +0000
@@ -8,13 +8,6 @@
  tal:replace="db/config/TRACKER_NAME" /></span>
 <td class="content" metal:fill-slot="content">
 
-<tal:block tal:define=" editok python:request.user.username=='anonymous' and
-           request.user.hasPermission('Web Registration')">
-
-<span tal:condition="python:not editok"
- i18n:translate="">You are not allowed to view this page.</span>
-
-<tal:block tal:condition="editok">
 <form method="POST" onSubmit="return submit_once()"
       enctype="multipart/form-data"
       tal:attributes="action context/designator">
@@ -73,10 +66,6 @@
 </table>
 </form>
 
-</tal:block>
-
-</tal:block>
-
 </td>
 
 </tal:block>
--- a/templates/classic/schema.py	Tue Jul 27 11:36:01 2004 +0000
+++ b/templates/classic/schema.py	Wed Jul 28 02:29:46 2004 +0000
@@ -80,6 +80,16 @@
 #
 # See the configuration and customisation document for information
 # about security setup.
+
+#
+# REGULAR USERS
+#
+# Give the regular users access to the web and email interface
+p = db.security.getPermission('Web Access')
+db.security.addPermissionToRole('User', p)
+p = db.security.getPermission('Email Access')
+db.security.addPermissionToRole('User', p)
+
 # Assign the access and edit Permissions for issue, file and message
 # to regular users now
 for cl in 'issue', 'file', 'msg', 'query', 'keyword':
@@ -87,43 +97,64 @@
     db.security.addPermissionToRole('User', p)
     p = db.security.getPermission('Edit', cl)
     db.security.addPermissionToRole('User', p)
+    p = db.security.getPermission('Create', cl)
+    db.security.addPermissionToRole('User', p)
 for cl in 'priority', 'status':
     p = db.security.getPermission('View', cl)
     db.security.addPermissionToRole('User', p)
 
-# and give the regular users access to the web and email interface
-p = db.security.getPermission('Web Access')
-db.security.addPermissionToRole('User', p)
-p = db.security.getPermission('Email Access')
-db.security.addPermissionToRole('User', p)
-
 # May users view other user information? Comment these lines out
 # if you don't want them to
 p = db.security.getPermission('View', 'user')
 db.security.addPermissionToRole('User', p)
 
+# Users should be able to edit their own details. Note that this
+# permission is limited to only the situation where the Viewed or
+# Edited item is their own.
+def own_record(db, userid, itemid):
+    '''Determine whether the userid matches the item being accessed.'''
+    return userid == itemid
+p = db.security.addPermission(name='View', klass='user', check=own_record,
+    description="User is allowed to view their own user details")
+p = db.security.addPermission(name='Edit', klass='user', check=own_record,
+    description="User is allowed to edit their own user details")
+db.security.addPermissionToRole('User', p)
+
+#
+# ANONYMOUS USER PERMISSIONS
+#
+# Let anonymous users access the web interface. Note that almost all
+# trackers will need this Permission. The only situation where it's not
+# required is in a tracker that uses an HTTP Basic Authenticated front-end.
+p = db.security.getPermission('Web Access')
+db.security.addPermissionToRole('Anonymous', p)
+
+# Let anonymous users access the email interface (note that this implies
+# that they will be registered automatically, hence they will need the
+# "Create" user Prmission below)
+p = db.security.getPermission('Email Access')
+db.security.addPermissionToRole('Anonymous', p)
+
 # Assign the appropriate permissions to the anonymous user's Anonymous
 # Role. Choices here are:
-# - Allow anonymous users to register through the web
-p = db.security.getPermission('Web Registration')
+# - Allow anonymous users to register
+p = db.security.getPermission('Create', 'user')
 db.security.addPermissionToRole('Anonymous', p)
-# - Allow anonymous (new) users to register through the email gateway
-p = db.security.getPermission('Email Registration')
-db.security.addPermissionToRole('Anonymous', p)
-# - Allow anonymous users access to view issues (which implies being
-#   able to view all linked information too
+
+# Allow anonymous users access to view issues (and the related, linked
+# information)
 for cl in 'issue', 'file', 'msg', 'keyword', 'priority', 'status':
     p = db.security.getPermission('View', cl)
     db.security.addPermissionToRole('Anonymous', p)
-# - Allow anonymous users access to edit the "issue" class of data
-#   Note: this also grants access to create related information like
-#         files and messages etc that are linked to issues
-#p = db.security.getPermission('Edit', 'issue')
-#db.security.addPermissionToRole('Anonymous', p)
 
-# oh, g'wan, let anonymous access the web interface too
-p = db.security.getPermission('Web Access')
-db.security.addPermissionToRole('Anonymous', p)
+# [OPTIONAL]
+# Allow anonymous users access to create or edit "issue" items (and the
+# related file and message items)
+#for cl in 'issue', 'file', 'msg':
+#   p = db.security.getPermission('Create', cl)
+#   db.security.addPermissionToRole('Anonymous', p)
+#   p = db.security.getPermission('Edit', cl)
+#   db.security.addPermissionToRole('Anonymous', p)
 
 
 # vim: set filetype=python sts=4 sw=4 et si

Roundup Issue Tracker: http://roundup-tracker.org/