From: richard Date: Fri, 26 Jul 2002 08:27:00 +0000 (+0000) Subject: Very close now. The cgi and mailgw now use the new security API. The two X-Git-Url: https://git.tokkee.org/?a=commitdiff_plain;h=4d8d40c99d434209f317e08d9977334681efd92d;p=roundup.git Very close now. The cgi and mailgw now use the new security API. The two 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 --- diff --git a/COPYING.txt b/COPYING.txt index 330c6f7..db92559 100644 --- a/COPYING.txt +++ b/COPYING.txt @@ -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. diff --git a/doc/security.txt b/doc/security.txt index 2d3748a..66f9f7e 100644 --- a/doc/security.txt +++ b/doc/security.txt @@ -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 diff --git a/doc/upgrading.txt b/doc/upgrading.txt index e768d85..2fb2159 100644 --- a/doc/upgrading.txt +++ b/doc/upgrading.txt @@ -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 diff --git a/roundup/backends/back_anydbm.py b/roundup/backends/back_anydbm.py index 128453f..3b85d78 100644 --- a/roundup/backends/back_anydbm.py +++ b/roundup/backends/back_anydbm.py @@ -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 diff --git a/roundup/cgi_client.py b/roundup/cgi_client.py index 9574d95..99a65f1 100644 --- a/roundup/cgi_client.py +++ b/roundup/cgi_client.py @@ -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 Issues') ] - if userid: + user_info = _('Login') + 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('%s' % (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('%s'%(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 = _(''' My Details | Logout ''')%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 ' - '%(cap_class)s')%locals()) - - # if there's no config header link spec, force a user link here - if not hasattr(self.instance, 'HEADER_INDEX_LINKS'): - links.append(_('My Issues')%locals()) + + # figure the "add class" links + if hasattr(self.instance, 'HEADER_ADD_LINKS'): + classes = self.instance.HEADER_ADD_LINKS else: - user_info = _('Login') - 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 ' + '%(cap_class)s')%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(_('Class List')) - links.append(_('User List')) + links.append(_('User List')) links.append(_('Add User')) # 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 ' '%(cap_class)s')%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(_('''

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 ("").

+

Multilink properties have their multiple + values colon (":") separated (... ,"one:two:three", ...)

Remove entries by deleting their line. Add new entries by appending them to the table - put an X in the id column.

''')%{'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(_('
')) @@ -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('\n') - for cn in classnames: - cl = self.db.getclass(cn) - self.write(''%(cn, cn.capitalize())) - for key, value in cl.properties.items(): - if value is None: value = '' - else: value = str(value) - self.write(''%( - key, cgi.escape(value))) - self.write('
' - '%s
%s%s
') - 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('\n') + for cn in classnames: + cl = self.db.getclass(cn) + self.write(''%(cn, cn.capitalize())) + for key, value in cl.properties.items(): + if value is None: value = '' + else: value = str(value) + self.write(''%( + key, cgi.escape(value))) + self.write('
' + '%s
%s%s
') + 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) { ''')%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('') 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. # diff --git a/roundup/htmltemplate.py b/roundup/htmltemplate.py index 2558f37..56074fa 100644 --- a/roundup/htmltemplate.py +++ b/roundup/htmltemplate.py @@ -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. # diff --git a/roundup/mailgw.py b/roundup/mailgw.py index 88868fd..a1c21ac 100644 --- a/roundup/mailgw.py +++ b/roundup/mailgw.py @@ -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. diff --git a/roundup/roundupdb.py b/roundup/roundupdb.py index 57e678d..1b2a385 100644 --- a/roundup/roundupdb.py +++ b/roundup/roundupdb.py @@ -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. diff --git a/roundup/security.py b/roundup/security.py index 7475ca6..49e4477 100644 --- a/roundup/security.py +++ b/roundup/security.py @@ -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 diff --git a/roundup/templates/classic/dbinit.py b/roundup/templates/classic/dbinit.py index 1edc2dd..7c2153d 100644 --- a/roundup/templates/classic/dbinit.py +++ b/roundup/templates/classic/dbinit.py @@ -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 # diff --git a/roundup/templates/classic/html/user.item b/roundup/templates/classic/html/user.item index e4a4eac..f4ab594 100644 --- a/roundup/templates/classic/html/user.item +++ b/roundup/templates/classic/html/user.item @@ -1,4 +1,4 @@ - + @@ -17,6 +17,12 @@ + + + + + + diff --git a/roundup/templates/classic/instance_config.py b/roundup/templates/classic/instance_config.py index cd06f42..7b3e954 100644 --- a/roundup/templates/classic/instance_config.py +++ b/roundup/templates/classic/instance_config.py @@ -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 diff --git a/roundup/templates/extended/dbinit.py b/roundup/templates/extended/dbinit.py index 0ada216..6149c4d 100644 --- a/roundup/templates/extended/dbinit.py +++ b/roundup/templates/extended/dbinit.py @@ -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 diff --git a/roundup/templates/extended/html/user.item b/roundup/templates/extended/html/user.item index 76bd4e5..c5a4b8a 100644 --- a/roundup/templates/extended/html/user.item +++ b/roundup/templates/extended/html/user.item @@ -1,4 +1,4 @@ - +
Login Password
Roles
Phone
@@ -17,6 +17,12 @@ + + + + + + diff --git a/roundup/templates/extended/instance_config.py b/roundup/templates/extended/instance_config.py index ec6a3bf..532e149 100644 --- a/roundup/templates/extended/instance_config.py +++ b/roundup/templates/extended/instance_config.py @@ -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 diff --git a/roundup/volatiledb.py b/roundup/volatiledb.py index 6bffec3..56cf98e 100644 --- a/roundup/volatiledb.py +++ b/roundup/volatiledb.py @@ -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] + diff --git a/test/test_db.py b/test/test_db.py index ffc07c7..d179672 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -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 diff --git a/test/test_htmltemplate.py b/test/test_htmltemplate.py index 8b155fc..6ed63d9 100644 --- a/test/test_htmltemplate.py +++ b/test/test_htmltemplate.py @@ -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. # diff --git a/test/test_init.py b/test/test_init.py index 1a9520d..11e0aeb 100644 --- a/test/test_init.py +++ b/test/test_init.py @@ -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 *** # diff --git a/test/test_mailgw.py b/test/test_mailgw.py index e8325bc..524f850 100644 --- a/test/test_mailgw.py +++ b/test/test_mailgw.py @@ -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 +To: issue_tracker@your.tracker.email.domain.example +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). diff --git a/test/test_security.py b/test/test_security.py index 89b4c46..dce68fd 100644 --- a/test/test_security.py +++ b/test/test_security.py @@ -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 #
Login Password
Roles
Phone