Code

Very close now. The cgi and mailgw now use the new security API. The two
authorrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Fri, 26 Jul 2002 08:27:00 +0000 (08:27 +0000)
committerrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Fri, 26 Jul 2002 08:27:00 +0000 (08:27 +0000)
templates have been migrated to that setup. Lots of unit tests. Still some
issue in the web form for editing Roles assigned to users.

git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@921 57a73879-2fb5-44c3-a270-3262357dd7e2

21 files changed:
COPYING.txt
doc/security.txt
doc/upgrading.txt
roundup/backends/back_anydbm.py
roundup/cgi_client.py
roundup/htmltemplate.py
roundup/mailgw.py
roundup/roundupdb.py
roundup/security.py
roundup/templates/classic/dbinit.py
roundup/templates/classic/html/user.item
roundup/templates/classic/instance_config.py
roundup/templates/extended/dbinit.py
roundup/templates/extended/html/user.item
roundup/templates/extended/instance_config.py
roundup/volatiledb.py
test/test_db.py
test/test_htmltemplate.py
test/test_init.py
test/test_mailgw.py
test/test_security.py

index 330c6f7085766db427c5a39273173bb8a267c909..db92559a13e33ddb961d4aed5e05707edf3f8a35 100644 (file)
@@ -1,7 +1,26 @@
 
 
-Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
 Copyright (c) 2002 eKit.com Inc (http://www.ekit.com/)
 
 Copyright (c) 2002 eKit.com Inc (http://www.ekit.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.
+
+Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
+
 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.
 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.
@@ -17,7 +36,6 @@ 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.
 
 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.
 
 The stylesheet included with this package has been copied from the Zope
 management interface and presumably belongs to Digital Creations.
 
index 2d3748a77cd188222108714d379994344504868e..66f9f7eb83f473729c8ce852fe0440f9fc4f9fed 100644 (file)
@@ -2,7 +2,7 @@
 Security Mechanisms
 ===================
 
 Security Mechanisms
 ===================
 
-:Version: $Revision: 1.12 $
+:Version: $Revision: 1.13 $
 
 Current situation
 =================
 
 Current situation
 =================
@@ -184,7 +184,7 @@ A security module defines::
                 base roles (for admin user).
             '''
 
                 base roles (for admin user).
             '''
 
-        def hasClassPermission(self, db, classname, permission, userid):
+        def hasPermission(self, db, classname, permission, userid):
             ''' Look through all the Roles, and hence Permissions, and see if
                 "permission" is there for the specified classname.
 
             ''' Look through all the Roles, and hence Permissions, and see if
                 "permission" is there for the specified classname.
 
@@ -241,26 +241,24 @@ The instance dbinit module then has in ``open()``::
     ei = db.security.addPermission(name="Edit", klass="issue",
                     description="User is allowed to edit issues")
     db.security.addPermissionToRole('User', ei)
     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", klass="issue",
-                    description="User may be assigned to issues")
-    db.security.addPermissionToRole('User', ei)
+    ai = db.security.addPermission(name="View", klass="issue",
+                    description="User is allowed to access issues")
+    db.security.addPermissionToRole('User', ai)
 
 In the dbinit ``init()``::
 
 
 In the dbinit ``init()``::
 
+    # create the two default users
     r = db.getclass('role').lookup('Admin')
     user.create(username="admin", password=Password(adminpw),
     r = db.getclass('role').lookup('Admin')
     user.create(username="admin", password=Password(adminpw),
-                address=instance_config.ADMIN_EMAIL, roles=[r])
-
-    # choose your anonymous user access permission here
-    #r = db.getclass('role').lookup('No Rego')
-    r = db.getclass('role').lookup('User')
-    user.create(username="anonymous", roles=[r])
+                address=instance_config.ADMIN_EMAIL, roles='Admin')
+    r = db.getclass('role').lookup('Anonymous')
+    user.create(username="anonymous", roles='Anonymous')
 
 
-Then in the code that matters, calls to ``hasClassPermission`` and
+Then in the code that matters, calls to ``hasPermission`` and
 ``hasNodePermission`` are made to determine if the user has permission
 to perform some action::
 
 ``hasNodePermission`` are made to determine if the user has permission
 to perform some action::
 
-    if db.security.hasClassPermission('issue', 'Edit', userid):
+    if db.security.hasPermission('issue', 'Edit', userid):
         # all ok
 
     if db.security.hasNodePermission('issue', nodeid, assignedto=userid):
         # all ok
 
     if db.security.hasNodePermission('issue', nodeid, assignedto=userid):
@@ -279,7 +277,7 @@ which has the form::
 where:
 
 - the permission attribute gives a comma-separated list of permission names.
 where:
 
 - the permission attribute gives a comma-separated list of permission names.
-  These are checked in turn using ``hasClassPermission`` and requires one to
+  These are checked in turn using ``hasPermission`` 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.
   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.
@@ -293,8 +291,7 @@ Implementation as shipped
 A set of Permissions are built in to the security module by default:
 
 - Edit (everything)
 A set of Permissions are built in to the security module by default:
 
 - Edit (everything)
-- Access (everything)
-- Assign (everything)
+- View (everything)
 
 The default interfaces define:
 
 
 The default interfaces define:
 
@@ -303,7 +300,7 @@ The default interfaces define:
 
 These are hooked into the default Roles:
 
 
 These are hooked into the default Roles:
 
-- Admin (Edit everything, Access everything, Assign everything)
+- Admin (Edit everything, View everything)
 - User ()
 - Anonymous (Web Registration, Email Registration)
 
 - User ()
 - Anonymous (Web Registration, Email Registration)
 
@@ -311,10 +308,16 @@ 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:
 
 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)
+- Edit issue, View issue (both)
+- Edit file, View file (both)
+- Edit msg, View msg (both)
+- Edit support, View support (extended only)
+
+and assign those Permissions to the "User" Role. New users are assigned the
+Roles defined in the config file as:
 
 
-and assign those Permissions to the "User" Role.
+- NEW_WEB_USER_ROLES
+- NEW_EMAIL_USER_ROLES
 
 
 Authentication of Users
 
 
 Authentication of Users
@@ -354,6 +357,7 @@ The CGI interface must be changed to:
   - implement htmltemplate tests on permissions
   - switch all code over from using config vars for permission checks to using
     permissions
   - implement htmltemplate tests on permissions
   - switch all code over from using config vars for permission checks to using
     permissions
+  - change all explicit admin user checks for Role checks
   - include config vars for initial Roles for anonymous web, new web and new
     email users
 
   - include config vars for initial Roles for anonymous web, new web and new
     email users
 
index e768d857f384a2d79b8460ee9e761722451391b3..2fb2159025c62c2931182ba06a66024af8f5b310 100644 (file)
@@ -18,6 +18,7 @@ TODO: mention that the dbinit needs the db.post_init() method call for
 reindexing
 TODO: dbinit now imports classes from selct_db
 TODO: select_db needs fixing to include Class, FileClass and IssueClass
 reindexing
 TODO: dbinit now imports classes from selct_db
 TODO: select_db needs fixing to include Class, FileClass and IssueClass
+TODO: migration of security settings
 
 
 Migrating from 0.4.1 to 0.4.2
 
 
 Migrating from 0.4.1 to 0.4.2
