chiark / gitweb /
userinfo/edituser implementation
authorRichard Kettlewell <rjk@greenend.org.uk>
Thu, 20 Dec 2007 18:31:37 +0000 (18:31 +0000)
committerRichard Kettlewell <rjk@greenend.org.uk>
Thu, 20 Dec 2007 18:31:37 +0000 (18:31 +0000)
20 files changed:
lib/Makefile.am
lib/client.c
lib/client.h
lib/cookies.c
lib/rights.c [new file with mode: 0644]
lib/rights.h [new file with mode: 0644]
lib/trackdb.c
lib/trackdb.h
python/disorder.py.in
server/api-server.c
server/dbupgrade.c
server/deadlock.c
server/disorderd.c
server/dump.c
server/play.c
server/rescan.c
server/server.c
server/state.c
server/stats.c
tests/user.py

index 7c792d8..1cbb7bf 100644 (file)
@@ -52,6 +52,7 @@ libdisorder_a_SOURCES=charset.c charset.h             \
        asprintf.c fprintf.c snprintf.c                 \
        queue.c queue.h                                 \
        regsub.c regsub.h                               \
+       rights.c rights.h                               \
        rtp.h                                           \
        selection.c selection.h                         \
        signame.c signame.h                             \
index e4e28e8..8184306 100644 (file)
@@ -50,6 +50,7 @@
 #include "addr.h"
 #include "authhash.h"
 #include "client-common.h"
