Code

Add new config-option 'password_pbkdf2_default_rounds' in 'main' section
[roundup.git] / roundup / cgi / actions.py
index ac43e4c0a91fe1164b6e1379bf01c695607738db..d48fb595eb75514bd47c74a4bd8abbbfefec8466 100755 (executable)
@@ -1,12 +1,12 @@
-#$Id: actions.py,v 1.73 2008-08-18 05:04:01 richard Exp $
-
-import re, cgi, StringIO, urllib, time, random, csv, codecs
+import re, cgi, 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.mailgw import uidFromAddress
+from roundup.anypy import io_, urllib_
 
 __all__ = ['Action', 'ShowAction', 'RetireAction', 'SearchAction',
            'EditCSVAction', 'EditItemAction', 'PassResetAction',
@@ -54,17 +54,17 @@ class Action:
         if (self.permissionType and
                 not self.hasPermission(self.permissionType)):
             info = {'action': self.name, 'classname': self.classname}
-            raise exceptions.Unauthorisedself._(
+            raise exceptions.Unauthorised(self._(
                 'You do not have permission to '
-                '%(action)s the %(classname)s class.')%info
+                '%(action)s the %(classname)s class.')%info)
 
     _marker = []
-    def hasPermission(self, permission, classname=_marker, itemid=None):
+    def hasPermission(self, permission, classname=_marker, itemid=None, property=None):
         """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,
-            classname=classname, itemid=itemid)
+            classname=classname, itemid=itemid, property=property)
 
     def gettext(self, msgid):
         """Return the localized translation of msgid"""
@@ -80,23 +80,23 @@ class ShowAction(Action):
     def handle(self):
         """Show a node of a particular class/id."""
         t = n = ''
-        for key in self.form.keys():
+        for key in self.form:
             if self.typere.match(key):
                 t = self.form[key].value.strip()
             elif self.numre.match(key):
                 n = self.form[key].value.strip()
         if not t:
-            raise ValueError, self._('No type specified')
+            raise ValueError(self._('No type specified'))
         if not n:
-            raise exceptions.SeriousError, self._('No ID entered')
+            raise exceptions.SeriousError(self._('No ID entered'))
         try:
             int(n)
         except ValueError:
             d = {'input': n, 'classname': t}
-            raise exceptions.SeriousErrorself._(
-                '"%(input)s" is not an ID (%(classname)s ID required)')%d
+            raise exceptions.SeriousError(self._(
+                '"%(input)s" is not an ID (%(classname)s ID required)')%d)
         url = '%s%s%s'%(self.base, t, n)
-        raise exceptions.Redirect, url
+        raise exceptions.Redirect(url)
 
 class RetireAction(Action):
     name = 'retire'
@@ -104,30 +104,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'):
-            raise ValueError, self._(
-                'You may not retire the admin or anonymous user')
+                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'
@@ -165,14 +172,14 @@ class SearchAction(Action):
                 try:
                     qid = self.db.query.lookup(old_queryname)
                     if not self.hasPermission('Edit', 'query', itemid=qid):
-                        raise exceptions.Unauthorisedself._(
-                            "You do not have permission to edit queries")
+                        raise exceptions.Unauthorised(self._(
+                            "You do not have permission to edit queries"))
                     self.db.query.set(qid, klass=self.classname, url=url)
                 except KeyError:
                     # create a query
                     if not self.hasPermission('Create', 'query'):
-                        raise exceptions.Unauthorisedself._(
-                            "You do not have permission to store queries")
+                        raise exceptions.Unauthorised(self._(
+                            "You do not have permission to store queries"))
                     qid = self.db.query.create(name=queryname,
                         klass=self.classname, url=url)
             else:
@@ -193,15 +200,15 @@ class SearchAction(Action):
                         if old_queryname != self.db.query.get(qid, 'name'):
                             continue
                         if not self.hasPermission('Edit', 'query', itemid=qid):
-                            raise exceptions.Unauthorisedself._(
-                            "You do not have permission to edit queries")
+                            raise exceptions.Unauthorised(self._(
+                            "You do not have permission to edit queries"))
                         self.db.query.set(qid, klass=self.classname,
                             url=url, name=queryname)
                 else:
                     # create a query
                     if not self.hasPermission('Create', 'query'):
-                        raise exceptions.Unauthorisedself._(
-                            "You do not have permission to store queries")
+                        raise exceptions.Unauthorised(self._(
+                            "You do not have permission to store queries"))
                     qid = self.db.query.create(name=queryname,
                         klass=self.classname, url=url, private_for=uid)
 
@@ -217,7 +224,7 @@ class SearchAction(Action):
     def fakeFilterVars(self):
         """Add a faked :filter form variable for each filtering prop."""
         cls = self.db.classes[self.classname]
-        for key in self.form.keys():
+        for key in self.form:
             prop = cls.get_transitive_prop(key)
             if not prop:
                 continue
@@ -234,7 +241,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:
@@ -258,7 +265,7 @@ class SearchAction(Action):
 
     def getFromForm(self, name):
         for key in ('@' + name, ':' + name):
-            if self.form.has_key(key):
+            if key in self.form:
                 return self.form[key].value.strip()
         return ''
 
@@ -275,15 +282,22 @@ 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 = list(cl.getprops(protected=0))
+
+        # 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)
+        rows = io_.StringIO(self.form['rows'].value)
         reader = csv.reader(rows)
         found = {}
         line = 0
@@ -294,25 +308,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
@@ -321,7 +353,7 @@ class EditCSVAction(Action):
                     if isinstance(prop, hyperdb.Multilink):
                         value = value.split(':')
                     elif isinstance(prop, hyperdb.Password):
-                        value = password.Password(value)
+                        value = password.Password(value, config=self.db.config)
                     elif isinstance(prop, hyperdb.Interval):
                         value = date.Interval(value)
                     elif isinstance(prop, hyperdb.Date):
@@ -341,15 +373,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 itemid not in found:
+                # 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()
@@ -368,12 +406,12 @@ class EditCommon(Action):
         links = {}
         for cn, nodeid, propname, vlist in all_links:
             numeric_id = int (nodeid or 0)
-            if not (numeric_id > 0 or all_props.has_key((cn, nodeid))):
+            if not (numeric_id > 0 or (cn, nodeid) in all_props):
                 # link item to link to doesn't (and won't) exist
                 continue
 
             for value in vlist:
-                if not all_props.has_key(value):
+                if value not in all_props:
                     # link item to link to doesn't (and won't) exist
                     continue
                 deps.setdefault((cn, nodeid), []).append(value)
@@ -385,19 +423,19 @@ class EditCommon(Action):
         # loop detection
         change = 0
         while len(all_props) != len(done):
-            for needed in all_props.keys():
-                if done.has_key(needed):
+            for needed in all_props:
+                if needed in done:
                     continue
                 tlist = deps.get(needed, [])
                 for target in tlist:
-                    if not done.has_key(target):
+                    if target not in done:
                         break
                 else:
                     done[needed] = 1
                     order.append(needed)
                     change = 1
             if not change:
-                raise ValueError, 'linking must not loop!'
+                raise ValueError('linking must not loop!')
 
         # now, edit / create
         m = []
@@ -411,7 +449,7 @@ class EditCommon(Action):
 
                     # and some nice feedback for the user
                     if props:
-                        info = ', '.join(map(self._, props.keys()))
+                        info = ', '.join(map(self._, props))
                         m.append(
                             self._('%(class)s %(id)s %(properties)s edited ok')
                             % {'class':cn, 'id':nodeid, 'properties':info})
@@ -432,18 +470,18 @@ class EditCommon(Action):
                         % {'class':cn, 'id':newid})
 
             # fill in new ids in links
-            if links.has_key(needed):
+            if needed in links:
                 for linkcn, linkid, linkprop in links[needed]:
                     props = all_props[(linkcn, linkid)]
                     cl = self.db.classes[linkcn]
                     propdef = cl.getprops()[linkprop]
-                    if not props.has_key(linkprop):
+                    if linkprop not in props:
                         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 +489,7 @@ class EditCommon(Action):
                                 existing.append(nodeid)
                                 props[linkprop] = existing
                             else:
-                                props[linkprop] = newid
+                                props[linkprop] = nodeid
 
         return '<br>'.join(m)
 
@@ -459,9 +497,9 @@ class EditCommon(Action):
         """Change the node based on the contents of the form."""
         # check for permission
         if not self.editItemPermission(props, classname=cn, itemid=nodeid):
-            raise exceptions.Unauthorisedself._(
+            raise exceptions.Unauthorised(self._(
                 'You do not have permission to edit %(class)s'
-            ) % {'class': cn}
+            ) % {'class': cn})
 
         # make the changes
         cl = self.db.classes[cn]
@@ -471,9 +509,9 @@ class EditCommon(Action):
         """Create a node based on the contents of the form."""
         # check for permission
         if not self.newItemPermission(props, classname=cn):
-            raise exceptions.Unauthorisedself._(
+            raise exceptions.Unauthorised(self._(
                 'You do not have permission to create %(class)s'
-            ) % {'class': cn}
+            ) % {'class': cn})
 
         # create the node and return its id
         cl = self.db.classes[cn]
@@ -486,26 +524,20 @@ class EditCommon(Action):
 
     _cn_marker = []
     def editItemPermission(self, props, classname=_cn_marker, itemid=None):
-        """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 exceptions.Unauthorised, self._(
-                    "You do not have permission to edit user roles")
-            if self.isEditingSelf():
-                return 1
+        """Determine whether the user has permission to edit this item."""
         if itemid is None:
             itemid = self.nodeid
         if classname is self._cn_marker:
             classname = self.classname
-        if self.hasPermission('Edit', itemid=itemid, classname=classname):
-            return 1
-        return 0
+        # 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):
+                return 0
+        # Since the user has permission to edit all of the properties,
+        # the edit is OK.
+        return 1
 
     def newItemPermission(self, props, classname=None):
         """Determine whether the user has permission to create this item.
@@ -513,15 +545,26 @@ 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):
-        if self.form.has_key(':lastactivity'):
+        if ':lastactivity' in self.form:
             d = date.Date(self.form[':lastactivity'].value)
-        elif self.form.has_key('@lastactivity'):
+        elif '@lastactivity' in self.form:
             d = date.Date(self.form['@lastactivity'].value)
         else:
             return None
@@ -541,7 +584,7 @@ class EditItemAction(EditCommon):
             props, links = self.client.parsePropsFromForm()
             key = (self.classname, self.nodeid)
             # we really only collide for direct prop edit conflicts
-            return props[key].keys()
+            return list(props[key])
         else:
             return []
 
@@ -559,6 +602,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())
@@ -587,12 +634,12 @@ class EditItemAction(EditCommon):
         # we will want to include index-page args in this URL too
         if self.nodeid is not None:
             url += self.nodeid
-        url += '?@ok_message=%s&@template=%s'%(urllib.quote(message),
-            urllib.quote(self.template))
+        url += '?@ok_message=%s&@template=%s'%(urllib_.quote(message),
+            urllib_.quote(self.template))
         if self.nodeid is None:
             req = templating.HTMLRequest(self.client)
             url += '&' + req.indexargs_url('', {})[1:]
-        raise exceptions.Redirect, url
+        raise exceptions.Redirect(url)
 
 class NewItemAction(EditCommon):
     def handle(self):
@@ -601,6 +648,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)
@@ -623,9 +674,9 @@ class NewItemAction(EditCommon):
         self.db.commit()
 
         # redirect to the new item's page
-        raise exceptions.Redirect'%s%s%s?@ok_message=%s&@template=%s' % (
-            self.base, self.classname, self.nodeid, urllib.quote(messages),
-            urllib.quote(self.template))
+        raise exceptions.Redirect('%s%s%s?@ok_message=%s&@template=%s' % (
+            self.base, self.classname, self.nodeid, urllib_.quote(messages),
+            urllib_.quote(self.template)))
 
 class PassResetAction(Action):
     def handle(self):
@@ -636,7 +687,7 @@ class PassResetAction(Action):
 
         """
         otks = self.db.getOTKManager()
-        if self.form.has_key('otk'):
+        if 'otk' in self.form:
             # pull the rego information out of the otk database
             otk = self.form['otk'].value
             uid = otks.get(otk, 'uid', default=None)
@@ -660,7 +711,7 @@ class PassResetAction(Action):
             # XXX we need to make the "default" page be able to display errors!
             try:
                 # set the password
-                cl.set(uid, password=password.Password(newpw))
+                cl.set(uid, password=password.Password(newpw, config=self.db.config))
                 # clear the props from the otk database
                 otks.destroy(otk)
                 self.db.commit()
@@ -688,7 +739,7 @@ Your password is now: %(password)s
             return
 
         # no OTK, so now figure the user
-        if self.form.has_key('username'):
+        if 'username' in self.form:
             name = self.form['username'].value
             try:
                 uid = self.db.user.lookup(name)
@@ -696,7 +747,7 @@ Your password is now: %(password)s
                 self.client.error_message.append(self._('Unknown username'))
                 return
             address = self.db.user.get(uid, 'address')
-        elif self.form.has_key('address'):
+        elif 'address' in self.form:
             address = self.form['address'].value
             uid = uidFromAddress(self.db, ('', address), create=0)
             if not uid:
@@ -747,7 +798,7 @@ class RegoCommon(Action):
         # nice message
         message = self._('You are now registered, welcome!')
         url = '%suser%s?@ok_message=%s'%(self.base, self.userid,
-            urllib.quote(message))
+            urllib_.quote(message))
 
         # redirect to the user's page (but not 302, as some email clients seem
         # to want to reload the page, or something)