index 128453ff67575d67f11b1ded6dfd1912d8d3759a..3b85d788a954f1b3023b9b93a533d781d2e68007 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-#$Id: back_anydbm.py,v 1.53 2002-07-25 07:14:06 richard Exp $
+#$Id: back_anydbm.py,v 1.54 2002-07-26 08:26:59 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
 '''
 This module defines a backend that saves the hyperdatabase in a database
 chosen by anydbm. It is guaranteed to always be available in python
@@ -1333,7 +1333,7 @@ class Class(hyperdb.Class):
                 if node.has_key(self.db.RETIRED_FLAG):
                     continue
                 for key, value in requirements.items():
                 if node.has_key(self.db.RETIRED_FLAG):
                     continue
                 for key, value in requirements.items():
-                    if node[key] and node[key].lower() != value:
+                    if node[key] is None or node[key].lower() != value:
                         break
                 else:
                     l.append(nodeid)
                         break
                 else:
                     l.append(nodeid)
@@ -1776,6 +1776,14 @@ class IssueClass(Class, roundupdb.IssueClass):
 
 #
 #$Log: not supported by cvs2svn $
 
 #
 #$Log: not supported by cvs2svn $
+#Revision 1.53  2002/07/25 07:14:06  richard
+#Bugger it. Here's the current shape of the new security implementation.
+#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 :)
+#
 #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
 #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
index 9574d95690cf0df269752160c3af5fc5e4974c41..99a65f15c2693acc8f3bfbedab70ba7178bb81de 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: cgi_client.py,v 1.144 2002-07-25 07:14:05 richard Exp $
+# $Id: cgi_client.py,v 1.145 2002-07-26 08:26:59 richard Exp $
 
 __doc__ = """
 WWW request handler (also used in the stand-alone server).
 
 __doc__ = """
 WWW request handler (also used in the stand-alone server).
@@ -39,9 +39,13 @@ def initialiseSecurity(security):
         This function is directly invoked by security.Security.__init__()
         as a part of the Security object instantiation.
     '''
         This function is directly invoked by security.Security.__init__()
         as a part of the Security object instantiation.
     '''
-    newid = security.addPermission(name="Web Registration",
+    security.addPermission(name="Web Registration",
         description="User may register through the web")
         description="User may register through the web")
-    security.addPermissionToRole('Anonymous', newid)
+
+    # doing Role stuff through the web - make sure Admin can
+    p = security.addPermission(name="Web Roles",
+        description="User may manipulate user Roles through the web")
+    security.addPermissionToRole('Admin', p)
 
 class Client:
     '''
 
 class Client:
     '''
@@ -97,14 +101,14 @@ class Client:
                 err = _("sanity check: unknown user name `%s'")%self.user
             raise Unauthorised, errmsg
 
                 err = _("sanity check: unknown user name `%s'")%self.user
             raise Unauthorised, errmsg
 
-    def header(self, headers=None):
+    def header(self, headers=None, response=200):
         '''Put up the appropriate header.
         '''
         if headers is None:
             headers = {'Content-Type':'text/html'}
         if not headers.has_key('Content-Type'):
             headers['Content-Type'] = 'text/html'
         '''Put up the appropriate header.
         '''
         if headers is None:
             headers = {'Content-Type':'text/html'}
         if not headers.has_key('Content-Type'):
             headers['Content-Type'] = 'text/html'
-        self.request.send_response(200)
+        self.request.send_response(response)
         for entry in headers.items():
             self.request.send_header(*entry)
         self.request.end_headers()
         for entry in headers.items():
             self.request.send_header(*entry)
         self.request.end_headers()
@@ -178,22 +182,19 @@ function help_window(helpurl, width, height) {
         style = open(os.path.join(self.instance.TEMPLATES, 'style.css')).read()
 
         # figure who the user is
         style = open(os.path.join(self.instance.TEMPLATES, 'style.css')).read()
 
         # figure who the user is
-        user_name = self.user or ''
-        if user_name not in ('', 'anonymous'):
-            userid = self.db.user.lookup(self.user)
-        else:
-            userid = None
+        user_name = self.user
+        userid = self.db.user.lookup(user_name)
 
         # figure all the header links
         if hasattr(self.instance, 'HEADER_INDEX_LINKS'):
             links = []
             for name in self.instance.HEADER_INDEX_LINKS:
                 spec = getattr(self.instance, name + '_INDEX')
 
         # figure all the header links
         if hasattr(self.instance, 'HEADER_INDEX_LINKS'):
             links = []
             for name in self.instance.HEADER_INDEX_LINKS:
                 spec = getattr(self.instance, name + '_INDEX')
-                # skip if we need to fill in the logged-in user id there's
-                # no user logged in
+                # skip if we need to fill in the logged-in user id and
+                # we're anonymous
                 if (spec['FILTERSPEC'].has_key('assignedto') and
                         spec['FILTERSPEC']['assignedto'] in ('CURRENT USER',
                 if (spec['FILTERSPEC'].has_key('assignedto') and
                         spec['FILTERSPEC']['assignedto'] in ('CURRENT USER',
-                        None) and userid is None):
+                        None) and user_name == 'anonymous'):
                     continue
                 links.append(self.make_index_link(name))
         else:
                     continue
                 links.append(self.make_index_link(name))
         else:
@@ -203,59 +204,55 @@ function help_window(helpurl, width, height) {
                 _('Unassigned <a href="issue?assignedto=-1&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=-activity&:filter=status,assignedto&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">Issues</a>')
             ]
 
                 _('Unassigned <a href="issue?assignedto=-1&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=-activity&:filter=status,assignedto&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">Issues</a>')
             ]
 
-        if userid:
+        user_info = _('<a href="login">Login</a>')
+        add_links = ''
+        if user_name != 'anonymous':
             # add any personal queries to the menu
             try:
                 queries = self.db.getclass('query')
             except KeyError:
                 # no query class
             # add any personal queries to the menu
             try:
                 queries = self.db.getclass('query')
             except KeyError:
                 # no query class
-                queries = self.instance.dbinit.Class(self.db,
-                                                    "query",
-                                                    klass=hyperdb.String(),
-                                                    name=hyperdb.String(),
-                                                    url=hyperdb.String())
+                queries = self.instance.dbinit.Class(self.db, "query",
+                    klass=hyperdb.String(), name=hyperdb.String(),
+                    url=hyperdb.String())
                 queries.setkey('name')
                 queries.setkey('name')
-#queries.disableJournalling()
+                #queries.disableJournalling()
             try:
                 qids = self.db.getclass('user').get(userid, 'queries')
             except KeyError, e:
                 #self.db.getclass('user').addprop(queries=hyperdb.Multilink('query'))
                 qids = []
             for qid in qids:
             try:
                 qids = self.db.getclass('user').get(userid, 'queries')
             except KeyError, e:
                 #self.db.getclass('user').addprop(queries=hyperdb.Multilink('query'))
                 qids = []
             for qid in qids:
-                links.append('<a href=%s?%s>%s</a>' % (queries.get(qid, 'klass'),
-                                                       queries.get(qid, 'url'),
-                                                       queries.get(qid, 'name')))
-                
-        # if they're logged in, include links to their information, and the
-        # ability to add an issue
-        if user_name not in ('', 'anonymous'):
+                links.append('<a href=%s?%s>%s</a>'%(queries.get(qid, 'klass'),
+                    queries.get(qid, 'url'), queries.get(qid, 'name')))
+
+            # if they're logged in, include links to their information,
+            # and the ability to add an issue
             user_info = _('''
 <a href="user%(userid)s">My Details</a> | <a href="logout">Logout</a>
 ''')%locals()
 
             user_info = _('''
 <a href="user%(userid)s">My Details</a> | <a href="logout">Logout</a>
 ''')%locals()
 
-            # figure the "add class" links
-            if hasattr(self.instance, 'HEADER_ADD_LINKS'):
-                classes = self.instance.HEADER_ADD_LINKS
-            else:
-                classes = ['issue']
-            l = []
-            for class_name in classes:
-                cap_class = class_name.capitalize()
-                links.append(_('Add <a href="new%(class_name)s">'
-                    '%(cap_class)s</a>')%locals())
-
-            # if there's no config header link spec, force a user link here
-            if not hasattr(self.instance, 'HEADER_INDEX_LINKS'):
-                links.append(_('<a href="issue?assignedto=%(userid)s&status=-1,unread,chatting,open,pending&:filter=status,resolution,assignedto&:sort=-activity&:columns=id,activity,status,resolution,title,creator&:group=type&show_customization=1">My Issues</a>')%locals())
+
+        # figure the "add class" links
+        if hasattr(self.instance, 'HEADER_ADD_LINKS'):
+            classes = self.instance.HEADER_ADD_LINKS
         else:
         else:
-            user_info = _('<a href="login">Login</a>')
-            add_links = ''
+            classes = ['issue']
+        l = []
+        for class_name in classes:
+            # make sure the user has permission to add
+            if not self.db.security.hasPermission('Edit', userid, class_name):
+                continue
+            cap_class = class_name.capitalize()
+            links.append(_('Add <a href="new%(class_name)s">'
+                '%(cap_class)s</a>')%locals())
 
 
-        # if the user is admin, include admin links
+        # if the user can edit everything, include the links
         admin_links = ''
         admin_links = ''
-        if user_name == 'admin':
+        userid = self.db.user.lookup(user_name)
+        if self.db.security.hasPermission('Edit', userid):
             links.append(_('<a href="list_classes">Class List</a>'))
             links.append(_('<a href="list_classes">Class List</a>'))
-            links.append(_('<a href="user?:sort=username">User List</a>'))
+            links.append(_('<a href="user?:sort=username&:group=roles">User List</a>'))
             links.append(_('<a href="newuser">Add User</a>'))
 
         # add the search links
             links.append(_('<a href="newuser">Add User</a>'))
 
         # add the search links
@@ -265,6 +262,9 @@ function help_window(helpurl, width, height) {
             classes = ['issue']
         l = []
         for class_name in classes:
             classes = ['issue']
         l = []
         for class_name in classes:
+            # make sure the user has permission to view
+            if not self.db.security.hasPermission('View', userid, class_name):
+                continue
             cap_class = class_name.capitalize()
             links.append(_('Search <a href="search%(class_name)s">'
                 '%(cap_class)s</a>')%locals())
             cap_class = class_name.capitalize()
             links.append(_('Search <a href="search%(class_name)s">'
                 '%(cap_class)s</a>')%locals())
@@ -486,7 +486,8 @@ function help_window(helpurl, width, height) {
     # XXX deviates from spec - loses the '+' (that's a reserved character
     # in URLS
     def list(self, sort=None, group=None, filter=None, columns=None,
     # XXX deviates from spec - loses the '+' (that's a reserved character
     # in URLS
     def list(self, sort=None, group=None, filter=None, columns=None,
-            filterspec=None, show_customization=None, show_nodes=1, pagesize=None):
+            filterspec=None, show_customization=None, show_nodes=1,
+            pagesize=None):
         ''' call the template index with the args
 
             :sort    - sort by prop name, optionally preceeded with '-'
         ''' call the template index with the args
 
             :sort    - sort by prop name, optionally preceeded with '-'
@@ -569,7 +570,8 @@ function help_window(helpurl, width, height) {
         '''Display a basic edit page that allows simple editing of the
            nodes of the current class
         '''
         '''Display a basic edit page that allows simple editing of the
            nodes of the current class
         '''
-        if self.user != 'admin':
+        userid = self.db.user.lookup(self.user)
+        if not self.db.security.hasPermission('Edit', userid):
             raise Unauthorised
         w = self.write
         cn = self.classname
             raise Unauthorised
         w = self.write
         cn = self.classname
@@ -577,7 +579,6 @@ function help_window(helpurl, width, height) {
         idlessprops = cl.getprops(protected=0).keys()
         props = ['id'] + idlessprops
 
         idlessprops = cl.getprops(protected=0).keys()
         props = ['id'] + idlessprops
 
-
         # get the CSV module
         try:
             import csv
         # get the CSV module
         try:
             import csv
@@ -610,7 +611,13 @@ function help_window(helpurl, width, height) {
                 # extract the new values
                 d = {}
                 for name, value in zip(idlessprops, values):
                 # extract the new values
                 d = {}
                 for name, value in zip(idlessprops, values):
-                    d[name] = value.strip()
+                    value = value.strip()
+                    # only add the property if it has a value
+                    if value:
+                        # if it's a multilink, split it
+                        if isinstance(cl.properties[name], hyperdb.Multilink):
+                            value = value.split(':')
+                        d[name] = value
 
                 # perform the edit
                 if cl.hasnode(nodeid):
 
                 # perform the edit
                 if cl.hasnode(nodeid):
@@ -626,10 +633,12 @@ function help_window(helpurl, width, height) {
                     cl.retire(nodeid)
 
         w(_('''<p class="form-help">You may edit the contents of the
                     cl.retire(nodeid)
 
         w(_('''<p class="form-help">You may edit the contents of the
-        "%(classname)s" class using this form. The lines are full-featured
-        Comma-Separated-Value lines, so you may include commas and even
+        "%(classname)s" class using this form. Commas, newlines and double
+        quotes (") must be handled delicately. You may include commas and
         newlines by enclosing the values in double-quotes ("). Double
         quotes themselves must be quoted by doubling ("").</p>
         newlines by enclosing the values in double-quotes ("). Double
         quotes themselves must be quoted by doubling ("").</p>
+        <p class="form-help">Multilink properties have their multiple
+        values colon (":") separated (... ,"one:two:three", ...)</p>
         <p class="form-help">Remove entries by deleting their line. Add
         new entries by appending
         them to the table - put an X in the id column.</p>''')%{'classname':cn})
         <p class="form-help">Remove entries by deleting their line. Add
         new entries by appending
         them to the table - put an X in the id column.</p>''')%{'classname':cn})
@@ -647,7 +656,13 @@ function help_window(helpurl, width, height) {
         for nodeid in cl.list():
             l = []
             for name in props:
         for nodeid in cl.list():
             l = []
             for name in props:
-                l.append(cgi.escape(str(cl.get(nodeid, name))))
+                value = cl.get(nodeid, name)
+                if value is None:
+                    l.append('')
+                elif isinstance(value, type([])):
+                    l.append(cgi.escape(':'.join(map(str, value))))
+                else:
+                    l.append(cgi.escape(str(cl.get(nodeid, name))))
             w(p.join(l) + '\n')
 
         w(_('</textarea><br><input type="submit" value="Save Changes"></form>'))
             w(p.join(l) + '\n')
 
         w(_('</textarea><br><input type="submit" value="Save Changes"></form>'))
@@ -934,6 +949,9 @@ function help_window(helpurl, width, height) {
         node's id. The node id will be appended to the multilink.
         '''
         cn = self.classname
         node's id. The node id will be appended to the multilink.
         '''
         cn = self.classname
+        userid = self.db.user.lookup(self.user)
+        if not self.db.security.hasPermission('View', userid, cn):
+            raise Unauthorised
         cl = self.db.classes[cn]
         if self.form.has_key(':multilink'):
             link = self.form[':multilink'].value
         cl = self.db.classes[cn]
         if self.form.has_key(':multilink'):
             link = self.form[':multilink'].value
@@ -945,6 +963,9 @@ function help_window(helpurl, width, height) {
         # possibly perform a create
         keys = self.form.keys()
         if [i for i in keys if i[0] != ':']:
         # possibly perform a create
         keys = self.form.keys()
         if [i for i in keys if i[0] != ':']:
+            # no dice if you can't edit!
+            if not self.db.security.hasPermission('Edit', userid, cn):
+                raise Unauthorised
             props = {}
             try:
                 nid = self._createnode()
             props = {}
             try:
                 nid = self._createnode()
@@ -985,6 +1006,10 @@ function help_window(helpurl, width, height) {
 
             Don't do any of the message or file handling, just create the node.
         '''
 
             Don't do any of the message or file handling, just create the node.
         '''
+        userid = self.db.user.lookup(self.user)
+        if not self.db.security.hasPermission('Edit', userid, 'user'):
+            raise Unauthorised
+
         cn = self.classname
         cl = self.db.classes[cn]
 
         cn = self.classname
         cl = self.db.classes[cn]
 
@@ -1019,6 +1044,9 @@ function help_window(helpurl, width, height) {
         This form works very much the same way as newnode - it just has a
         file upload.
         '''
         This form works very much the same way as newnode - it just has a
         file upload.
         '''
+        userid = self.db.user.lookup(self.user)
+        if not self.db.security.hasPermission('Edit', userid, 'file'):
+            raise Unauthorised
         cn = self.classname
         cl = self.db.classes[cn]
         props = parsePropsFromForm(self.db, cl, self.form)
         cn = self.classname
         cl = self.db.classes[cn]
         props = parsePropsFromForm(self.db, cl, self.form)
@@ -1064,15 +1092,16 @@ function help_window(helpurl, width, height) {
         '''Display a user page for editing. Make sure the user is allowed
             to edit this node, and also check for password changes.
         '''
         '''Display a user page for editing. Make sure the user is allowed
             to edit this node, and also check for password changes.
         '''
-        if self.user == 'anonymous':
-            raise Unauthorised
-
         user = self.db.user
 
         # get the username of the node being edited
         node_user = user.get(self.nodeid, 'username')
 
         user = self.db.user
 
         # get the username of the node being edited
         node_user = user.get(self.nodeid, 'username')
 
-        if self.user not in ('admin', node_user):
+        # ok, so we need to be able to edit everything, or be this node's
+        # user
+        userid = self.db.user.lookup(self.user)
+        if (not self.db.security.hasPermission('Edit', userid)
+                and self.user != node_user):
             raise Unauthorised
         
         #
             raise Unauthorised
         
         #
@@ -1129,28 +1158,33 @@ function help_window(helpurl, width, height) {
         self.header(headers={'Content-Type': mime_type})
         self.write(cl.get(nodeid, 'content'))
 
         self.header(headers={'Content-Type': mime_type})
         self.write(cl.get(nodeid, 'content'))
 
+    def permission(self):
+        '''
+        '''
+
     def classes(self, message=None):
         ''' display a list of all the classes in the database
         '''
     def classes(self, message=None):
         ''' display a list of all the classes in the database
         '''
-        if self.user == 'admin':
-            self.pagehead(_('Table of classes'), message)
-            classnames = self.db.classes.keys()
-            classnames.sort()
-            self.write('<table border=0 cellspacing=0 cellpadding=2>\n')
-            for cn in classnames:
-                cl = self.db.getclass(cn)
-                self.write('<tr class="list-header"><th colspan=2 align=left>'
-                    '<a href="%s">%s</a></th></tr>'%(cn, cn.capitalize()))
-                for key, value in cl.properties.items():
-                    if value is None: value = ''
-                    else: value = str(value)
-                    self.write('<tr><th align=left>%s</th><td>%s</td></tr>'%(
-                        key, cgi.escape(value)))
-            self.write('</table>')
-            self.pagefoot()
-        else:
+        userid = self.db.user.lookup(self.user)
+        if not self.db.security.hasPermission('Edit', userid):
             raise Unauthorised
 
             raise Unauthorised
 
+        self.pagehead(_('Table of classes'), message)
+        classnames = self.db.classes.keys()
+        classnames.sort()
+        self.write('<table border=0 cellspacing=0 cellpadding=2>\n')
+        for cn in classnames:
+            cl = self.db.getclass(cn)
+            self.write('<tr class="list-header"><th colspan=2 align=left>'
+                '<a href="%s">%s</a></th></tr>'%(cn, cn.capitalize()))
+            for key, value in cl.properties.items():
+                if value is None: value = ''
+                else: value = str(value)
+                self.write('<tr><th align=left>%s</th><td>%s</td></tr>'%(
+                    key, cgi.escape(value)))
+        self.write('</table>')
+        self.pagefoot()
+
     def login(self, message=None, newuser_form=None, action='index'):
         '''Display a login page.
         '''
     def login(self, message=None, newuser_form=None, action='index'):
         '''Display a login page.
         '''
@@ -1168,7 +1202,8 @@ function help_window(helpurl, width, height) {
     <td><input type="submit" value="Log In"></td></tr>
 </form>
 ''')%locals())
     <td><input type="submit" value="Log In"></td></tr>
 </form>
 ''')%locals())
-        if self.user is None and self.instance.ANONYMOUS_REGISTER == 'deny':
+        userid = self.db.user.lookup(self.user)
+        if not self.db.security.hasPermission('Web Registration', userid):
             self.write('</table>')
             self.pagefoot()
             return
             self.write('</table>')
             self.pagefoot()
             return
@@ -1251,6 +1286,11 @@ function help_window(helpurl, width, height) {
 
         return 1 on successful login
         '''
 
         return 1 on successful login
         '''
+        # make sure we're allowed to register
+        userid = self.db.user.lookup(self.user)
+        if not self.db.security.hasPermission('Web Registration', userid):
+            raise Unauthorised
+
         # re-open the database as "admin"
         self.opendb('admin')
 
         # re-open the database as "admin"
         self.opendb('admin')
 
@@ -1258,7 +1298,9 @@ function help_window(helpurl, width, height) {
         cl = self.db.user
         try:
             props = parsePropsFromForm(self.db, cl, self.form)
         cl = self.db.user
         try:
             props = parsePropsFromForm(self.db, cl, self.form)
+            props['roles'] = self.instance.NEW_WEB_USER_ROLES
             uid = cl.create(**props)
             uid = cl.create(**props)
+            self.db.commit()
         except ValueError, message:
             action = self.form['__destination_url'].value
             self.login(message, action=action)
         except ValueError, message:
             action = self.form['__destination_url'].value
             self.login(message, action=action)
@@ -1283,8 +1325,6 @@ function help_window(helpurl, width, height) {
           else:
             session = session[:-1]
 
           else:
             session = session[:-1]
 
-        print 'session set to', `session`
-
         # insert the session in the sessiondb
         sessions = self.db.getclass('__sessions')
         self.session = sessions.create(sessid=session, user=user,
         # insert the session in the sessiondb
         sessions = self.db.getclass('__sessions')
         self.session = sessions.create(sessid=session, user=user,
@@ -1303,12 +1343,13 @@ function help_window(helpurl, width, height) {
             session, expire, path)})
 
     def make_user_anonymous(self):
             session, expire, path)})
 
     def make_user_anonymous(self):
-        # make us anonymous if we can
-        try:
-            self.db.user.lookup('anonymous')
-            self.user = 'anonymous'
-        except KeyError:
-            self.user = None
+        ''' Make use anonymous
+
+            This method used to handle non-existence of the 'anonymous'
+            user, but that user is mandatory now.
+        '''
+        self.db.user.lookup('anonymous')
+        self.user = 'anonymous'
 
     def logout(self, message=None):
         self.make_user_anonymous()
 
     def logout(self, message=None):
         self.make_user_anonymous()
@@ -1341,6 +1382,19 @@ function help_window(helpurl, width, height) {
             sessions.disableJournalling()
 
     def main(self):
             sessions.disableJournalling()
 
     def main(self):
+        ''' Wrap the request and handle unauthorised requests
+        '''
+        self.desired_action = None
+        try:
+            self.main_action()
+        except Unauthorised:
+            self.header(response=403)
+            if self.desired_action is None or self.desired_action == 'login':
+                self.login()             # go to the index after login
+            else:
+                self.login(action=self.desired_action)
+
+    def main_action(self):
         '''Wrap the database accesses so we can close the database cleanly
         '''
         # determine the uid to use
         '''Wrap the database accesses so we can close the database cleanly
         '''
         # determine the uid to use
@@ -1378,6 +1432,12 @@ function help_window(helpurl, width, height) {
                 self.db.commit()
                 user = sessions.get(sessid, 'user')
 
                 self.db.commit()
                 user = sessions.get(sessid, 'user')
 
+        # sanity check on the user still being valid
+        try:
+            self.db.user.lookup(user)
+        except KeyError:
+            user = 'anonymous'
+
         # make sure the anonymous user is valid if we're using it
         if user == 'anonymous':
             self.make_user_anonymous()
         # make sure the anonymous user is valid if we're using it
         if user == 'anonymous':
             self.make_user_anonymous()
@@ -1392,6 +1452,7 @@ function help_window(helpurl, width, height) {
             action = 'index'
         else:
             action = path[0]
             action = 'index'
         else:
             action = path[0]
+        self.desired_action = action
 
         # Everthing ignores path[1:]
         #  - The file download link generator actually relies on this - it
 
         # Everthing ignores path[1:]
         #  - The file download link generator actually relies on this - it
@@ -1412,14 +1473,6 @@ function help_window(helpurl, width, height) {
 
         # allow anonymous people to register
         if action == 'newuser_action':
 
         # allow anonymous people to register
         if action == 'newuser_action':
-            # if we don't have a login and anonymous people aren't allowed to
-            # register, then spit up the login form
-            if self.instance.ANONYMOUS_REGISTER == 'deny' and self.user is None:
-                if action == 'login':
-                    self.login()         # go to the index after login
-                else:
-                    self.login(action=action)
-                return
             # try to add the user
             if not self.newuser_action():
                 return
             # try to add the user
             if not self.newuser_action():
                 return
@@ -1428,14 +1481,6 @@ function help_window(helpurl, width, height) {
             if not action:
                 action = 'index'
 
             if not action:
                 action = 'index'
 
-        # no login or registration, make sure totally anonymous access is OK
-        elif self.instance.ANONYMOUS_ACCESS == 'deny' and self.user is None:
-            if action == 'login':
-                self.login()             # go to the index after login
-            else:
-                self.login(action=action)
-            return
-
         # re-open the database for real, using the user
         self.opendb(self.user)
 
         # re-open the database for real, using the user
         self.opendb(self.user)
 
@@ -1623,6 +1668,14 @@ def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
 
 #
 # $Log: not supported by cvs2svn $
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.144  2002/07/25 07:14:05  richard
+# Bugger it. Here's the current shape of the new security implementation.
+# 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 :)
+#
 # Revision 1.143  2002/07/20 19:29:10  gmcm
 # Fixes/improvements to the search form & saved queries.
 #
 # Revision 1.143  2002/07/20 19:29:10  gmcm
 # Fixes/improvements to the search form & saved queries.
 #
index 2558f3750f42edae51cc119c319903534433fbc8..56074fa14c3849e7c903139d92b77aadc38eb696 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: htmltemplate.py,v 1.104 2002-07-25 07:14:05 richard Exp $
+# $Id: htmltemplate.py,v 1.105 2002-07-26 08:26:59 richard Exp $
 
 __doc__ = """
 Template engine.
 
 __doc__ = """
 Template engine.
@@ -881,7 +881,7 @@ class TemplateFunctions:
         if d.has_key('permission'):
             l.remove(('permission', d['permission']))
             for value in d['permission'].split(','):
         if d.has_key('permission'):
             l.remove(('permission', d['permission']))
             for value in d['permission'].split(','):
-                if security.hasClassPermission(self.classname, value, userid):
+                if security.hasPermission(value, userid, self.classname):
                     # just passing the permission is OK
                     return self.execute_template(ok)
 
                     # just passing the permission is OK
                     return self.execute_template(ok)
 
@@ -1049,7 +1049,8 @@ class IndexTemplate(TemplateFunctions):
                         old_group = this_group
 
                 # display this node's row
                         old_group = this_group
 
                 # display this node's row
-                w(replace.execute_template(template))
+                self.nodeid = nodeid
+                w(self.execute_template(template))
                 if matches:
                     self.node_matches(matches[nodeid], len(columns))
                 self.nodeid = None
                 if matches:
                     self.node_matches(matches[nodeid], len(columns))
                 self.nodeid = None
@@ -1417,6 +1418,14 @@ class NewItemTemplate(ItemTemplate):
 
 #
 # $Log: not supported by cvs2svn $
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.104  2002/07/25 07:14:05  richard
+# Bugger it. Here's the current shape of the new security implementation.
+# 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 :)
+#
 # Revision 1.103  2002/07/20 19:29:10  gmcm
 # Fixes/improvements to the search form & saved queries.
 #
 # Revision 1.103  2002/07/20 19:29:10  gmcm
 # Fixes/improvements to the search form & saved queries.
 #
index 88868fd39cfa789d5333183e0de60eed58013f59..a1c21ac82da78bfa4ec7dc39131ee57f315baea9 100644 (file)
@@ -73,7 +73,7 @@ are calling the create() method to create a new node). If an auditor raises
 an exception, the original message is bounced back to the sender with the
 explanatory message given in the exception. 
 
 an exception, the original message is bounced back to the sender with the
 explanatory message given in the exception. 
 
-$Id: mailgw.py,v 1.78 2002-07-25 07:14:06 richard Exp $
+$Id: mailgw.py,v 1.79 2002-07-26 08:26:59 richard Exp $
 '''
 
 
 '''
 
 
@@ -93,7 +93,7 @@ class MailUsageError(ValueError):
 class MailUsageHelp(Exception):
     pass
 
 class MailUsageHelp(Exception):
     pass
 
-class UnAuthorized(Exception):
+class Unauthorized(Exception):
     """ Access denied """
 
 def initialiseSecurity(security):
     """ Access denied """
 
 def initialiseSecurity(security):
@@ -104,7 +104,6 @@ def initialiseSecurity(security):
     '''
     newid = security.addPermission(name="Email Registration",
         description="Anonymous may register through e-mail")
     '''
     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
 
 class Message(mimetools.Message):
     ''' subclass mimetools.Message so we can retrieve the parts of the
