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/)
 
+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.
@@ -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.
 
-
 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
 ===================
 
-:Version: $Revision: 1.12 $
+:Version: $Revision: 1.13 $
 
 Current situation
 =================
@@ -184,7 +184,7 @@ A security module defines::
                 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.
 
@@ -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)
-    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()``::
 
+    # create the two default users
     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::
 
-    if db.security.hasClassPermission('issue', 'Edit', userid):
+    if db.security.hasPermission('issue', 'Edit', 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.
-  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.
@@ -293,8 +291,7 @@ Implementation as shipped
 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:
 
@@ -303,7 +300,7 @@ The default interfaces define:
 
 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)
 
@@ -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:
 
-- 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
@@ -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
+  - change all explicit admin user checks for Role checks
   - 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
+TODO: migration of security settings
 
 
 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.
 # 
-#$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
@@ -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[key] and node[key].lower() != value:
+                    if node[key] is None or node[key].lower() != value:
                         break
                 else:
                     l.append(nodeid)
@@ -1776,6 +1776,14 @@ class IssueClass(Class, roundupdb.IssueClass):
 
 #
 #$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
index 9574d95690cf0df269752160c3af5fc5e4974c41..99a65f15c2693acc8f3bfbedab70ba7178bb81de 100644 (file)
@@ -15,7 +15,7 @@
 # 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).
@@ -39,9 +39,13 @@ def initialiseSecurity(security):
         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")
-    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:
     '''
@@ -97,14 +101,14 @@ class Client:
                 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'
-        self.request.send_response(200)
+        self.request.send_response(response)
         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
-        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')
-                # 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',
-                        None) and userid is None):
+                        None) and user_name == 'anonymous'):
                     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>')
             ]
 
-        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
-                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.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:
-                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()
 
-            # 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:
-            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 = ''
-        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="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
@@ -265,6 +262,9 @@ function help_window(helpurl, width, height) {
             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())
@@ -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,
-            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 '-'
@@ -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
         '''
-        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
@@ -577,7 +579,6 @@ function help_window(helpurl, width, height) {
         idlessprops = cl.getprops(protected=0).keys()
         props = ['id'] + idlessprops
 
-
         # 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):
-                    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):
@@ -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
-        "%(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>
+        <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})
@@ -647,7 +656,13 @@ function help_window(helpurl, width, height) {
         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>'))
@@ -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
+        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
@@ -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] != ':']:
+            # no dice if you can't edit!
+            if not self.db.security.hasPermission('Edit', userid, cn):
+                raise Unauthorised
             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.
         '''
+        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]
 
@@ -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.
         '''
+        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)
@@ -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.
         '''
-        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')
 
-        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
         
         #
@@ -1129,28 +1158,33 @@ function help_window(helpurl, width, height) {
         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
         '''
-        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
 