@@ -770,7 +821,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
@@ -778,6 +829,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)
@@ -786,12 +841,6 @@ class RegisterAction(RegoCommon, EditCommon):
                 % str(message))
             return
 
-        # registration isn't allowed to supply roles
-        user_props = props[('user', None)]
-        if user_props.has_key('roles'):
-            raise exceptions.Unauthorised, self._(
-                "It is not permitted to supply roles at registration.")
-
         # skip the confirmation step?
         if self.db.config['INSTANT_REGISTRATION']:
             # handle the create now
@@ -816,7 +865,8 @@ class RegisterAction(RegoCommon, EditCommon):
             return self.finishRego()
 
         # generate the one-time-key and store the props for later
-        for propname, proptype in self.db.user.getprops().items():
+        user_props = props[('user', None)]
+        for propname, proptype in self.db.user.getprops().iteritems():
             value = user_props.get(propname, None)
             if value is None:
                 pass
@@ -867,7 +917,18 @@ reply's additional "Re:" is ok),
         self.db.commit()
 
         # redirect to the "you're almost there" page
-        raise exceptions.Redirect, '%suser?@template=rego_progress'%self.base
+        raise exceptions.Redirect('%suser?@template=rego_progress'%self.base)
+
+    def newItemPermission(self, props, classname=None):
+        """Just check the "Register" permission.
+        """
+        # registration isn't allowed to supply roles
+        if 'roles' in props:
+            raise exceptions.Unauthorised(self._(
+                "It is not permitted to supply roles at registration."))
+
+        # technically already checked, but here for clarity
+        return self.hasPermission('Register', classname=classname)
 
 class LogoutAction(Action):
     def handle(self):