+#include "rights.h"
 #include "trackdb.h"
 
 struct disorder_client {
@@ -659,6 +660,15 @@ int disorder_deluser(disorder_client *c, const char *user) {
   return disorder_simple(c, 0, "deluser", user, (char *)0);
 }
 
+int disorder_userinfo(disorder_client *c, const char *user, const char *key) {
+  return disorder_simple(c, 0, "userinfo", user, key, (char *)0);
+}
+
+int disorder_edituser(disorder_client *c, const char *user,
+                     const char *key, const char *value) {
+  return disorder_simple(c, 0, "edituser", user, key, value, (char *)0);
+}
+
 /*
 Local Variables:
 c-basic-offset:2
index ab93115..64f58b8 100644 (file)
@@ -191,6 +191,9 @@ int disorder_rtp_address(disorder_client *c, char **addressp, char **portp);
 int disorder_adduser(disorder_client *c,
                     const char *user, const char *password);
 int disorder_deluser(disorder_client *c, const char *user);
+int disorder_userinfo(disorder_client *c, const char *user, const char *key);
+int disorder_edituser(disorder_client *c, const char *user,
+                     const char *key, const char *value);
 
 #endif /* CLIENT_H */
 
index 666c6ce..cce6b1b 100644 (file)
@@ -40,6 +40,7 @@
 #include "mime.h"
 #include "configuration.h"
 #include "kvp.h"
+#include "rights.h"
 #include "trackdb.h"
 
 /** @brief Hash function used in signing HMAC */
diff --git a/lib/rights.c b/lib/rights.c
new file mode 100644 (file)
index 0000000..8d84743
--- /dev/null
@@ -0,0 +1,148 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2007 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+/** @file lib/rights.c
+ * @brief User rights
+ */
+
+#include <config.h>
+#include "types.h"
+
+#include <string.h>
+
+#include "mem.h"
+#include "log.h"
+#include "configuration.h"
+#include "rights.h"
+#include "vector.h"
+
+static const struct {
+  rights_type bit;
+  const char *name;
+} rights_names[] = {
+  { RIGHT_READ, "read" },
+  { RIGHT_PLAY, "play" },
+  { RIGHT_MOVE_ANY, "move any" },
+  { RIGHT_MOVE_MINE, "move mine" },
+  { RIGHT_MOVE_RANDOM, "move random" },
+  { RIGHT_REMOVE_ANY, "remove any" },
+  { RIGHT_REMOVE_MINE, "remove mine" },
+  { RIGHT_REMOVE_RANDOM, "remove random" },
+  { RIGHT_SCRATCH_ANY, "scratch any" },
+  { RIGHT_SCRATCH_MINE, "scratch mine" },
+  { RIGHT_SCRATCH_RANDOM, "scratch random" },
+  { RIGHT_VOLUME, "volume" },
+  { RIGHT_ADMIN, "admin" },
+  { RIGHT_RESCAN, "rescan" },
+  { RIGHT_REGISTER, "register" },
+  { RIGHT_USERINFO, "userinfo" },
+  { RIGHT_PREFS, "prefs" },
+  { RIGHT_GLOBAL_PREFS, "global prefs" }
+};
+#define NRIGHTS (sizeof rights_names / sizeof *rights_names)
+
+/** @brief Convert a rights word to a string */
+char *rights_string(rights_type r) {
+  struct dynstr d[1];
+  size_t n;
+
+  dynstr_init(d);
+  for(n = 0; n < NRIGHTS; ++n) {
+    if(r & rights_names[n].bit) {
+      if(d->nvec)
+        dynstr_append(d, ',');
+      dynstr_append_string(d, rights_names[n].name);
+    }
+  }
+  dynstr_terminate(d);
+  return d->vec;
+}
+
+/** @brief Compute default rights for a new user
+ * @return Default rights value
+ */
+rights_type default_rights(void) {
+  /* TODO get rights from config */
+  rights_type r = RIGHTS__MASK & ~(RIGHT_ADMIN|RIGHT_REGISTER
+                                   |RIGHT_MOVE__MASK
+                                   |RIGHT_SCRATCH__MASK
+                                   |RIGHT_REMOVE__MASK);
+  if(config->restrictions & RESTRICT_SCRATCH)
+    r |= RIGHT_SCRATCH_MINE|RIGHT_SCRATCH_RANDOM;
+  else
+    r |= RIGHT_SCRATCH_ANY;
+  if(!(config->restrictions & RESTRICT_MOVE))
+    r |= RIGHT_MOVE_ANY;
+  if(config->restrictions & RESTRICT_REMOVE)
+    r |= RIGHT_REMOVE_MINE;
+  else
+    r |= RIGHT_REMOVE_ANY;
+  return r;
+}
+
+/** @brief Parse a rights list
+ * @param s Rights list in string form
+ * @param rp Where to store rights, or NULL to just validate
+ * @return 0 on success, non-0 if @p s is not valid
+ */
+int parse_rights(const char *s, rights_type *rp) {
+  rights_type r = 0;
+  const char *t;
+  size_t n, l;
+
+  if(!*s) {
+    /* You can't have no rights */
+    error(0, "empty rights string");
+    return -1;
+  }
+  while(*s) {
+    t = strchr(s, ',');
+    if(!t)
+      t = s + strlen(s);
+    l = (size_t)(t - s);
+    if(l == 3 && !strncmp(s, "all", 3))
+      r = RIGHTS__MASK;
+    else {
+      for(n = 0; n < NRIGHTS; ++n)
+       if(strlen(rights_names[n].name) == l
+          && !strncmp(rights_names[n].name, s, l))
+         break;
+      if(n >= NRIGHTS) {
+       error(0, "unknown user right '%.*s'", (int)l, s);
+       return -1;
+      }
+      r |= rights_names[n].bit;
+    }
+    s = t;
+    if(*s == ',')
+      ++s;
+  }
+  if(rp)
+    *rp = r;
+  return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/lib/rights.h b/lib/rights.h
new file mode 100644 (file)
index 0000000..f4bf363
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2007 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+/** @file lib/rights.h
+ * @brief User rights
+ */
+
+#ifndef RIGHTS_H
+#define RIGHTS_H
+
+/** @brief User can perform read-only operations */
+#define RIGHT_READ            0x00000001
+
+/** @brief User can add tracks to the queue */
+#define RIGHT_PLAY            0x00000002
+
+/** @brief User can move any track */
+#define RIGHT_MOVE_ANY        0x00000004
+
+/** @brief User can move their own tracks */
+#define RIGHT_MOVE_MINE       0x00000008
+
+/** @brief User can move randomly chosen tracks */
+#define RIGHT_MOVE_RANDOM     0x00000010
+
+#define RIGHT_MOVE__MASK      0x0000001c
+
+/** @brief User can remove any track */
+#define RIGHT_REMOVE_ANY      0x00000020
+
+/** @brief User can remove their own tracks */
+#define RIGHT_REMOVE_MINE     0x00000040
+
+/** @brief User can remove randomly chosen tracks */
+#define RIGHT_REMOVE_RANDOM   0x00000080
+
+#define RIGHT_REMOVE__MASK    0x000000e0
+
+/** @brief User can scratch any track */
+#define RIGHT_SCRATCH_ANY     0x00000100
+
+/** @brief User can scratch their own tracks */
+#define RIGHT_SCRATCH_MINE    0x00000200
+
+/** @brief User can scratch randomly chosen tracks */
+#define RIGHT_SCRATCH_RANDOM  0x00000400
+
+#define RIGHT_SCRATCH__MASK   0x00000700
+
+/** @brief User can change the volume */
+#define RIGHT_VOLUME          0x00000800
+
+/** @brief User can perform admin operations */
+#define RIGHT_ADMIN           0x00001000
+
+/** @brief User can initiate a rescan */
+#define RIGHT_RESCAN          0x00002000
+
+/** @brief User can register new users */
+#define RIGHT_REGISTER        0x00004000
+
+/** @brief User can edit their own userinfo */
+#define RIGHT_USERINFO        0x00008000
+
+/** @brief User can modify track preferences */
+#define RIGHT_PREFS           0x00010000
+
+/** @brief User can modify global preferences */
+#define RIGHT_GLOBAL_PREFS    0x00020000
+
+/** @brief Current rights mask */
+#define RIGHTS__MASK          0x0003ffff
+
+/** @brief Unsigned type big enough for rights */
+typedef uint32_t rights_type;
+
+rights_type default_rights(void);
+char *rights_string(rights_type r);
+int parse_rights(const char *s, rights_type *rp);
+
+#endif /* RIGHTS_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index d3e6e40..90d9457 100644 (file)
@@ -49,6 +49,7 @@
 #include "kvp.h"
 #include "log.h"
 #include "vector.h"
+#include "rights.h"
 #include "trackdb.h"
 #include "configuration.h"
 #include "syscalls.h"
@@ -2423,69 +2424,6 @@ static int trusted(const char *user) {
   return n < config->trust.n;
 }
 
-static const struct {
-  rights_type bit;
-  const char *name;
-} rights_names[] = {
-  { RIGHT_READ, "read" },
-  { RIGHT_PLAY, "play" },
-  { RIGHT_MOVE_ANY, "move any" },
-  { RIGHT_MOVE_MINE, "move mine" },
-  { RIGHT_MOVE_RANDOM, "move random" },
-  { RIGHT_REMOVE_ANY, "remove any" },
-  { RIGHT_REMOVE_MINE, "remove mine" },
-  { RIGHT_REMOVE_RANDOM, "remove random" },
-  { RIGHT_SCRATCH_ANY, "scratch any" },
-  { RIGHT_SCRATCH_MINE, "scratch mine" },
-  { RIGHT_SCRATCH_RANDOM, "scratch random" },
-  { RIGHT_VOLUME, "volume" },
-  { RIGHT_ADMIN, "admin" },
-  { RIGHT_RESCAN, "rescan" },
-  { RIGHT_REGISTER, "register" },
-  { RIGHT_USERINFO, "userinfo" },
-  { RIGHT_PREFS, "prefs" },
-  { RIGHT_GLOBAL_PREFS, "global prefs" }
-};
-#define NRIGHTS (sizeof rights_names / sizeof *rights_names)
-
-/** @brief Convert a rights word to a string */
-static char *rights_string(rights_type r) {
-  struct dynstr d[1];
-  size_t n;
-
-  dynstr_init(d);
-  for(n = 0; n < NRIGHTS; ++n) {
-    if(r & rights_names[n].bit) {
-      if(d->nvec)
-        dynstr_append(d, ',');
-      dynstr_append_string(d, rights_names[n].name);
-    }
-  }
-  dynstr_terminate(d);
-  return d->vec;
-}
-
-/** @brief Compute default rights for a new user */
-rights_type default_rights(void) {
-  /* TODO get rights from config.  This is probably in the wrong place but it
-   * will do for now... */
-  rights_type r = RIGHTS__MASK & ~(RIGHT_ADMIN|RIGHT_REGISTER
-                                   |RIGHT_MOVE__MASK
-                                   |RIGHT_SCRATCH__MASK
-                                   |RIGHT_REMOVE__MASK);
-  if(config->restrictions & RESTRICT_SCRATCH)
-    r |= RIGHT_SCRATCH_MINE|RIGHT_SCRATCH_RANDOM;
-  else
-    r |= RIGHT_SCRATCH_ANY;
-  if(!(config->restrictions & RESTRICT_MOVE))
-    r |= RIGHT_MOVE_ANY;
-  if(config->restrictions & RESTRICT_REMOVE)
-    r |= RIGHT_REMOVE_MINE;
-  else
-    r |= RIGHT_REMOVE_ANY;
-  return r;
-}
-
 /** @brief Add a user */
 static int create_user(const char *user,
                        const char *password,
@@ -2580,6 +2518,9 @@ void trackdb_create_root(void) {
  * Only works if running as a user that can read the database!
  *
  * If the user exists but has no password, "" is returned.
+ *
+ * If the user was created with 'register' and has not yet been confirmed then
+ * NULL is still returned.
  */
 const char *trackdb_get_password(const char *user) {
   int e;
@@ -2589,6 +2530,8 @@ const char *trackdb_get_password(const char *user) {
   WITH_TRANSACTION(trackdb_getdata(trackdb_usersdb, user, &k, tid));
   if(e)
     return 0;
+  if(kvp_get(k, "confirmation"))
+    return 0;
   password = kvp_get(k, "password");
   return password ? password : "";
 }
@@ -2685,10 +2628,33 @@ int trackdb_edituserinfo(const char *user,
                          const char *key, const char *value) {
   int e;
 
+  if(!strcmp(key, "rights")) {
+    if(!value) {
+      error(0, "cannot remove 'rights' key from user '%s'", user);
+      return -1;
+    }
+    if(parse_rights(value, 0)) {
+      error(0, "invalid rights string");
+      return -1;
+    }
+  } else if(!strcmp(key, "email")) {
+    if(!strchr(value, '@')) {
+      error(0, "invalid email address '%s' for user '%s'", user, value);
+      return -1;
+    }
+  } else if(!strcmp(key, "created")) {
+    error(0, "cannot change creation date for user '%s'", user);
+    return -1;
+  } else if(strcmp(key, "password")
+            && !strcmp(key, "confirmation")) {
+    error(0, "unknown user info key '%s' for user '%s'", key, user);
+    return -1;
+  }
   WITH_TRANSACTION(trackdb_edituserinfo_tid(user, key, value, tid));
-  if(e)
+  if(e) {
+    error(0, "unknown user '%s'", user);
     return -1;
-  else
+  else
     return 0;
 }
 
index b317974..a136537 100644 (file)
@@ -29,72 +29,6 @@ extern const struct cache_type cache_files_type;
 extern unsigned long cache_files_hits, cache_files_misses;
 /* Cache entry type and tracking for regexp-based lookups */
 
-/** @brief User can perform read-only operations */
-#define RIGHT_READ            0x00000001
-
-/** @brief User can add tracks to the queue */
-#define RIGHT_PLAY            0x00000002
-
-/** @brief User can move any track */
-#define RIGHT_MOVE_ANY        0x00000004
-
-/** @brief User can move their own tracks */
-#define RIGHT_MOVE_MINE       0x00000008
-
-/** @brief User can move randomly chosen tracks */
-#define RIGHT_MOVE_RANDOM     0x00000010
-
-#define RIGHT_MOVE__MASK      0x0000001c
-
-/** @brief User can remove any track */
-#define RIGHT_REMOVE_ANY      0x00000020
-
-/** @brief User can remove their own tracks */
-#define RIGHT_REMOVE_MINE     0x00000040
-
-/** @brief User can remove randomly chosen tracks */
-#define RIGHT_REMOVE_RANDOM   0x00000080
-
-#define RIGHT_REMOVE__MASK    0x000000e0
-
-/** @brief User can scratch any track */
-#define RIGHT_SCRATCH_ANY     0x00000100
-
-/** @brief User can scratch their own tracks */
-#define RIGHT_SCRATCH_MINE    0x00000200
-
-/** @brief User can scratch randomly chosen tracks */
-#define RIGHT_SCRATCH_RANDOM  0x00000400
-
-#define RIGHT_SCRATCH__MASK   0x00000700
-
-/** @brief User can change the volume */
-#define RIGHT_VOLUME          0x00000800
-
-/** @brief User can perform admin operations */
-#define RIGHT_ADMIN           0x00001000
-
-/** @brief User can initiate a rescan */
-#define RIGHT_RESCAN          0x00002000
-
-/** @brief User can register new users */
-#define RIGHT_REGISTER        0x00004000
-
-/** @brief User can edit their own userinfo */
-#define RIGHT_USERINFO        0x00008000
-
-/** @brief User can modify track preferences */
-#define RIGHT_PREFS           0x00010000
-
-/** @brief User can modify global preferences */
-#define RIGHT_GLOBAL_PREFS    0x00020000
-
-/** @brief Current rights mask */
-#define RIGHTS__MASK          0x0003ffff
-
-/** @brief Unsigned type big enough for rights */
-typedef uint32_t rights_type;
-
 /** @brief Do not attempt database recovery (trackdb_init()) */
 #define TRACKDB_NO_RECOVER 0x0000
 
@@ -234,8 +168,6 @@ struct kvp *trackdb_getuserinfo(const char *user);
 int trackdb_edituserinfo(const char *user,
                          const char *key, const char *value);
 
-rights_type default_rights(void);
-
 #endif /* TRACKDB_H */
 
 /*
index 6478981..325599b 100644 (file)
@@ -868,6 +868,17 @@ class client:
     """Delete a user"""
     self._simple("deluser", user)
 
+  def userinfo(self, user, key):
+    """Get user information"""
+    res, details = self._simple("userinfo", user, key)
+    if res == 555:
+      return None
+    return _split(details)[0]
+
+  def edituser(self, user, key, value):
+    """Set user information"""
+    self._simple("edituser", user, key, value)
+
   ########################################################################
   # I/O infrastructure
 
index 77960a3..e152980 100644 (file)
@@ -33,6 +33,7 @@
 #include "mem.h"
 #include "disorder.h"
 #include "event.h"
+#include "rights.h"
 #include "trackdb.h"
 
 int disorder_track_exists(const char *track)  {
index b4cf58e..0d8600d 100644 (file)
@@ -34,6 +34,7 @@
 #include "log.h"
 #include "defs.h"
 #include "kvp.h"
+#include "rights.h"
 #include "trackdb.h"
 #include "trackdb-int.h"
 #include "mem.h"
index bce3eee..99359b2 100644 (file)
@@ -39,6 +39,7 @@
 #include "defs.h"
 #include "mem.h"
 #include "kvp.h"
+#include "rights.h"
 #include "trackdb.h"
 #include "trackdb-int.h"
 
index c1f578e..f7bc378 100644 (file)
@@ -44,6 +44,7 @@
 #include "event.h"
 #include "log.h"
 #include "configuration.h"
+#include "rights.h"
 #include "trackdb.h"
 #include "queue.h"
 #include "mem.h"
index ec1fc4d..3be14be 100644 (file)
@@ -42,6 +42,7 @@
 #include "kvp.h"
 #include "vector.h"
 #include "inputline.h"
+#include "rights.h"
 #include "trackdb.h"
 #include "trackdb-int.h"
 #include "charset.h"
index 7b70799..e9c2b68 100644 (file)
@@ -44,6 +44,7 @@
 #include "configuration.h"
 #include "queue.h"
 #include "server-queue.h"
+#include "rights.h"
 #include "trackdb.h"
 #include "play.h"
 #include "plugin.h"
index e45eaa6..dab56df 100644 (file)
@@ -47,6 +47,7 @@
 #include "wstat.h"
 #include "kvp.h"
 #include "printf.h"
+#include "rights.h"
 #include "trackdb.h"
 #include "trackdb-int.h"
 #include "trackname.h"
index 145bfe2..6591d91 100644 (file)
@@ -51,6 +51,7 @@
 #include "split.h"
 #include "configuration.h"
 #include "hex.h"
+#include "rights.h"
 #include "trackdb.h"
 #include "table.h"
 #include "kvp.h"
@@ -1050,16 +1051,42 @@ static int c_deluser(struct conn *c,
 }
 
 static int c_edituser(struct conn *c,
-                     char attribute((unused)) **vec,
+                     char **vec,
                      int attribute((unused)) nvec) {
-  sink_writes(ev_writer_sink(c->w), "550 Not implemented\n"); /* TODO */
+  /* TODO local only */
+  if(trusted(c)
+     || (!strcmp(c->who, vec[0])
+        && (!strcmp(vec[1], "email")
+            || !strcmp(vec[1], "password")))) {
+    if(trackdb_edituserinfo(vec[0], vec[1], vec[2]))
+      sink_writes(ev_writer_sink(c->w), "550 Failed to change setting\n");
+    else
+      sink_writes(ev_writer_sink(c->w), "250 OK\n");
+  } else
+    sink_writes(ev_writer_sink(c->w), "550 Restricted to administrators\n");
   return 1;
 }
 
 static int c_userinfo(struct conn *c,
                      char attribute((unused)) **vec,
                      int attribute((unused)) nvec) {
-  sink_writes(ev_writer_sink(c->w), "550 Not implemented\n"); /* TODO */
+  struct kvp *k;
+  const char *value;
+  
+  /* TODO local only */
+  if(trusted(c)
+     || (!strcmp(c->who, vec[0])
+        && (!strcmp(vec[1], "email")
+            || !strcmp(vec[1], "rights")))) {
+    if((k = trackdb_getuserinfo(vec[0])))
+      if((value = kvp_get(k, vec[1])))
+       sink_printf(ev_writer_sink(c->w), "252 %s\n", quoteutf8(value));
+      else
+       sink_writes(ev_writer_sink(c->w), "555 Not set\n");
+    else
+      sink_writes(ev_writer_sink(c->w), "550 No such user\n");
+  } else
+    sink_writes(ev_writer_sink(c->w), "550 Restricted to administrators\n");
   return 1;
 }
 
index 79cae43..d6b6699 100644 (file)
@@ -36,6 +36,7 @@
 
 #include "event.h"
 #include "play.h"
+#include "rights.h"
 #include "trackdb.h"
 #include "state.h"
 #include "configuration.h"
index c397be8..44558eb 100644 (file)
@@ -34,6 +34,7 @@
 #include "log.h"
 #include "syscalls.h"
 #include "configuration.h"
+#include "rights.h"
 #include "trackdb.h"
 
 static const struct option options[] = {
index 8c13232..13bee66 100755 (executable)
@@ -30,6 +30,10 @@ def test():
     print " checking new user can log in"
     c = disorder.client(user="bob", password="bobpass")
     c.version()
+    print " checking bob can set their email address"
+    c.edituser("bob", "email", "foo@bar")
+    email = c.userinfo("bob", "email")
+    assert email == "foo@bar", "checking bob's email address"
     print " checking user deletion"
     c = disorder.client()
     c.deluser("bob")