@@ -182,7 +181,7 @@ class MailGW:
                 m.append('\n\nMail Gateway Help\n=================')
                 m.append(fulldoc)
                 m = self.bounce_message(message, sendto, m)
                 m.append('\n\nMail Gateway Help\n=================')
                 m.append(fulldoc)
                 m = self.bounce_message(message, sendto, m)
-            except UnAuthorized, value:
+            except Unauthorized, value:
                 # just inform the user that he is not authorized
                 sendto = [sendto[0][1]]
                 m = ['']
                 # just inform the user that he is not authorized
                 sendto = [sendto[0][1]]
                 m = ['']
@@ -522,19 +521,16 @@ Subject was: "%s"
         # handle the users
         #
 
         # handle the users
         #
 
-        # Don't create users if ANONYMOUS_REGISTER_MAIL is denied
-        # ... fall back on ANONYMOUS_REGISTER if the other doesn't exist
+        # Don't create users if anonymous isn't allowed to register
         create = 1
         create = 1
-        if hasattr(self.instance, 'ANONYMOUS_REGISTER_MAIL'):
-            if self.instance.ANONYMOUS_REGISTER_MAIL == 'deny':
-                create = 0
-        elif self.instance.ANONYMOUS_REGISTER == 'deny':
+        anonid = self.db.user.lookup('anonymous')
+        if not self.db.security.hasPermission('Email Registration', anonid):
             create = 0
 
             create = 0
 
