Code

Simple version of collision detection, with tests and a new generic template for...
[roundup.git] / roundup / cgi / actions.py
1 import re, cgi, StringIO, urllib, Cookie, time, random
3 from roundup import hyperdb, token, date, password, rcsv
4 from roundup.i18n import _
5 from roundup.cgi import templating
6 from roundup.cgi.exceptions import Redirect, Unauthorised
7 from roundup.mailgw import uidFromAddress
9 __all__ = ['Action', 'ShowAction', 'RetireAction', 'SearchAction',
10            'EditCSVAction', 'EditItemAction', 'PassResetAction',
11            'ConfRegoAction', 'RegisterAction', 'LoginAction', 'LogoutAction',
12            'NewItemAction']
14 # used by a couple of routines
15 chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
17 class Action:
18     def __init__(self, client):
19         self.client = client
20         self.form = client.form
21         self.db = client.db
22         self.nodeid = client.nodeid
23         self.template = client.template
24         self.classname = client.classname
25         self.userid = client.userid
26         self.base = client.base
27         self.user = client.user
28         
29     def handle(self):
30         """Execute the action specified by this object."""
31         raise NotImplementedError
33     def permission(self):
34         """Check whether the user has permission to execute this action.
36         True by default.
37         """
38         return 1
40 class ShowAction(Action):
41     def handle(self, typere=re.compile('[@:]type'),
42                numre=re.compile('[@:]number')):
43         """Show a node of a particular class/id."""
44         t = n = ''
45         for key in self.form.keys():
46             if typere.match(key):
47                 t = self.form[key].value.strip()
48             elif numre.match(key):
49                 n = self.form[key].value.strip()
50         if not t:
51             raise ValueError, 'Invalid %s number'%t
52         url = '%s%s%s'%(self.db.config.TRACKER_WEB, t, n)
53         raise Redirect, url
55 class RetireAction(Action):
56     def handle(self):
57         """Retire the context item."""
58         # if we want to view the index template now, then unset the nodeid
59         # context info (a special-case for retire actions on the index page)
60         nodeid = self.nodeid
61         if self.template == 'index':
62             self.client.nodeid = None
64         # generic edit is per-class only
65         if not self.permission():
66             raise Unauthorised, _('You do not have permission to retire %s' %
67                                   self.classname)
69         # make sure we don't try to retire admin or anonymous
70         if self.classname == 'user' and \
71                 self.db.user.get(nodeid, 'username') in ('admin', 'anonymous'):
72             raise ValueError, _('You may not retire the admin or anonymous user')
74         # do the retire
75         self.db.getclass(self.classname).retire(nodeid)
76         self.db.commit()
78         self.client.ok_message.append(
79             _('%(classname)s %(itemid)s has been retired')%{
80                 'classname': self.classname.capitalize(), 'itemid': nodeid})
82     def permission(self):
83         """Determine whether the user has permission to retire this class.
85         Base behaviour is to check the user can edit this class.
86         """ 
87         return self.db.security.hasPermission('Edit', self.client.userid,
88                                               self.client.classname)
90 class SearchAction(Action):
91     def handle(self, wcre=re.compile(r'[\s,]+')):
92         """Mangle some of the form variables.
94         Set the form ":filter" variable based on the values of the filter
95         variables - if they're set to anything other than "dontcare" then add
96         them to :filter.
98         Handle the ":queryname" variable and save off the query to the user's
99         query list.
101         Split any String query values on whitespace and comma.
103         """
104         # generic edit is per-class only
105         if not self.permission():
106             raise Unauthorised, _('You do not have permission to search %s' %
107                                   self.classname)
109         self.fakeFilterVars()
110         queryname = self.getQueryName()        
112         # handle saving the query params
113         if queryname:
114             # parse the environment and figure what the query _is_
115             req = templating.HTMLRequest(self.client)
117             # The [1:] strips off the '?' character, it isn't part of the
118             # query string.
119             url = req.indexargs_href('', {})[1:]
121             # handle editing an existing query
122             try:
123                 qid = self.db.query.lookup(queryname)
124                 self.db.query.set(qid, klass=self.classname, url=url)
125             except KeyError:
126                 # create a query
127                 qid = self.db.query.create(name=queryname,
128                     klass=self.classname, url=url)
130             # and add it to the user's query multilink
131             queries = self.db.user.get(self.userid, 'queries')
132             queries.append(qid)
133             self.db.user.set(self.userid, queries=queries)
135             # commit the query change to the database
136             self.db.commit()
138     def fakeFilterVars(self):
139         """Add a faked :filter form variable for each filtering prop."""
140         props = self.db.classes[self.classname].getprops()
141         for key in self.form.keys():
142             if not props.has_key(key):
143                 continue
144             if isinstance(self.form[key], type([])):
145                 # search for at least one entry which is not empty
146                 for minifield in self.form[key]:
147                     if minifield.value:
148                         break
149                 else:
150                     continue
151             else:
152                 if not self.form[key].value:
153                     continue
154                 if isinstance(props[key], hyperdb.String):
155                     v = self.form[key].value
156                     l = token.token_split(v)
157                     if len(l) > 1 or l[0] != v:
158                         self.form.value.remove(self.form[key])
159                         # replace the single value with the split list
160                         for v in l:
161                             self.form.value.append(cgi.MiniFieldStorage(key, v))
162         
163             self.form.value.append(cgi.MiniFieldStorage('@filter', key))
165     FV_QUERYNAME = re.compile(r'[@:]queryname')
166     def getQueryName(self):
167         for key in self.form.keys():
168             if self.FV_QUERYNAME.match(key):
169                 return self.form[key].value.strip()
170         return ''
171         
172     def permission(self):
173         return self.db.security.hasPermission('View', self.client.userid,
174                                               self.client.classname)
176 class EditCSVAction(Action):
177     def handle(self):
178         """Performs an edit of all of a class' items in one go.
180         The "rows" CGI var defines the CSV-formatted entries for the class. New
181         nodes are identified by the ID 'X' (or any other non-existent ID) and
182         removed lines are retired.
184         """
185         # this is per-class only
186         if not self.permission():
187             self.client.error_message.append(
188                  _('You do not have permission to edit %s' %self.classname))
189             return
191         # get the CSV module
192         if rcsv.error:
193             self.client.error_message.append(_(rcsv.error))
194             return
196         cl = self.db.classes[self.classname]
197         idlessprops = cl.getprops(protected=0).keys()
198         idlessprops.sort()
199         props = ['id'] + idlessprops
201         # do the edit
202         rows = StringIO.StringIO(self.form['rows'].value)
203         reader = rcsv.reader(rows, rcsv.comma_separated)
204         found = {}
205         line = 0
206         for values in reader:
207             line += 1
208             if line == 1: continue
209             # skip property names header
210             if values == props:
211                 continue
213             # extract the nodeid
214             nodeid, values = values[0], values[1:]
215             found[nodeid] = 1
217             # see if the node exists
218             if nodeid in ('x', 'X') or not cl.hasnode(nodeid):
219                 exists = 0
220             else:
221                 exists = 1
223             # confirm correct weight
224             if len(idlessprops) != len(values):
225                 self.client.error_message.append(
226                     _('Not enough values on line %(line)s')%{'line':line})
227                 return
229             # extract the new values
230             d = {}
231             for name, value in zip(idlessprops, values):
232                 prop = cl.properties[name]
233                 value = value.strip()
234                 # only add the property if it has a value
235                 if value:
236                     # if it's a multilink, split it
237                     if isinstance(prop, hyperdb.Multilink):
238                         value = value.split(':')
239                     elif isinstance(prop, hyperdb.Password):
240                         value = password.Password(value)
241                     elif isinstance(prop, hyperdb.Interval):
242                         value = date.Interval(value)
243                     elif isinstance(prop, hyperdb.Date):
244                         value = date.Date(value)
245                     elif isinstance(prop, hyperdb.Boolean):
246                         value = value.lower() in ('yes', 'true', 'on', '1')
247                     elif isinstance(prop, hyperdb.Number):
248                         value = float(value)
249                     d[name] = value
250                 elif exists:
251                     # nuke the existing value
252                     if isinstance(prop, hyperdb.Multilink):
253                         d[name] = []
254                     else:
255                         d[name] = None
257             # perform the edit
258             if exists:
259                 # edit existing
260                 cl.set(nodeid, **d)
261             else:
262                 # new node
263                 found[cl.create(**d)] = 1
265         # retire the removed entries
266         for nodeid in cl.list():
267             if not found.has_key(nodeid):
268                 cl.retire(nodeid)
270         # all OK
271         self.db.commit()
273         self.client.ok_message.append(_('Items edited OK'))
275     def permission(self):
276         return self.db.security.hasPermission('Edit', self.client.userid,
277                                               self.client.classname)
279 class _EditAction(Action):
280     def editItemPermission(self, props):
281         """Determine whether the user has permission to edit this item.
283         Base behaviour is to check the user can edit this class. If we're
284         editing the"user" class, users are allowed to edit their own details.
285         Unless it's the "roles" property, which requires the special Permission
286         "Web Roles".
287         """
288         # if this is a user node and the user is editing their own node, then
289         # we're OK
290         has = self.db.security.hasPermission
291         if self.classname == 'user':
292             # reject if someone's trying to edit "roles" and doesn't have the
293             # right permission.
294             if props.has_key('roles') and not has('Web Roles', self.userid,
295                     'user'):
296                 return 0
297             # if the item being edited is the current user, we're ok
298             if (self.nodeid == self.userid
299                 and self.db.user.get(self.nodeid, 'username') != 'anonymous'):
300                 return 1
301         if self.db.security.hasPermission('Edit', self.userid, self.classname):
302             return 1
303         return 0
305     def newItemPermission(self, props):
306         """Determine whether the user has permission to create (edit) this item.
308         Base behaviour is to check the user can edit this class. No additional
309         property checks are made. Additionally, new user items may be created
310         if the user has the "Web Registration" Permission.
312         """
313         has = self.db.security.hasPermission
314         if self.classname == 'user' and has('Web Registration', self.userid,
315                 'user'):
316             return 1
317         if has('Edit', self.userid, self.classname):
318             return 1
319         return 0
321     #
322     #  Utility methods for editing
323     #
324     def _editnodes(self, all_props, all_links, newids=None):
325         ''' Use the props in all_props to perform edit and creation, then
326             use the link specs in all_links to do linking.
327         '''
328         # figure dependencies and re-work links
329         deps = {}
330         links = {}
331         for cn, nodeid, propname, vlist in all_links:
332             if not all_props.has_key((cn, nodeid)):
333                 # link item to link to doesn't (and won't) exist
334                 continue
335             for value in vlist:
336                 if not all_props.has_key(value):
337                     # link item to link to doesn't (and won't) exist
338                     continue
339                 deps.setdefault((cn, nodeid), []).append(value)
340                 links.setdefault(value, []).append((cn, nodeid, propname))
342         # figure chained dependencies ordering
343         order = []
344         done = {}
345         # loop detection
346         change = 0
347         while len(all_props) != len(done):
348             for needed in all_props.keys():
349                 if done.has_key(needed):
350                     continue
351                 tlist = deps.get(needed, [])
352                 for target in tlist:
353                     if not done.has_key(target):
354                         break
355                 else:
356                     done[needed] = 1
357                     order.append(needed)
358                     change = 1
359             if not change:
360                 raise ValueError, 'linking must not loop!'
362         # now, edit / create
363         m = []
364         for needed in order:
365             props = all_props[needed]
366             if not props:
367                 # nothing to do
368                 continue
369             cn, nodeid = needed
371             if nodeid is not None and int(nodeid) > 0:
372                 # make changes to the node
373                 props = self._changenode(cn, nodeid, props)
375                 # and some nice feedback for the user
376                 if props:
377                     info = ', '.join(props.keys())
378                     m.append('%s %s %s edited ok'%(cn, nodeid, info))
379                 else:
380                     m.append('%s %s - nothing changed'%(cn, nodeid))
381             else:
382                 assert props
384                 # make a new node
385                 newid = self._createnode(cn, props)
386                 if nodeid is None:
387                     self.nodeid = newid
388                 nodeid = newid
390                 # and some nice feedback for the user
391                 m.append('%s %s created'%(cn, newid))
393             # fill in new ids in links
394             if links.has_key(needed):
395                 for linkcn, linkid, linkprop in links[needed]:
396                     props = all_props[(linkcn, linkid)]
397                     cl = self.db.classes[linkcn]
398                     propdef = cl.getprops()[linkprop]
399                     if not props.has_key(linkprop):
400                         if linkid is None or linkid.startswith('-'):
401                             # linking to a new item
402                             if isinstance(propdef, hyperdb.Multilink):
403                                 props[linkprop] = [newid]
404                             else:
405                                 props[linkprop] = newid
406                         else:
407                             # linking to an existing item
408                             if isinstance(propdef, hyperdb.Multilink):
409                                 existing = cl.get(linkid, linkprop)[:]
410                                 existing.append(nodeid)
411                                 props[linkprop] = existing
412                             else:
413                                 props[linkprop] = newid
415         return '<br>'.join(m)
417     def _changenode(self, cn, nodeid, props):
418         """Change the node based on the contents of the form."""
419         # check for permission
420         if not self.editItemPermission(props):
421             raise Unauthorised, 'You do not have permission to edit %s'%cn
423         # make the changes
424         cl = self.db.classes[cn]
425         return cl.set(nodeid, **props)
427     def _createnode(self, cn, props):
428         """Create a node based on the contents of the form."""
429         # check for permission
430         if not self.newItemPermission(props):
431             raise Unauthorised, 'You do not have permission to create %s'%cn
433         # create the node and return its id
434         cl = self.db.classes[cn]
435         return cl.create(**props)
437 class EditItemAction(_EditAction):
438     def lastUserActivity(self):
439         if self.form.has_key(':lastactivity'):
440             return date.Date(self.form[':lastactivity'].value)
441         elif self.form.has_key('@lastactivity'):
442             return date.Date(self.form['@lastactivity'].value)
443         else:
444             return None
446     def lastNodeActivity(self):
447         cl = getattr(self.client.db, self.classname)
448         return cl.get(self.nodeid, 'activity')
450     def detectCollision(self, userActivity, nodeActivity):
451         # Result from lastUserActivity may be None. If it is, assume there's no
452         # conflict, or at least not one we can detect.
453         if userActivity:
454             return userActivity < nodeActivity
456     def handleCollision(self):
457         self.client.template = 'collision'
458     
459     def handle(self):
460         """Perform an edit of an item in the database.
462         See parsePropsFromForm and _editnodes for special variables.
463         
464         """
465         if self.detectCollision(self.lastUserActivity(), self.lastNodeActivity()):
466             self.handleCollision()
467             return
469         props, links = self.client.parsePropsFromForm()
471         # handle the props
472         try:
473             message = self._editnodes(props, links)
474         except (ValueError, KeyError, IndexError), message:
475             self.client.error_message.append(_('Apply Error: ') + str(message))
476             return
478         # commit now that all the tricky stuff is done
479         self.db.commit()
481         # redirect to the item's edit page
482         # redirect to finish off
483         url = self.base + self.classname
484         # note that this action might have been called by an index page, so
485         # we will want to include index-page args in this URL too
486         if self.nodeid is not None:
487             url += self.nodeid
488         url += '?@ok_message=%s&@template=%s'%(urllib.quote(message),
489             urllib.quote(self.template))
490         if self.nodeid is None:
491             req = templating.HTMLRequest(self)
492             url += '&' + req.indexargs_href('', {})[1:]
493         raise Redirect, url
494     
495 class NewItemAction(_EditAction):
496     def handle(self):
497         ''' Add a new item to the database.
499             This follows the same form as the EditItemAction, with the same
500             special form values.
501         '''
502         # parse the props from the form
503         try:
504             props, links = self.client.parsePropsFromForm(create=True)
505         except (ValueError, KeyError), message:
506             self.error_message.append(_('Error: ') + str(message))
507             return
509         # handle the props - edit or create
510         try:
511             # when it hits the None element, it'll set self.nodeid
512             messages = self._editnodes(props, links)
514         except (ValueError, KeyError, IndexError), message:
515             # these errors might just be indicative of user dumbness
516             self.error_message.append(_('Error: ') + str(message))
517             return
519         # commit now that all the tricky stuff is done
520         self.db.commit()
522         # redirect to the new item's page
523         raise Redirect, '%s%s%s?@ok_message=%s&@template=%s'%(self.base,
524             self.classname, self.nodeid, urllib.quote(messages),
525             urllib.quote(self.template))
526         
527 class PassResetAction(Action):
528     def handle(self):
529         """Handle password reset requests.
530     
531         Presence of either "name" or "address" generates email. Presence of
532         "otk" performs the reset.
533     
534         """
535         if self.form.has_key('otk'):
536             # pull the rego information out of the otk database
537             otk = self.form['otk'].value
538             uid = self.db.otks.get(otk, 'uid')
539             if uid is None:
540                 self.client.error_message.append("""Invalid One Time Key!
541 (a Mozilla bug may cause this message to show up erroneously,
542  please check your email)""")
543                 return
545             # re-open the database as "admin"
546             if self.user != 'admin':
547                 self.client.opendb('admin')
548                 self.db = self.client.db
550             # change the password
551             newpw = password.generatePassword()
553             cl = self.db.user
554 # XXX we need to make the "default" page be able to display errors!
555             try:
556                 # set the password
557                 cl.set(uid, password=password.Password(newpw))
558                 # clear the props from the otk database
559                 self.db.otks.destroy(otk)
560                 self.db.commit()
561             except (ValueError, KeyError), message:
562                 self.client.error_message.append(str(message))
563                 return
565             # user info
566             address = self.db.user.get(uid, 'address')
567             name = self.db.user.get(uid, 'username')
569             # send the email
570             tracker_name = self.db.config.TRACKER_NAME
571             subject = 'Password reset for %s'%tracker_name
572             body = '''
573 The password has been reset for username "%(name)s".
575 Your password is now: %(password)s
576 '''%{'name': name, 'password': newpw}
577             if not self.client.standard_message([address], subject, body):
578                 return
580             self.client.ok_message.append('Password reset and email sent to %s' %
581                                           address)
582             return
584         # no OTK, so now figure the user
585         if self.form.has_key('username'):
586             name = self.form['username'].value
587             try:
588                 uid = self.db.user.lookup(name)
589             except KeyError:
590                 self.client.error_message.append('Unknown username')
591                 return
592             address = self.db.user.get(uid, 'address')
593         elif self.form.has_key('address'):
594             address = self.form['address'].value
595             uid = uidFromAddress(self.db, ('', address), create=0)
596             if not uid:
597                 self.client.error_message.append('Unknown email address')
598                 return
599             name = self.db.user.get(uid, 'username')
600         else:
601             self.client.error_message.append('You need to specify a username '
602                 'or address')
603             return
605         # generate the one-time-key and store the props for later
606         otk = ''.join([random.choice(chars) for x in range(32)])
607         self.db.otks.set(otk, uid=uid, __time=time.time())
609         # send the email
610         tracker_name = self.db.config.TRACKER_NAME
611         subject = 'Confirm reset of password for %s'%tracker_name
612         body = '''
613 Someone, perhaps you, has requested that the password be changed for your
614 username, "%(name)s". If you wish to proceed with the change, please follow
615 the link below:
617   %(url)suser?@template=forgotten&@action=passrst&otk=%(otk)s
619 You should then receive another email with the new password.
620 '''%{'name': name, 'tracker': tracker_name, 'url': self.base, 'otk': otk}
621         if not self.client.standard_message([address], subject, body):
622             return
624         self.client.ok_message.append('Email sent to %s'%address)
626 class ConfRegoAction(Action):
627     def handle(self):
628         """Grab the OTK, use it to load up the new user details."""
629         try:
630             # pull the rego information out of the otk database
631             self.userid = self.db.confirm_registration(self.form['otk'].value)
632         except (ValueError, KeyError), message:
633             # XXX: we need to make the "default" page be able to display errors!
634             self.client.error_message.append(str(message))
635             return
636         
637         # log the new user in
638         self.client.user = self.db.user.get(self.userid, 'username')
639         # re-open the database for real, using the user
640         self.client.opendb(self.client.user)
641         self.db = client.db
643         # if we have a session, update it
644         if hasattr(self, 'session'):
645             self.db.sessions.set(self.session, user=self.user,
646                 last_use=time.time())
647         else:
648             # new session cookie
649             self.client.set_cookie(self.user)
651         # nice message
652         message = _('You are now registered, welcome!')
654         # redirect to the user's page
655         raise Redirect, '%suser%s?@ok_message=%s'%(self.base,
656                                                    self.userid, urllib.quote(message))
658 class RegisterAction(Action):
659     def handle(self):
660         """Attempt to create a new user based on the contents of the form
661         and then set the cookie.
663         Return 1 on successful login.
664         """
665         props = self.client.parsePropsFromForm()[0][('user', None)]
667         # make sure we're allowed to register
668         if not self.permission(props):
669             raise Unauthorised, _("You do not have permission to register")
671         try:
672             self.db.user.lookup(props['username'])
673             self.client.error_message.append('Error: A user with the username "%s" '
674                 'already exists'%props['username'])
675             return
676         except KeyError:
677             pass
679         # generate the one-time-key and store the props for later
680         otk = ''.join([random.choice(chars) for x in range(32)])
681         for propname, proptype in self.db.user.getprops().items():
682             value = props.get(propname, None)
683             if value is None:
684                 pass
685             elif isinstance(proptype, hyperdb.Date):
686                 props[propname] = str(value)
687             elif isinstance(proptype, hyperdb.Interval):
688                 props[propname] = str(value)
689             elif isinstance(proptype, hyperdb.Password):
690                 props[propname] = str(value)
691         props['__time'] = time.time()
692         self.db.otks.set(otk, **props)
694         # send the email
695         tracker_name = self.db.config.TRACKER_NAME
696         tracker_email = self.db.config.TRACKER_EMAIL
697         subject = 'Complete your registration to %s -- key %s' % (tracker_name,
698                                                                   otk)
699         body = """To complete your registration of the user "%(name)s" with
700 %(tracker)s, please do one of the following:
702 - send a reply to %(tracker_email)s and maintain the subject line as is (the
703 reply's additional "Re:" is ok),
705 - or visit the following URL:
707 %(url)s?@action=confrego&otk=%(otk)s
708 """ % {'name': props['username'], 'tracker': tracker_name, 'url': self.base,
709         'otk': otk, 'tracker_email': tracker_email}
710         if not self.client.standard_message([props['address']], subject, body,
711         tracker_email):
712             return
714         # commit changes to the database
715         self.db.commit()
717         # redirect to the "you're almost there" page
718         raise Redirect, '%suser?@template=rego_progress'%self.base
720     def permission(self, props):
721         """Determine whether the user has permission to register
722         
723         Base behaviour is to check the user has "Web Registration".
724         
725         """
726         # registration isn't allowed to supply roles
727         if props.has_key('roles'):
728             return 0
729         if self.db.security.hasPermission('Web Registration', self.userid):
730             return 1
731         return 0
733 class LogoutAction(Action):
734     def handle(self):
735         """Make us really anonymous - nuke the cookie too."""
736         # log us out
737         self.client.make_user_anonymous()
739         # construct the logout cookie
740         now = Cookie._getdate()
741         self.client.additional_headers['Set-Cookie'] = \
742            '%s=deleted; Max-Age=0; expires=%s; Path=%s;'%(self.client.cookie_name,
743             now, self.client.cookie_path)
745         # Let the user know what's going on
746         self.client.ok_message.append(_('You are logged out'))
748 class LoginAction(Action):
749     def handle(self):
750         """Attempt to log a user in.
752         Sets up a session for the user which contains the login credentials.
754         """
755         # we need the username at a minimum
756         if not self.form.has_key('__login_name'):
757             self.client.error_message.append(_('Username required'))
758             return
760         # get the login info
761         self.client.user = self.form['__login_name'].value
762         if self.form.has_key('__login_password'):
763             password = self.form['__login_password'].value
764         else:
765             password = ''
767         # make sure the user exists
768         try:
769             self.client.userid = self.db.user.lookup(self.client.user)
770         except KeyError:
771             name = self.client.user
772             self.client.error_message.append(_('No such user "%(name)s"')%locals())
773             self.client.make_user_anonymous()
774             return
776         # verify the password
777         if not self.verifyPassword(self.client.userid, password):
778             self.client.make_user_anonymous()
779             self.client.error_message.append(_('Incorrect password'))
780             return
782         # make sure we're allowed to be here
783         if not self.permission():
784             self.client.make_user_anonymous()
785             self.client.error_message.append(_("You do not have permission to login"))
786             return
788         # now we're OK, re-open the database for real, using the user
789         self.client.opendb(self.client.user)
791         # set the session cookie
792         self.client.set_cookie(self.client.user)
794     def verifyPassword(self, userid, password):
795         ''' Verify the password that the user has supplied
796         '''
797         stored = self.db.user.get(self.client.userid, 'password')
798         if password == stored:
799             return 1
800         if not password and not stored:
801             return 1
802         return 0
804     def permission(self):
805         """Determine whether the user has permission to log in.
807         Base behaviour is to check the user has "Web Access".
809         """    
810         if not self.db.security.hasPermission('Web Access', self.client.userid):
811             return 0
812         return 1