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