-        author = self.db.uidFromAddress(message.getaddrlist('from')[0],
+        author = uidFromAddress(self.db, message.getaddrlist('from')[0],
             create=create)
         if not author:
             create=create)
         if not author:
-            raise UnAuthorized, '''
+            raise Unauthorized, '''
 You are not a registered user.
 
 Unknown address: %s
 You are not a registered user.
 
 Unknown address: %s
@@ -561,7 +557,7 @@ Unknown address: %s
 
             # look up the recipient - create if necessary (and we're
             # allowed to)
 
             # look up the recipient - create if necessary (and we're
             # allowed to)
-            recipient = self.db.uidFromAddress(recipient, create)
+            recipient = uidFromAddress(self.db, recipient, create)
 
             # if all's well, add the recipient to the list
             if recipient:
 
             # if all's well, add the recipient to the list
             if recipient:
@@ -731,6 +727,53 @@ There was a problem with the message you sent:
 
         return nodeid
 
 
         return nodeid
 
+def extractUserFromList(userClass, users):
+    '''Given a list of users, try to extract the first non-anonymous user
+       and return that user, otherwise return None
+    '''
+    if len(users) > 1:
+        for user in users:
+            # make sure we don't match the anonymous or admin user
+            if userClass.get(user, 'username') in ('admin', 'anonymous'):
+                continue
+            # first valid match will do
+            return user
+        # well, I guess we have no choice
+        return user[0]
+    elif users:
+        return users[0]
+    return None
+
+def uidFromAddress(db, address, create=1):
+    ''' address is from the rfc822 module, and therefore is (name, addr)
+
+        user is created if they don't exist in the db already
+    '''
+    (realname, address) = address
+
+    # try a straight match of the address
+    user = extractUserFromList(db.user, db.user.stringFind(address=address))
+    if user is not None: return user
+
+    # try the user alternate addresses if possible
+    props = db.user.getprops()
+    if props.has_key('alternate_addresses'):
+        users = db.user.filter(None, {'alternate_addresses': address},
+            [], [])
+        user = extractUserFromList(db.user, users)
+        if user is not None: return user
+
+    # try to match the username to the address (for local
+    # submissions where the address is empty)
+    user = extractUserFromList(db.user, db.user.stringFind(username=address))
+
+    # couldn't match address or username, so create a new user
+    if create:
+        return db.user.create(username=address, address=address,
+            realname=realname, roles=db.config.NEW_EMAIL_USER_ROLES)
+    else:
+        return 0
+
 def parseContent(content, keep_citations, keep_body,
         blank_line=re.compile(r'[\r\n]+\s*[\r\n]+'),
         eol=re.compile(r'[\r\n]+'), 
 def parseContent(content, keep_citations, keep_body,
         blank_line=re.compile(r'[\r\n]+\s*[\r\n]+'),
         eol=re.compile(r'[\r\n]+'), 
@@ -800,6 +843,14 @@ def parseContent(content, keep_citations, keep_body,
 
 #
 # $Log: not supported by cvs2svn $
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.78  2002/07/25 07:14:06  richard
+# Bugger it. Here's the current shape of the new security implementation.
+# 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 :)
+#
 # 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.
 # 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.
index 57e678da6ed9cbc74b4a2d7b412913e93d0c21e8..1b2a385551b31cfaefac752acdca7b1e3bcb8456 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: roundupdb.py,v 1.62 2002-07-14 02:05:53 richard Exp $
+# $Id: roundupdb.py,v 1.63 2002-07-26 08:26:59 richard Exp $
 
 __doc__ = """
 Extending hyperdb with types specific to issue-tracking.
 
 __doc__ = """
 Extending hyperdb with types specific to issue-tracking.
@@ -36,62 +36,12 @@ import hyperdb
 # this var must contain a file to write the mail to
 SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '')
 
 # this var must contain a file to write the mail to
 SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '')
 
-
-def extractUserFromList(userClass, users):
-    '''Given a list of users, try to extract the first non-anonymous user
-       and return that user, otherwise return None
-    '''
-    if len(users) > 1:
-        # make sure we don't match the anonymous or admin user
-        for user in users:
-            if user == '1': continue
-            if userClass.get(user, 'username') == 'anonymous': continue
-            # first valid match will do
-            return user
-        # well, I guess we have no choice
-        return user[0]
-    elif users:
-        return users[0]
-    return None
-
 class Database:
     def getuid(self):
         """Return the id of the "user" node associated with the user
         that owns this connection to the hyperdatabase."""
         return self.user.lookup(self.journaltag)
 
 class Database:
     def getuid(self):
         """Return the id of the "user" node associated with the user
         that owns this connection to the hyperdatabase."""
         return self.user.lookup(self.journaltag)
 
-    def uidFromAddress(self, address, create=1):
-        ''' address is from the rfc822 module, and therefore is (name, addr)
-
-            user is created if they don't exist in the db already
-        '''
-        (realname, address) = address
-
-        # try a straight match of the address
-        user = extractUserFromList(self.user,
-            self.user.stringFind(address=address))
-        if user is not None: return user
-
-        # try the user alternate addresses if possible
-        props = self.user.getprops()
-        if props.has_key('alternate_addresses'):
-            users = self.user.filter(None, {'alternate_addresses': address},
-                [], [])
-            user = extractUserFromList(self.user, users)
-            if user is not None: return user
-
-        # try to match the username to the address (for local
-        # submissions where the address is empty)
-        user = extractUserFromList(self.user,
-            self.user.stringFind(username=address))
-
-        # couldn't match address or username, so create a new user
-        if create:
-            return self.user.create(username=address, address=address,
-                realname=realname)
-        else:
-            return 0
-
 class MessageSendError(RuntimeError):
     pass
 
 class MessageSendError(RuntimeError):
     pass
 
@@ -476,6 +426,9 @@ class IssueClass:
 
 #
 # $Log: not supported by cvs2svn $
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.62  2002/07/14 02:05:53  richard
+# . all storage-specific code (ie. backend) is now implemented by the backends
+#
 # Revision 1.61  2002/07/09 04:19:09  richard
 # Added reindex command to roundup-admin.
 # Fixed reindex on first access.
 # Revision 1.61  2002/07/09 04:19:09  richard
 # Added reindex command to roundup-admin.
 # Fixed reindex on first access.
index 7475ca62eb9067272f1088094522ee4cee47a452..49e447795014c40af6a5b2c521dca2fa5acfd127 100644 (file)
@@ -63,31 +63,49 @@ class Security:
         ee = self.addPermission(name="Edit",
             description="User may edit everthing")
         self.addPermissionToRole('Admin', ee)
         ee = self.addPermission(name="Edit",
             description="User may edit everthing")
         self.addPermissionToRole('Admin', ee)
-        ae = self.addPermission(name="Access",
+        ae = self.addPermission(name="View",
             description="User may access everything")
         self.addPermissionToRole('Admin', ae)
             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")
         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")
         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)
 
 
         # 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):
+    def getPermission(self, permission, classname=None):
+        ''' Find the Permission matching the name and for the class, if the
+            classname is specified.
+
+            Raise ValueError if there is no exact match.
+        '''
+        perm = self.db.permission
+        for permissionid in perm.stringFind(name=permission):
+            klass = perm.get(permissionid, 'klass')
+            if classname is not None and classname == klass:
+                return permissionid
+            elif not classname and not klass:
+                return permissionid
+        if not classname:
+            raise ValueError, 'No permission "%s" defined'%permission
+        raise ValueError, 'No permission "%s" defined for "%s"'%(permission,
+            classname)
+
+    def hasPermission(self, permission, userid, classname=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')
         ''' 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:
+        if roles is None:
+            return 0
+        for rolename in roles.split(','):
+            if not rolename:
+                continue
+            roleid = self.db.role.lookup(rolename)
             for permissionid in self.db.role.get(roleid, 'permissions'):
                 if self.db.permission.get(permissionid, 'name') != permission:
                     continue
             for permissionid in self.db.role.get(roleid, 'permissions'):
                 if self.db.permission.get(permissionid, 'name') != permission:
                     continue
index 1edc2dd1bf73fdfbe68a4026caf1d330692a1703..7c2153ddf5e5c2ada0db13b21c82fe4cdee4ac09 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: dbinit.py,v 1.20 2002-07-17 12:39:10 gmcm Exp $
+# $Id: dbinit.py,v 1.21 2002-07-26 08:26:59 richard Exp $
 
 import os
 
 
 import os
 
@@ -56,11 +56,13 @@ def open(name=None):
                     url=String())
     query.setkey("name")
     
                     url=String())
     query.setkey("name")
     
+    # Note: roles is a comma-separated string of Role names
     user = Class(db, "user", 
                     username=String(),   password=Password(),
                     address=String(),    realname=String(), 
                     phone=String(),      organisation=String(),
     user = Class(db, "user", 
                     username=String(),   password=Password(),
                     address=String(),    realname=String(), 
                     phone=String(),      organisation=String(),
-                    alternate_addresses=String(), queries=Multilink("query"))
+                    alternate_addresses=String(),
+                    queries=Multilink('query'), roles=String())
     user.setkey("username")
 
     # FileClass automatically gets these properties:
     user.setkey("username")
 
     # FileClass automatically gets these properties:
@@ -86,6 +88,43 @@ def open(name=None):
                     assignedto=Link("user"), topic=Multilink("keyword"),
                     priority=Link("priority"), status=Link("status"))
 
                     assignedto=Link("user"), topic=Multilink("keyword"),
                     priority=Link("priority"), status=Link("status"))
 
+    #
+    # SECURITY SETTINGS
+    #
+    # new permissions for this schema
+    for cl in 'issue', 'file', 'msg':
+        db.security.addPermission(name="Edit", klass=cl,
+            description="User is allowed to edit "+cl)
+        db.security.addPermission(name="View", klass=cl,
+            description="User is allowed to access "+cl)
+
+    # 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')
+    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 the "issue" class of data
+    #   Note: this also grants access to related information like files,
+    #         messages, statuses etc that are linked to issues
+    #p = db.security.getPermission('View', 'issue')
+    #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)
+
+    # Assign the access and edit permissions for issue, file and message
+    # to regular users now
+    for cl in 'issue', 'file', 'msg':
+        p = db.security.getPermission('View', cl)
+        db.security.addPermissionToRole('User', p)
+        p = db.security.getPermission('Edit', cl)
+        db.security.addPermissionToRole('User', p)
+
     import detectors
     detectors.init(db)
 
     import detectors
     detectors.init(db)
 
@@ -107,6 +146,9 @@ def init(adminpw):
     db = open("admin")
     db.clear()
 
     db = open("admin")
     db.clear()
 
+    #
+    # INITIAL PRIORITY AND STATUS VALUES
+    #
     pri = db.getclass('priority')
     pri.create(name="critical", order="1")
     pri.create(name="urgent", order="2")
     pri = db.getclass('priority')
     pri.create(name="critical", order="1")
     pri.create(name="urgent", order="2")
@@ -124,13 +166,19 @@ def init(adminpw):
     stat.create(name="done-cbb", order="7")
     stat.create(name="resolved", order="8")
 
     stat.create(name="done-cbb", order="7")
     stat.create(name="resolved", order="8")
 
+    # create the two default users
     user = db.getclass('user')
     user = db.getclass('user')
-    user.create(username="admin", password=adminpw, 
-                                  address=instance_config.ADMIN_EMAIL)
+    user.create(username="admin", password=adminpw,
+        address=instance_config.ADMIN_EMAIL, roles='Admin')
+    user.create(username="anonymous", roles='Anonymous')
+
     db.commit()
 
 #
 # $Log: not supported by cvs2svn $
     db.commit()
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.20  2002/07/17 12:39:10  gmcm
+# Saving, running & editing queries.
+#
 # Revision 1.19  2002/07/14 02:05:54  richard
 # . all storage-specific code (ie. backend) is now implemented by the backends
 #
 # Revision 1.19  2002/07/14 02:05:54  richard
 # . all storage-specific code (ie. backend) is now implemented by the backends
 #
index e4a4eacfbf2fbd53d94e51ba7e825343fb6a3af1..f4ab59447bef9a4aff2e53c0f404fdeb0aec0c70 100644 (file)
@@ -1,4 +1,4 @@
-<!-- $Id: user.item,v 1.4 2002-07-17 12:39:11 gmcm Exp $-->
+<!-- $Id: user.item,v 1.5 2002-07-26 08:27:00 richard Exp $-->
 <table border=0 cellspacing=0 cellpadding=2>
 
 <tr class="strong-header">
 <table border=0 cellspacing=0 cellpadding=2>
 
 <tr class="strong-header">
     <td width=1% nowrap align=right><span class="form-label">Login Password</span></td>
     <td class="form-text"><display call="field('password', size=10)"></td>
 </tr>
     <td width=1% nowrap align=right><span class="form-label">Login Password</span></td>
     <td class="form-text"><display call="field('password', size=10)"></td>
 </tr>
+<require permission="Web Roles">
+ <tr bgcolor="ffffea">
+    <td width=1% nowrap align=right><span class="form-label">Roles</span></td>
+    <td class="form-text"><display call="field('roles', size=10)"></td>
+ </tr>
+</require>
 <tr  bgcolor="ffffea">
     <td width=1% nowrap align=right><span class="form-label">Phone</span></td>
     <td class="form-text"><display call="field('phone', size=40)"></td>
 <tr  bgcolor="ffffea">
     <td width=1% nowrap align=right><span class="form-label">Phone</span></td>
     <td class="form-text"><display call="field('phone', size=40)"></td>
index cd06f426ff99d3268d29a074304bd17d5a790442..7b3e9548071b787e72e8a4ee5b738a762fdcd93a 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: instance_config.py,v 1.18 2002-05-25 07:16:25 rochecompaan Exp $
+# $Id: instance_config.py,v 1.19 2002-07-26 08:26:59 richard Exp $
 
 MAIL_DOMAIN=MAILHOST=HTTP_HOST=None
 HTTP_PORT=0
 
 MAIL_DOMAIN=MAILHOST=HTTP_HOST=None
 HTTP_PORT=0
@@ -68,14 +68,13 @@ LOG = os.path.join(INSTANCE_HOME, 'roundup.log')
 # Where to place the web filtering HTML on the index page
 FILTER_POSITION = 'bottom'          # one of 'top', 'bottom', 'top and bottom'
 
 # Where to place the web filtering HTML on the index page
 FILTER_POSITION = 'bottom'          # one of 'top', 'bottom', 'top and bottom'
 
-# Deny or allow anonymous access to the web interface
-ANONYMOUS_ACCESS = 'deny'           # either 'deny' or 'allow'
-
-# Deny or allow anonymous users to register through the web interface
-ANONYMOUS_REGISTER = 'deny'         # either 'deny' or 'allow'
-
-# Deny or allow anonymous users to register through the mail interface
-ANONYMOUS_REGISTER_MAIL = 'deny'    # either 'deny' or 'allow'
+# 
+# SECURITY DEFINITIONS
+#
+# define the Roles that a user gets when they register with the tracker
+# these are a comma-separated string of role names (e.g. 'Admin,User')
+NEW_WEB_USER_ROLES = 'User'
+NEW_EMAIL_USER_ROLES = 'User'
 
 # Send nosy messages to the author of the message
 MESSAGES_TO_AUTHOR = 'no'           # either 'yes' or 'no'
 
 # Send nosy messages to the author of the message
 MESSAGES_TO_AUTHOR = 'no'           # either 'yes' or 'no'
@@ -178,6 +177,9 @@ SUPPORT_FILTER = {
 
 #
 # $Log: not supported by cvs2svn $
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.18  2002/05/25 07:16:25  rochecompaan
+# Merged search_indexing-branch with HEAD
+#
 # Revision 1.17  2002/05/22 00:32:33  richard
 #  . changed the default message list in issues to display the message body
 #  . made backends.__init__ be more specific about which ImportErrors it really
 # Revision 1.17  2002/05/22 00:32:33  richard
 #  . changed the default message list in issues to display the message body
 #  . made backends.__init__ be more specific about which ImportErrors it really
index 0ada216bde71f11f1f8e46f6ec901c0630149145..6149c4dc5d8d9bf26408b4eb568703357dd85134 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: dbinit.py,v 1.23 2002-07-14 02:05:54 richard Exp $
+# $Id: dbinit.py,v 1.24 2002-07-26 08:27:00 richard Exp $
 
 import os
 
 
 import os
 
@@ -24,7 +24,6 @@ from select_db import Database, Class, FileClass, IssueClass
 
 def open(name=None):
     ''' as from the roundupdb method openDB 
 
 def open(name=None):
     ''' as from the roundupdb method openDB 
     ''' 
     from roundup.hyperdb import String, Password, Date, Link, Multilink
 
     ''' 
     from roundup.hyperdb import String, Password, Date, Link, Multilink
 
@@ -56,7 +55,8 @@ def open(name=None):
                     username=String(),   password=Password(),
                     address=String(),    realname=String(), 
                     phone=String(),      organisation=String(),
                     username=String(),   password=Password(),
                     address=String(),    realname=String(), 
                     phone=String(),      organisation=String(),
-                    alternate_addresses=String())
+                    alternate_addresses=String(),
+                    queries=Multilink('query'), roles=String())
     user.setkey("username")
 
     # FileClass automatically gets these properties:
     user.setkey("username")
 
     # FileClass automatically gets these properties:
@@ -112,6 +112,43 @@ def open(name=None):
                     platform=Multilink("platform"), version=String(),
                     targetversion=String(), supportcall=Multilink("support"))
 
                     platform=Multilink("platform"), version=String(),
                     targetversion=String(), supportcall=Multilink("support"))
 
+    #
+    # SECURITY SETTINGS
+    #
+    # new permissions for this schema
+    for cl in 'issue', 'support', 'file', 'msg':
+        db.security.addPermission(name="Edit", klass=cl,
+            description="User is allowed to edit "+cl)
+        db.security.addPermission(name="View", klass=cl,
+            description="User is allowed to access "+cl)
+
+    # 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')
+    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 the "issue" class of data
+    #   Note: this also grants access to related information like files,
+    #         messages, statuses etc that are linked to issues
+    #p = db.security.getPermission('View', 'issue')
+    #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)
+
+    # Assign the access and edit permissions for issue, file and message
+    # to regular users now
+    for cl in 'issue', 'support', 'file', 'msg':
+        p = db.security.getPermission('View', cl)
+        db.security.addPermissionToRole('User', p)
+        p = db.security.getPermission('Edit', cl)
+        db.security.addPermissionToRole('User', p)
+
     import detectors
     detectors.init(db)
 
     import detectors
     detectors.init(db)
 
@@ -173,12 +210,16 @@ def init(adminpw):
 
     user = db.getclass('user')
     user.create(username="admin", password=adminpw, 
 
     user = db.getclass('user')
     user.create(username="admin", password=adminpw, 
-                                  address=instance_config.ADMIN_EMAIL)
+        address=instance_config.ADMIN_EMAIL, roles="Admin")
+    user.create(username="anonymous", roles='Anonymous')
 
     db.commit()
 
 #
 # $Log: not supported by cvs2svn $
 
     db.commit()
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.23  2002/07/14 02:05:54  richard
+# . all storage-specific code (ie. backend) is now implemented by the backends
+#
 # Revision 1.22  2002/07/09 03:02:53  richard
 # More indexer work:
 # - all String properties may now be indexed too. Currently there's a bit of
 # Revision 1.22  2002/07/09 03:02:53  richard
 # More indexer work:
 # - all String properties may now be indexed too. Currently there's a bit of
index 76bd4e5547112cf3b0c2d39797833e1a996ce631..c5a4b8a49e433aec78036fc1db5db08ffd1ced62 100644 (file)
@@ -1,4 +1,4 @@
-<!-- $Id: user.item,v 1.2 2002-02-15 07:08:45 richard Exp $-->
+<!-- $Id: user.item,v 1.3 2002-07-26 08:27:00 richard Exp $-->
 <table border=0 cellspacing=0 cellpadding=2>
 
 <tr class="strong-header">
 <table border=0 cellspacing=0 cellpadding=2>
 
 <tr class="strong-header">
     <td width=1% nowrap align=right><span class="form-label">Login Password</span></td>
     <td class="form-text"><display call="field('password', size=10)"></td>
 </tr>
     <td width=1% nowrap align=right><span class="form-label">Login Password</span></td>
     <td class="form-text"><display call="field('password', size=10)"></td>
 </tr>
+<require permission="Web Roles">
+ <tr bgcolor="ffffea">
+    <td width=1% nowrap align=right><span class="form-label">Roles</span></td>
+    <td class="form-text"><display call="field('roles', size=10)"></td>
+ </tr>
+</require>
 <tr  bgcolor="ffffea">
     <td width=1% nowrap align=right><span class="form-label">Phone</span></td>
     <td class="form-text"><display call="field('phone', size=40)"></td>
 <tr  bgcolor="ffffea">
     <td width=1% nowrap align=right><span class="form-label">Phone</span></td>
     <td class="form-text"><display call="field('phone', size=40)"></td>
index ec6a3bf8f2fcaa7cc403849e9098782ae99b533a..532e1494defc2580cb1f322c6b89b2c029cf4fee 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: instance_config.py,v 1.18 2002-05-25 07:16:25 rochecompaan Exp $
+# $Id: instance_config.py,v 1.19 2002-07-26 08:27:00 richard Exp $
 
 MAIL_DOMAIN=MAILHOST=HTTP_HOST=None
 HTTP_PORT=0
 
 MAIL_DOMAIN=MAILHOST=HTTP_HOST=None
 HTTP_PORT=0
@@ -68,14 +68,13 @@ LOG = os.path.join(INSTANCE_HOME, 'roundup.log')
 # Where to place the web filtering HTML on the index page
 FILTER_POSITION = 'bottom'          # one of 'top', 'bottom', 'top and bottom'
 
 # Where to place the web filtering HTML on the index page
 FILTER_POSITION = 'bottom'          # one of 'top', 'bottom', 'top and bottom'
 
-# Deny or allow anonymous access to the web interface
-ANONYMOUS_ACCESS = 'deny'           # either 'deny' or 'allow'
-
-# Deny or allow anonymous users to register through the web interface
-ANONYMOUS_REGISTER = 'deny'         # either 'deny' or 'allow'
-
-# Deny or allow anonymous users to register through the mail interface
-ANONYMOUS_REGISTER_MAIL = 'deny'    # either 'deny' or 'allow'
+# 
+# SECURITY DEFINITIONS
+#
+# define the Roles that a user gets when they register with the tracker
+# these are a comma-separated string of role names (e.g. 'Admin,User')
+NEW_WEB_USER_ROLES = 'User'
+NEW_EMAIL_USER_ROLES = 'User'
 
 # Send nosy messages to the author of the message
 MESSAGES_TO_AUTHOR = 'no'           # either 'yes' or 'no'
 
 # Send nosy messages to the author of the message
 MESSAGES_TO_AUTHOR = 'no'           # either 'yes' or 'no'
@@ -214,6 +213,9 @@ SUPPORT_FILTER = {
 
 #
 # $Log: not supported by cvs2svn $
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.18  2002/05/25 07:16:25  rochecompaan
+# Merged search_indexing-branch with HEAD
+#
 # Revision 1.17  2002/05/22 00:32:34  richard
 #  . changed the default message list in issues to display the message body
 #  . made backends.__init__ be more specific about which ImportErrors it really
 # Revision 1.17  2002/05/22 00:32:34  richard
 #  . changed the default message list in issues to display the message body
 #  . made backends.__init__ be more specific about which ImportErrors it really
index 6bffec38ade91ab84575e1d4218cf04b50502cf3..56cf98e4035f4fd9e4d3aedee4a1ddd4be4ddc31 100644 (file)
@@ -272,3 +272,53 @@ class VolatileClass(hyperdb.Class):
     def index(self, nodeid):
         pass
 
     def index(self, nodeid):
         pass
 
+    def stringFind(self, **requirements):
+        """Locate a particular node by matching a set of its String
+           properties in a caseless search.
+
+           If the property is not a String property, a TypeError is raised.
+        
+           The return is a list of the id of all nodes that match.
+        """
+        for propname in requirements.keys():
+            prop = self.properties[propname]
+            if isinstance(not prop, String):
+                raise TypeError, "'%s' not a String property"%propname
+            requirements[propname] = requirements[propname].lower()
+        l = []
+        for nodeid, node in self.store.items():
+            for key, value in requirements.items():
+                if node[key] and node[key].lower() != value:
+                    break
+            else:
+                l.append(nodeid)
+        return l
+
+    def getkey(self):
+        """Return the name of the key property for this class or None."""
+        return self.key
+
+    def labelprop(self, default_to_id=0):
+        ''' Return the property name for a label for the given node.
+
+        This method attempts to generate a consistent label for the node.
+        It tries the following in order:
+            1. key property
+            2. "name" property
+            3. "title" property
+            4. first property from the sorted property name list
+        '''
+        k = self.getkey()
+        if  k:
+            return k
+        props = self.getprops()
+        if props.has_key('name'):
+            return 'name'
+        elif props.has_key('title'):
+            return 'title'
+        if default_to_id:
+            return 'id'
+        props = props.keys()
+        props.sort()
+        return props[0]
+
index ffc07c7c8e8414c96d7179d750f0171213ab1d3f..d1796720c7835bb7e03420610ce4f26f5b3d3876 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: test_db.py,v 1.37 2002-07-25 07:14:06 richard Exp $ 
+# $Id: test_db.py,v 1.38 2002-07-26 08:27:00 richard Exp $ 
 
 import unittest, os, shutil, time
 
 
 import unittest, os, shutil, time
 
@@ -28,7 +28,7 @@ def setupSchema(db, create, module):
     status = module.Class(db, "status", name=String())
     status.setkey("name")
     user = module.Class(db, "user", username=String(), password=Password(),
     status = module.Class(db, "status", name=String())
     status.setkey("name")
     user = module.Class(db, "user", username=String(), password=Password(),
-        assignable=Boolean(), age=Number(), roles=Multilink('role'))
+        assignable=Boolean(), age=Number(), roles=String())
     user.setkey("username")
     file = module.FileClass(db, "file", name=String(), type=String(),
         comment=String(indexme="yes"))
     user.setkey("username")
     file = module.FileClass(db, "file", name=String(), type=String(),
         comment=String(indexme="yes"))
@@ -603,6 +603,14 @@ def suite():
 
 #
 # $Log: not supported by cvs2svn $
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.37  2002/07/25 07:14:06  richard
+# Bugger it. Here's the current shape of the new security implementation.
+# 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 :)
+#
 # 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
 # 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
index 8b155fc2422845b87a5487696153426b03e54106..6ed63d97c8c6440013f8e44f80214a58ffb6994f 100644 (file)
@@ -8,7 +8,7 @@
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 #
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 #
-# $Id: test_htmltemplate.py,v 1.18 2002-07-25 07:14:06 richard Exp $ 
+# $Id: test_htmltemplate.py,v 1.19 2002-07-26 08:27:00 richard Exp $ 
 
 import unittest, cgi, time, os, shutil
 
 
 import unittest, cgi, time, os, shutil
 
@@ -434,10 +434,8 @@ class IndexTemplateCase(unittest.TestCase):
         tf.props = ['title']
 
         # admin user
         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])
+        self.db.user.create(username="admin", roles='Admin')
+        self.db.user.create(username="anonymous", roles='User')
 
     def testBasic(self):
         self.assertEqual(self.tf.execute_template('hello'), 'hello')
 
     def testBasic(self):
         self.assertEqual(self.tf.execute_template('hello'), 'hello')
@@ -503,10 +501,8 @@ class ItemTemplateCase(unittest.TestCase):
         tf.nodeid = self.db.issue.create(title="spam", status='1')
 
         # admin user
         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])
+        self.db.user.create(username="admin", roles='Admin')
+        self.db.user.create(username="anonymous", roles='User')
 
     def testBasic(self):
         self.assertEqual(self.tf.execute_template('hello'), 'hello')
 
     def testBasic(self):
         self.assertEqual(self.tf.execute_template('hello'), 'hello')
@@ -549,6 +545,14 @@ def suite():
 
 #
 # $Log: not supported by cvs2svn $
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.18  2002/07/25 07:14:06  richard
+# Bugger it. Here's the current shape of the new security implementation.
+# 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 :)
+#
 # Revision 1.17  2002/07/18 23:07:07  richard
 # Unit tests and a few fixes.
 #
 # Revision 1.17  2002/07/18 23:07:07  richard
 # Unit tests and a few fixes.
 #
index 1a9520ddf9b28a4e9ab64c06e24ebe417026d509..11e0aeb58413588bb5b75524ce1a316c3af494a0 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: test_init.py,v 1.13 2002-07-14 02:05:54 richard Exp $
+# $Id: test_init.py,v 1.14 2002-07-26 08:27:00 richard Exp $
 
 import unittest, os, shutil, errno, imp, sys
 
 
 import unittest, os, shutil, errno, imp, sys
 
@@ -60,7 +60,7 @@ class ClassicTestCase(MyTestCase):
         l = db.keyword.list()
         ae(l, [])
         l = db.user.list()
         l = db.keyword.list()
         ae(l, [])
         l = db.user.list()
-        ae(l, ['1'])
+        ae(l, ['1', '2'])
         l = db.msg.list()
         ae(l, [])
         l = db.file.list()
         l = db.msg.list()
         ae(l, [])
         l = db.file.list()
@@ -155,6 +155,9 @@ def suite():
 
 #
 # $Log: not supported by cvs2svn $
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.13  2002/07/14 02:05:54  richard
+# . all storage-specific code (ie. backend) is now implemented by the backends
+#
 # Revision 1.12  2002/07/11 01:13:13  richard
 # *** empty log message ***
 #
 # Revision 1.12  2002/07/11 01:13:13  richard
 # *** empty log message ***
 #
index e8325bc5a89fdd071b412cb05ef3e1d4e5ecd39b..524f85034749a41cffd2a4e5b94658eb387d72a3 100644 (file)
@@ -8,7 +8,7 @@
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 #
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 #
-# $Id: test_mailgw.py,v 1.23 2002-07-14 02:02:43 richard Exp $
+# $Id: test_mailgw.py,v 1.24 2002-07-26 08:27:00 richard Exp $
 
 import unittest, cStringIO, tempfile, os, shutil, errno, imp, sys, difflib
 
 
 import unittest, cStringIO, tempfile, os, shutil, errno, imp, sys, difflib
 
@@ -20,7 +20,7 @@ import unittest, cStringIO, tempfile, os, shutil, errno, imp, sys, difflib
 #except ImportError :
 #    import rfc822 as email
 
 #except ImportError :
 #    import rfc822 as email
 
-from roundup.mailgw import MailGW
+from roundup.mailgw import MailGW, Unauthorized
 from roundup import init, instance
 
 # TODO: make this output only enough equal lines for context, not all of
 from roundup import init, instance
 
 # TODO: make this output only enough equal lines for context, not all of
@@ -113,7 +113,7 @@ This is a test submission of a new issue.
             self.assertEqual('no error', error)
         l = self.db.issue.get(nodeid, 'nosy')
         l.sort()
             self.assertEqual('no error', error)
         l = self.db.issue.get(nodeid, 'nosy')
         l.sort()
-        self.assertEqual(l, ['2', '3'])
+        self.assertEqual(l, ['3', '4'])
 
     def testNewIssue(self):
         self.doNewIssue()
 
     def testNewIssue(self):
         self.doNewIssue()
@@ -138,7 +138,7 @@ This is a test submission of a new issue.
             self.assertEqual('no error', error)
         l = self.db.issue.get(nodeid, 'nosy')
         l.sort()
             self.assertEqual('no error', error)
         l = self.db.issue.get(nodeid, 'nosy')
         l.sort()
-        self.assertEqual(l, ['2', '3'])
+        self.assertEqual(l, ['3', '4'])
 
     def testAlternateAddress(self):
         message = cStringIO.StringIO('''Content-Type: text/plain;
 
     def testAlternateAddress(self):
         message = cStringIO.StringIO('''Content-Type: text/plain;
@@ -295,7 +295,7 @@ This is a followup
         handler.main(message)
         l = self.db.issue.get('1', 'nosy')
         l.sort()
         handler.main(message)
         l = self.db.issue.get('1', 'nosy')
         l.sort()
-        self.assertEqual(l, ['2', '3', '4', '5'])
+        self.assertEqual(l, ['3', '4', '5', '6'])
 
         self.compareStrings(open(os.environ['SENDMAILDEBUG']).read(),
 '''FROM: roundup-admin@your.tracker.email.domain.example
 
         self.compareStrings(open(os.environ['SENDMAILDEBUG']).read(),
 '''FROM: roundup-admin@your.tracker.email.domain.example
@@ -626,11 +626,49 @@ Subject: [issue1] Testing... [nosy=-richard]
         handler.main(message)
         l = self.db.issue.get('1', 'nosy')
         l.sort()
         handler.main(message)
         l = self.db.issue.get('1', 'nosy')
         l.sort()
-        self.assertEqual(l, ['2'])
+        self.assertEqual(l, ['3'])
 
         # NO NOSY MESSAGE SHOULD BE SENT!
         self.assert_(not os.path.exists(os.environ['SENDMAILDEBUG']))
 
 
         # NO NOSY MESSAGE SHOULD BE SENT!
         self.assert_(not os.path.exists(os.environ['SENDMAILDEBUG']))
 
+    def testNewUserAuthor(self):
+        # first without the permission
+        Anonid = self.db.role.lookup('Anonymous')
+        self.db.role.set(Anonid, permissions=[])
+        anonid = self.db.user.lookup('anonymous')
+        self.db.user.set(anonid, roles='Anonymous')
+
+        self.db.security.hasPermission('Email Registration', anonid)
+        l = self.db.user.list()
+        l.sort()
+        s = '''Content-Type: text/plain;
+  charset="iso-8859-1"
+From: fubar <fubar@bork.bork.bork>
+To: issue_tracker@your.tracker.email.domain.example
+Message-Id: <dummy_test_message_id>
+Subject: [issue] Testing...
+
+This is a test submission of a new issue.
+'''
+        message = cStringIO.StringIO(s)
+        handler = self.instance.MailGW(self.instance, self.db)
+        handler.trapExceptions = 0
+        self.assertRaises(Unauthorized, handler.main, message)
+        m = self.db.user.list()
+        m.sort()
+        self.assertEqual(l, m)
+
+        # now with the permission
+        p = self.db.security.getPermission('Email Registration')
+        self.db.role.set(Anonid, permissions=[p])
+        handler = self.instance.MailGW(self.instance, self.db)
+        handler.trapExceptions = 0
+        message = cStringIO.StringIO(s)
+        handler.main(message)
+        m = self.db.user.list()
+        m.sort()
+        self.assertNotEqual(l, m)
+
     def testEnc01(self):
         self.doNewIssue()
         message = cStringIO.StringIO('''Content-Type: text/plain;
     def testEnc01(self):
         self.doNewIssue()
         message = cStringIO.StringIO('''Content-Type: text/plain;
@@ -743,6 +781,9 @@ def suite():
 
 #
 # $Log: not supported by cvs2svn $
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.23  2002/07/14 02:02:43  richard
+# Fixed the unit tests for the new multilist controls in the mailgw
+#
 # Revision 1.22  2002/07/09 01:21:24  richard
 # Added ability for unit tests to turn off exception handling in mailgw so
 # that exceptions are reported earlier (and hence make sense).
 # Revision 1.22  2002/07/09 01:21:24  richard
 # Added ability for unit tests to turn off exception handling in mailgw so
 # that exceptions are reported earlier (and hence make sense).
index 89b4c461531fe78070e70ef75e4e64ac091ce612..dce68fdb0d8ea0459f2cbc7ae44baa8471095eab 100644 (file)
@@ -18,7 +18,7 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
 # 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 $
+# $Id: test_security.py,v 1.2 2002-07-26 08:27:00 richard Exp $
 
 import os, unittest, shutil
 
 
 import os, unittest, shutil
 
@@ -48,31 +48,42 @@ class PermissionTest(MyTestCase):
         ei = self.db.security.addPermission(name="Edit", klass="issue",
                         description="User is allowed to edit issues")
         self.db.security.addPermissionToRole('User', ei)
         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")
+        ai = self.db.security.addPermission(name="View", klass="issue",
+                        description="User is allowed to access issues")
         self.db.security.addPermissionToRole('User', ai)
 
         self.db.security.addPermissionToRole('User', ai)
 
+    def testGetPermission(self):
+        self.db.security.getPermission('Edit')
+        self.db.security.getPermission('View')
+        self.assertRaises(ValueError, self.db.security.getPermission, 'x')
+        self.assertRaises(ValueError, self.db.security.getPermission, 'Edit',
+            'fubar')
+        ei = self.db.security.addPermission(name="Edit", klass="issue",
+                        description="User is allowed to edit issues")
+        self.db.security.getPermission('Edit', 'issue')
+        ai = self.db.security.addPermission(name="View", klass="issue",
+                        description="User is allowed to access issues")
+        self.db.security.getPermission('View', 'issue')
+
     def testDBinit(self):
     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])
+        self.db.user.create(username="admin", roles='Admin')
+        self.db.user.create(username="anonymous", roles='User')
 
 
-    def testAccess(self):
+    def testAccessControls(self):
         self.testDBinit()
         self.testInitialiseSecurity()
 
         # test class-level access
         userid = self.db.user.lookup('admin')
         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)
+        self.assertEquals(self.db.security.hasPermission('Edit', userid,
+            'issue'), 1)
+        self.assertEquals(self.db.security.hasPermission('Edit', userid,
+            'user'), 1)
         userid = self.db.user.lookup('anonymous')
         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)
+        self.assertEquals(self.db.security.hasPermission('Edit', userid,
+            'issue'), 1)
+        self.assertEquals(self.db.security.hasPermission('Edit', userid,
+            'user'), 0)
 
         # test node-level access
         issueid = self.db.issue.create(title='foo', assignedto='admin')
 
         # test node-level access
         issueid = self.db.issue.create(title='foo', assignedto='admin')
@@ -91,6 +102,14 @@ def suite():
 
 #
 # $Log: not supported by cvs2svn $
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.1  2002/07/25 07:14:06  richard
+# Bugger it. Here's the current shape of the new security implementation.
+# 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 :)
+#
 # Revision 1.1  2002/07/10 06:40:01  richard
 # ehem, forgot to add
 #
 # Revision 1.1  2002/07/10 06:40:01  richard
 # ehem, forgot to add
 #