@@ -892,14 +953,18 @@ 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'):
+        if '__login_name' not in self.form:
             self.client.error_message.append(self._('Username required'))
             return
 
         # get the login info
         self.client.user = self.form['__login_name'].value
-        if self.form.has_key('__login_password'):
+        if '__login_password' in self.form:
             password = self.form['__login_password'].value
         else:
             password = ''
@@ -916,36 +981,43 @@ class LoginAction(Action):
 
         # save user in session
         self.client.session_api.set(user=self.client.user)
-        if self.form.has_key('remember'):
+        if 'remember' in self.form:
             self.client.session_api.update(set_cookie=True, expire=24*3600*365)
 
         # If we came from someplace, go back there
-        if self.form.has_key('__came_from'):
-            raise exceptions.Redirect, self.form['__came_from'].value
+        if '__came_from' in self.form:
+            raise exceptions.Redirect(self.form['__came_from'].value)
 
     def verifyLogin(self, username, password):
         # make sure the user exists
         try:
             self.client.userid = self.db.user.lookup(username)
         except KeyError:
-            raise exceptions.LoginError, self._('Invalid login')
+            raise exceptions.LoginError(self._('Invalid login'))
 
         # verify the password
         if not self.verifyPassword(self.client.userid, password):
-            raise exceptions.LoginError, self._('Invalid login')
+            raise exceptions.LoginError(self._('Invalid login'))
 
         # Determine whether the user has permission to log in.
         # Base behaviour is to check the user has "Web Access".
         if not self.hasPermission("Web Access"):
-            raise exceptions.LoginErrorself._(
-                "You do not have permission to login")
+            raise exceptions.LoginError(self._(
+                "You do not have permission to login"))
 
-    def verifyPassword(self, userid, password):
-        '''Verify the password that the user has supplied'''
-        stored = self.db.user.get(userid, 'password')
-        if password == stored:
+    def verifyPassword(self, userid, givenpw):
+        '''Verify the password that the user has supplied.
+           Optionally migrate to new password scheme if configured
+        '''
+        db = self.db
+        stored = db.user.get(userid, 'password')
+        if givenpw == stored:
+            if db.config.WEB_MIGRATE_PASSWORDS and stored.needs_migration():
+                newpw = password.Password(givenpw, config=db.config)
+                db.user.set(userid, password=newpw)
+                db.commit()
             return 1
-        if not password and not stored:
+        if not givenpw and not stored:
             return 1
         return 0
 
@@ -991,8 +1063,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:
+            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 :