+        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.
         '''
@@ -1168,7 +1202,8 @@ function help_window(helpurl, width, height) {
     <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
@@ -1251,6 +1286,11 @@ function help_window(helpurl, width, height) {
 
         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')
 
@@ -1258,7 +1298,9 @@ function help_window(helpurl, width, height) {
         cl = self.db.user
         try:
             props = parsePropsFromForm(self.db, cl, self.form)
+            props['roles'] = self.instance.NEW_WEB_USER_ROLES
             uid = cl.create(**props)
+            self.db.commit()
         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]
 
-        print 'session set to', `session`
-
         # 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):
-        # 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()
@@ -1341,6 +1382,19 @@ function help_window(helpurl, width, height) {
             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
@@ -1378,6 +1432,12 @@ function help_window(helpurl, width, height) {
                 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()
@@ -1392,6 +1452,7 @@ function help_window(helpurl, width, height) {
             action = 'index'
         else:
             action = path[0]
+        self.desired_action = action
 
         # 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':
-            # 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
@@ -1428,14 +1481,6 @@ function help_window(helpurl, width, height) {
             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)
 
@@ -1623,6 +1668,14 @@ def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
 
 #
 # $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.
 #
index 2558f3750f42edae51cc119c319903534433fbc8..56074fa14c3849e7c903139d92b77aadc38eb696 100644 (file)
@@ -15,7 +15,7 @@
 # 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.
@@ -881,7 +881,7 @@ class TemplateFunctions:
         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)
 
@@ -1049,7 +1049,8 @@ class IndexTemplate(TemplateFunctions):
                         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
@@ -1417,6 +1418,14 @@ class NewItemTemplate(ItemTemplate):
 
 #
 # $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.
 #
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. 
 
-$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 UnAuthorized(Exception):
+class Unauthorized(Exception):
     """ 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")
-    security.addPermissionToRole('Anonymous', newid)
 
 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)
-            except UnAuthorized, value:
+            except Unauthorized, value:
                 # just inform the user that he is not authorized
                 sendto = [sendto[0][1]]
                 m = ['']
@@ -522,19 +521,16 @@ Subject was: "%s"
         # 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
-        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
 
-        author = self.db.uidFromAddress(message.getaddrlist('from')[0],
+        author = uidFromAddress(self.db, message.getaddrlist('from')[0],
             create=create)
         if not author:
-            raise UnAuthorized, '''
+            raise Unauthorized, '''
 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)
-            recipient = self.db.uidFromAddress(recipient, create)
+            recipient = uidFromAddress(self.db, recipient, create)
 
             # 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
 
+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]+'), 
@@ -800,6 +843,14 @@ def parseContent(content, keep_citations, keep_body,
 
 #
 # $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.
index 57e678da6ed9cbc74b4a2d7b412913e93d0c21e8..1b2a385551b31cfaefac752acdca7b1e3bcb8456 100644 (file)
@@ -15,7 +15,7 @@
 # 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.
@@ -36,62 +36,12 @@ import hyperdb
 # 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)
 
-    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
 
@@ -476,6 +426,9 @@ class IssueClass:
 
 #
 # $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.
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)
-        ae = self.addPermission(name="Access",
+        ae = self.addPermission(name="View",
             description="User may access everything")
         self.addPermissionToRole('Admin', ae)
-        ae = self.addPermission(name="Assign",
-            description="User may be assigned to anything")
-        self.addPermissionToRole('Admin', ae)
         reg = self.addPermission(name="Register Web",
             description="User may register through the web")
-        self.addPermissionToRole('Anonymous', reg)
         reg = self.addPermission(name="Register Email",
             description="User may register through the email")
-        self.addPermissionToRole('Anonymous', reg)
 
         # initialise the permissions and roles needed for the UIs
         from roundup import cgi_client, mailgw
         cgi_client.initialiseSecurity(self)
         mailgw.initialiseSecurity(self)
 
-    def hasClassPermission(self, classname, permission, userid):
+    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')
-        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
index 1edc2dd1bf73fdfbe68a4026caf1d330692a1703..7c2153ddf5e5c2ada0db13b21c82fe4cdee4ac09 100644 (file)
@@ -15,7 +15,7 @@
 # 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
 
@@ -56,11 +56,13 @@ def open(name=None):
                     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(),
-                    alternate_addresses=String(), queries=Multilink("query"))
+                    alternate_addresses=String(),
+                    queries=Multilink('query'), roles=String())
     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"))
 
+    #
+    # 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)
 
@@ -107,6 +146,9 @@ def init(adminpw):
     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")
@@ -124,13 +166,19 @@ def init(adminpw):
     stat.create(name="done-cbb", order="7")
     stat.create(name="resolved", order="8")
 
+    # create the two default users
     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 $
+# 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
 #
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">
     <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>
index cd06f426ff99d3268d29a074304bd17d5a790442..7b3e9548071b787e72e8a4ee5b738a762fdcd93a 100644 (file)
@@ -15,7 +15,7 @@
 # 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
@@ -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'
 
-# 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'
@@ -178,6 +177,9 @@ SUPPORT_FILTER = {
 
 #
 # $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
index 0ada216bde71f11f1f8e46f6ec901c0630149145..6149c4dc5d8d9bf26408b4eb568703357dd85134 100644 (file)
@@ -15,7 +15,7 @@
 # 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
 
@@ -24,7 +24,6 @@ from select_db import Database, Class, FileClass, IssueClass
 
 def open(name=None):
     ''' as from the roundupdb method openDB 
     ''' 
     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(),
-                    alternate_addresses=String())
+                    alternate_addresses=String(),
+                    queries=Multilink('query'), roles=String())
     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"))
 
+    #
+    # 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)
 
@@ -173,12 +210,16 @@ def init(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 $
+# 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
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">
     <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>
index ec6a3bf8f2fcaa7cc403849e9098782ae99b533a..532e1494defc2580cb1f322c6b89b2c029cf4fee 100644 (file)
@@ -15,7 +15,7 @@
 # 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
@@ -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'
 
-# 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'
@@ -214,6 +213,9 @@ SUPPORT_FILTER = {
 
 #
 # $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
index 6bffec38ade91ab84575e1d4218cf04b50502cf3..56cf98e4035f4fd9e4d3aedee4a1ddd4be4ddc31 100644 (file)
@@ -272,3 +272,53 @@ class VolatileClass(hyperdb.Class):
     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.
 # 
-# $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
 
@@ -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(),
-        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"))
@@ -603,6 +603,14 @@ def suite():
 
 #
 # $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
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.
 #
-# $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
 
@@ -434,10 +434,8 @@ class IndexTemplateCase(unittest.TestCase):
         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')
@@ -503,10 +501,8 @@ class ItemTemplateCase(unittest.TestCase):
         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')
@@ -549,6 +545,14 @@ def suite():
 
 #
 # $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.
 #
index 1a9520ddf9b28a4e9ab64c06e24ebe417026d509..11e0aeb58413588bb5b75524ce1a316c3af494a0 100644 (file)
@@ -15,7 +15,7 @@
 # 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
 
@@ -60,7 +60,7 @@ class ClassicTestCase(MyTestCase):
         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()
@@ -155,6 +155,9 @@ def suite():
 
 #
 # $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 ***
 #
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.
 #
-# $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
 
@@ -20,7 +20,7 @@ import unittest, cStringIO, tempfile, os, shutil, errno, imp, sys, difflib
 #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
@@ -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(l, ['2', '3'])
+        self.assertEqual(l, ['3', '4'])
 
     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(l, ['2', '3'])
+        self.assertEqual(l, ['3', '4'])
 
     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()
-        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
@@ -626,11 +626,49 @@ Subject: [issue1] Testing... [nosy=-richard]
         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']))
 
+    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;
@@ -743,6 +781,9 @@ def suite():
 
 #
 # $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).
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.
 
-# $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
 
@@ -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)
-        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)
 
+    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):
-        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.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')
-        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')
@@ -91,6 +102,14 @@ def suite():
 
 #
 # $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
 #