Code

Proper handling of 'Create' permissions in both mail gateway (earlier
[roundup.git] / roundup / cgi / actions.py
index 136fb995eae523d8ddd398465249eab8547dd248..36dfee3707f96a02c0c12c0cd49c70f66c8f718c 100755 (executable)
@@ -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
 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
 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."""
 
     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)
         # 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 \
         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')
 
             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
         # 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')%{
         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'
 
 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 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:
                         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.
         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]
         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)
 
         # do the edit
         rows = StringIO.StringIO(self.form['rows'].value)
@@ -294,25 +307,43 @@ class EditCSVAction(Action):
             if values == props:
                 continue
 
             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
 
             # 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
                 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
             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 = {}
                 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
                 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
             # 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
             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()
 
         # 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):
                         if linkid is None or linkid.startswith('-'):
                             # linking to a new item
                             if isinstance(propdef, hyperdb.Multilink):
-                                props[linkprop] = [newid]
+                                props[linkprop] = [nodeid]
                             else:
                             else:
-                                props[linkprop] = newid
+                                props[linkprop] = nodeid
                         else:
                             # linking to an existing item
                             if isinstance(propdef, hyperdb.Multilink):
                         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:
                                 existing.append(nodeid)
                                 props[linkprop] = existing
                             else:
-                                props[linkprop] = newid
+                                props[linkprop] = nodeid
 
         return '<br>'.join(m)
 
 
         return '<br>'.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:
         # 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.
                 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.
         """
         Base behaviour is to check the user can edit this class. No additional
         property checks are made.
         """
+
         if not classname :
             classname = self.client.classname
         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):
 
 class EditItemAction(EditCommon):
     def lastUserActivity(self):
@@ -555,6 +601,10 @@ class EditItemAction(EditCommon):
         See parsePropsFromForm and _editnodes for special variables.
 
         """
         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())
         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.
         '''
             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)
         # 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'
 
 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
 
     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.
         """
 
         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)
         # 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.
 
         """
         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'))
         # 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):
 
         # 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'
 
 
         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 :
 # vim: set filetype=python sts=4 sw=4 et si :