X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fcgi%2Factions.py;h=36dfee3707f96a02c0c12c0cd49c70f66c8f718c;hb=cb6d804aac7cf46239363eb13fb3ff3b56afe1a6;hp=136fb995eae523d8ddd398465249eab8547dd248;hpb=f141e7d05bcb9b0aa26d084fde384e3eb405f418;p=roundup.git diff --git a/roundup/cgi/actions.py b/roundup/cgi/actions.py index 136fb99..36dfee3 100755 --- a/roundup/cgi/actions.py +++ b/roundup/cgi/actions.py @@ -1,8 +1,7 @@ -#$Id: actions.py,v 1.73 2008-08-18 05:04:01 richard Exp $ - import re, cgi, StringIO, urllib, time, random, csv, codecs from roundup import hyperdb, token, date, password +from roundup.actions import Action as BaseAction from roundup.i18n import _ import roundup.exceptions from roundup.cgi import exceptions, templating @@ -104,30 +103,37 @@ class RetireAction(Action): def handle(self): """Retire the context item.""" - # if we want to view the index template now, then unset the nodeid + # ensure modification comes via POST + if self.client.env['REQUEST_METHOD'] != 'POST': + raise roundup.exceptions.Reject(self._('Invalid request')) + + # if we want to view the index template now, then unset the itemid # context info (a special-case for retire actions on the index page) - nodeid = self.nodeid + itemid = self.nodeid if self.template == 'index': self.client.nodeid = None # make sure we don't try to retire admin or anonymous if self.classname == 'user' and \ - self.db.user.get(nodeid, 'username') in ('admin', 'anonymous'): + self.db.user.get(itemid, 'username') in ('admin', 'anonymous'): raise ValueError, self._( 'You may not retire the admin or anonymous user') + # check permission + if not self.hasPermission('Retire', classname=self.classname, + itemid=itemid): + raise exceptions.Unauthorised, self._( + 'You do not have permission to retire %(class)s' + ) % {'class': self.classname} + # do the retire - self.db.getclass(self.classname).retire(nodeid) + self.db.getclass(self.classname).retire(itemid) self.db.commit() self.client.ok_message.append( self._('%(classname)s %(itemid)s has been retired')%{ - 'classname': self.classname.capitalize(), 'itemid': nodeid}) + 'classname': self.classname.capitalize(), 'itemid': itemid}) - def hasPermission(self, permission, classname=Action._marker, itemid=None): - if itemid is None: - itemid = self.nodeid - return Action.hasPermission(self, permission, classname, itemid) class SearchAction(Action): name = 'search' @@ -234,7 +240,7 @@ class SearchAction(Action): if isinstance(prop, hyperdb.String): v = self.form[key].value l = token.token_split(v) - if len(l) > 1 or l[0] != v: + if len(l) != 1 or l[0] != v: self.form.value.remove(self.form[key]) # replace the single value with the split list for v in l: @@ -275,12 +281,19 @@ class EditCSVAction(Action): The "rows" CGI var defines the CSV-formatted entries for the class. New nodes are identified by the ID 'X' (or any other non-existent ID) and removed lines are retired. - """ + # ensure modification comes via POST + if self.client.env['REQUEST_METHOD'] != 'POST': + raise roundup.exceptions.Reject(self._('Invalid request')) + + # figure the properties list for the class cl = self.db.classes[self.classname] - idlessprops = cl.getprops(protected=0).keys() - idlessprops.sort() - props = ['id'] + idlessprops + props_without_id = cl.getprops(protected=0).keys() + + # the incoming CSV data will always have the properties in colums + # sorted and starting with the "id" column + props_without_id.sort() + props = ['id'] + props_without_id # do the edit rows = StringIO.StringIO(self.form['rows'].value) @@ -294,25 +307,43 @@ class EditCSVAction(Action): if values == props: continue - # extract the nodeid - nodeid, values = values[0], values[1:] - found[nodeid] = 1 + # extract the itemid + itemid, values = values[0], values[1:] + found[itemid] = 1 # see if the node exists - if nodeid in ('x', 'X') or not cl.hasnode(nodeid): + if itemid in ('x', 'X') or not cl.hasnode(itemid): exists = 0 + + # check permission to create this item + if not self.hasPermission('Create', classname=self.classname): + raise exceptions.Unauthorised, self._( + 'You do not have permission to create %(class)s' + ) % {'class': self.classname} + elif cl.hasnode(itemid) and cl.is_retired(itemid): + # If a CSV line just mentions an id and the corresponding + # item is retired, then the item is restored. + cl.restore(itemid) + continue else: exists = 1 # confirm correct weight - if len(idlessprops) != len(values): + if len(props_without_id) != len(values): self.client.error_message.append( self._('Not enough values on line %(line)s')%{'line':line}) return # extract the new values d = {} - for name, value in zip(idlessprops, values): + for name, value in zip(props_without_id, values): + # check permission to edit this property on this item + if exists and not self.hasPermission('Edit', itemid=itemid, + classname=self.classname, property=name): + raise exceptions.Unauthorised, self._( + 'You do not have permission to edit %(class)s' + ) % {'class': self.classname} + prop = cl.properties[name] value = value.strip() # only add the property if it has a value @@ -341,15 +372,21 @@ class EditCSVAction(Action): # perform the edit if exists: # edit existing - cl.set(nodeid, **d) + cl.set(itemid, **d) else: # new node found[cl.create(**d)] = 1 # retire the removed entries - for nodeid in cl.list(): - if not found.has_key(nodeid): - cl.retire(nodeid) + for itemid in cl.list(): + if not found.has_key(itemid): + # check permission to retire this item + if not self.hasPermission('Retire', itemid=itemid, + classname=self.classname): + raise exceptions.Unauthorised, self._( + 'You do not have permission to retire %(class)s' + ) % {'class': self.classname} + cl.retire(itemid) # all OK self.db.commit() @@ -441,9 +478,9 @@ class EditCommon(Action): if linkid is None or linkid.startswith('-'): # linking to a new item if isinstance(propdef, hyperdb.Multilink): - props[linkprop] = [newid] + props[linkprop] = [nodeid] else: - props[linkprop] = newid + props[linkprop] = nodeid else: # linking to an existing item if isinstance(propdef, hyperdb.Multilink): @@ -451,7 +488,7 @@ class EditCommon(Action): existing.append(nodeid) props[linkprop] = existing else: - props[linkprop] = newid + props[linkprop] = nodeid return '
'.join(m) @@ -494,10 +531,8 @@ class EditCommon(Action): # The user must have permission to edit each of the properties # being changed. for p in props: - if not self.hasPermission('Edit', - itemid=itemid, - classname=classname, - property=p): + if not self.hasPermission('Edit', itemid=itemid, + classname=classname, property=p): return 0 # Since the user has permission to edit all of the properties, # the edit is OK. @@ -509,9 +544,20 @@ class EditCommon(Action): Base behaviour is to check the user can edit this class. No additional property checks are made. """ + if not classname : classname = self.client.classname - return self.hasPermission('Create', classname=classname) + + if not self.hasPermission('Create', classname=classname): + return 0 + + # Check Create permission for each property, to avoid being able + # to set restricted ones on new item creation + for key in props: + if not self.hasPermission('Create', classname=classname, + property=key): + return 0 + return 1 class EditItemAction(EditCommon): def lastUserActivity(self): @@ -555,6 +601,10 @@ class EditItemAction(EditCommon): See parsePropsFromForm and _editnodes for special variables. """ + # ensure modification comes via POST + if self.client.env['REQUEST_METHOD'] != 'POST': + raise roundup.exceptions.Reject(self._('Invalid request')) + user_activity = self.lastUserActivity() if user_activity: props = self.detectCollision(user_activity, self.lastNodeActivity()) @@ -597,6 +647,10 @@ class NewItemAction(EditCommon): This follows the same form as the EditItemAction, with the same special form values. ''' + # ensure modification comes via POST + if self.client.env['REQUEST_METHOD'] != 'POST': + raise roundup.exceptions.Reject(self._('Invalid request')) + # parse the props from the form try: props, links = self.client.parsePropsFromForm(create=1) @@ -766,7 +820,7 @@ class ConfRegoAction(RegoCommon): class RegisterAction(RegoCommon, EditCommon): name = 'register' - permissionType = 'Create' + permissionType = 'Register' def handle(self): """Attempt to create a new user based on the contents of the form @@ -774,6 +828,10 @@ class RegisterAction(RegoCommon, EditCommon): Return 1 on successful login. """ + # ensure modification comes via POST + if self.client.env['REQUEST_METHOD'] != 'POST': + raise roundup.exceptions.Reject(self._('Invalid request')) + # parse the props from the form try: props, links = self.client.parsePropsFromForm(create=1) @@ -888,6 +946,10 @@ class LoginAction(Action): Sets up a session for the user which contains the login credentials. """ + # ensure modification comes via POST + if self.client.env['REQUEST_METHOD'] != 'POST': + raise roundup.exceptions.Reject(self._('Invalid request')) + # we need the username at a minimum if not self.form.has_key('__login_name'): self.client.error_message.append(self._('Username required')) @@ -987,8 +1049,59 @@ class ExportCSVAction(Action): # and search for itemid in klass.filter(matches, filterspec, sort, group): - self.client._socket_op(writer.writerow, [str(klass.get(itemid, col)) for col in columns]) + row = [] + for name in columns: + # check permission to view this property on this item + if not self.hasPermission('View', itemid=itemid, + classname=request.classname, property=name): + raise exceptions.Unauthorised, self._( + 'You do not have permission to view %(class)s' + ) % {'class': request.classname} + row.append(str(klass.get(itemid, name))) + self.client._socket_op(writer.writerow, row) return '\n' + +class Bridge(BaseAction): + """Make roundup.actions.Action executable via CGI request. + + Using this allows users to write actions executable from multiple frontends. + CGI Form content is translated into a dictionary, which then is passed as + argument to 'handle()'. XMLRPC requests have to pass this dictionary + directly. + """ + + def __init__(self, *args): + + # As this constructor is callable from multiple frontends, each with + # different Action interfaces, we have to look at the arguments to + # figure out how to complete construction. + if (len(args) == 1 and + hasattr(args[0], '__class__') and + args[0].__class__.__name__ == 'Client'): + self.cgi = True + self.execute = self.execute_cgi + self.client = args[0] + self.form = self.client.form + else: + self.cgi = False + + def execute_cgi(self): + args = {} + for key in self.form.keys(): + args[key] = self.form.getvalue(key) + self.permission(args) + return self.handle(args) + + def permission(self, args): + """Raise Unauthorised if the current user is not allowed to execute + this action. Users may override this method.""" + + pass + + def handle(self, args): + + raise NotImplementedError + # vim: set filetype=python sts=4 sw=4 et si :