chiark / gitweb /
register/confirm commands, and tests, and docs
authorRichard Kettlewell <rjk@greenend.org.uk>
Fri, 21 Dec 2007 20:24:54 +0000 (20:24 +0000)
committerRichard Kettlewell <rjk@greenend.org.uk>
Fri, 21 Dec 2007 20:24:54 +0000 (20:24 +0000)
doc/disorder_protocol.5.in
lib/client.c
lib/client.h
lib/trackdb.c
lib/trackdb.h
python/disorder.py.in
server/server.c
tests/user.py

index fa98a8144751c888e20cc688be0f78b498af16aa..a1a4af209c5d0df4b7b67a9a6838b51da7718694 100644 (file)
@@ -58,6 +58,10 @@ Creates a new user with the given username and password.  Requires the
 Lists all the files and directories in \fIDIRECTORY\fR in a response body.
 If \fIREGEXP\fR is present only matching files and directories are returned.
 .TP
+.B confirm \fICONFIRMATION
+Confirm user registration.  \fICONFIRMATION\fR is as returned from
+\fBregister\fR below.  This command can be used without logging in.
+.TP
 .B cookie \fICOOKIE
 Log a user back in using a cookie created with \fBmake-cookie\fR.
 .TP
@@ -210,6 +214,11 @@ information syntax.
 Request that DisOrder reconfigure itself.   Requires the \fBadmin\fR right.
 command.
 .TP
+.B register \fIUSER PASSWORD EMAIL
+Register a new user.  Requires the \fBregister\fR right.  The result contains a
+confirmation string; the user will be be able to log in until this has been
+presented back to the server via the \fBconfirm\fR command.
+.TP
 .B remove \fIID\fR
 Remove the track identified by \fIID\fR.  Requires one of the \fBremove
 mine\fR, \fBremove random\fR or \fBremove any\fR rights depending on how the
index 739fc20e1cac4a07878f04184c8b11d1ab90171d..39272e6f6953031e25edf4b6741d7df88b1b7bfa 100644 (file)
@@ -697,6 +697,18 @@ int disorder_edituser(disorder_client *c, const char *user,
   return disorder_simple(c, 0, "edituser", user, key, value, (char *)0);
 }
 
+int disorder_register(disorder_client *c, const char *user,
+                     const char *password, const char *email,
+                     char **confirmp) {
+  return dequote(disorder_simple(c, confirmp, "register",
+                                user, password, email, (char *)0),
+                confirmp);
+}
+
+int disorder_confirm(disorder_client *c, const char *confirm) {
+  return disorder_simple(c, 0, "confirm", confirm, (char *)0);
+}
+
 /*
 Local Variables:
 c-basic-offset:2
index f336de6a51600e78dc1ee954af7e23adf58c6272..c685b3db65ee0611d5e1f97ad676bc1f0289f306 100644 (file)
@@ -197,6 +197,10 @@ int disorder_edituser(disorder_client *c, const char *user,
                      const char *key, const char *value);
 int disorder_users(disorder_client *c,
                   char ***vecp, int *nvecp);
+int disorder_register(disorder_client *c, const char *user,
+                     const char *password, const char *email,
+                     char **confirmp);
+int disorder_confirm(disorder_client *c, const char *confirm);
 
 #endif /* CLIENT_H */
 
index 46a875c5a3f48c6e6ed6636c7eb55d614a2f334b..369474857803a919cca8da6cbe6619efd93f3c1a 100644 (file)
@@ -2421,22 +2421,52 @@ static int trusted(const char *user) {
   return n < config->trust.n;
 }
 
+/** @brief Return non-zero for a valid username
+ *
+ * Currently we only allow the letters and digits in ASCII.  We could be more
+ * liberal than this but it is a nice simple test.  It is critical that
+ * semicolons are never allowed.
+ */
+static int valid_username(const char *user) {
+  if(!*user)
+    return 0;
+  while(*user) {
+    const uint8_t c = *user++;
+    /* For now we are very strict */
+    if((c >= 'a' && c <= 'z')
+       || (c >= 'A' && c <= 'Z')
+       || (c >= '0' && c <= '9'))
+      /* ok */;
+    else
+      return 0;
+  }
+  return 1;
+}
+
 /** @brief Add a user */
 static int create_user(const char *user,
                        const char *password,
                        const char *rights,
                        const char *email,
+                       const char *confirmation,
                        DB_TXN *tid,
                        uint32_t flags) {
   struct kvp *k = 0;
   char s[64];
 
+  /* sanity check user */
+  if(!valid_username(user)) {
+    error(0, "invalid username '%s'", user);
+    return -1;
+  }
   /* data for this user */
   if(password)
     kvp_set(&k, "password", password);
   kvp_set(&k, "rights", rights);
   if(email)
     kvp_set(&k, "email", email);
+  if(confirmation)
+    kvp_set(&k, "confirmation", confirmation);
   snprintf(s, sizeof s, "%jd", (intmax_t)time(0));
   kvp_set(&k, "created", s);
   return trackdb_putdata(trackdb_usersdb, user, k, tid, flags);
@@ -2459,7 +2489,8 @@ static int one_old_user(const char *user, const char *password,
     rights = rights_string(config->default_rights|RIGHT_ADMIN|RIGHT_RESCAN);
   else
     rights = rights_string(config->default_rights);
-  return create_user(user, password, rights, 0/*email*/, tid, DB_NOOVERWRITE);
+  return create_user(user, password, rights, 0/*email*/, 0/*confirmation*/,
+                     tid, DB_NOOVERWRITE);
 }
 
 static int trackdb_old_users_tid(DB_TXN *tid) {
@@ -2502,8 +2533,9 @@ void trackdb_create_root(void) {
   gcry_randomize(pwbin, sizeof pwbin, GCRY_STRONG_RANDOM);
   pw = mime_to_base64(pwbin, sizeof pwbin);
   /* Create the root user if it does not exist */
-  WITH_TRANSACTION(create_user("root", pw, "all", 0/*email*/, tid,
-                               DB_NOOVERWRITE));
+  WITH_TRANSACTION(create_user("root", pw, "all",
+                               0/*email*/, 0/*confirmation*/,
+                               tid, DB_NOOVERWRITE));
   if(e == 0)
     info("created root user");
 }
@@ -2538,11 +2570,12 @@ const char *trackdb_get_password(const char *user) {
 int trackdb_adduser(const char *user,
                     const char *password,
                     rights_type rights,
-                    const char *email) {
+                    const char *email,
+                    const char *confirmation) {
   int e;
   const char *r = rights_string(rights);
 
-  WITH_TRANSACTION(create_user(user, password, r, email,
+  WITH_TRANSACTION(create_user(user, password, r, email, confirmation,
                                tid, DB_NOOVERWRITE));
   if(e) {
     error(0, "cannot created user '%s' because they already exist", user);
@@ -2662,6 +2695,49 @@ char **trackdb_listusers(void) {
   return v->vec;
 }
 
+static int trackdb_confirm_tid(const char *user, const char *confirmation,
+                               DB_TXN *tid) {
+  const char *stored_confirmation;
+  struct kvp *k;
+  int e;
+  
+  if((e = trackdb_getdata(trackdb_usersdb, user, &k, tid)))
+    return e;
+  if(!(stored_confirmation = kvp_get(k, "confirmation"))) {
+    error(0, "already confirmed user '%s'", user);
+    /* DB claims -30,800 to -30,999 so -1 should be a safe bet */
+    return -1;
+  }
+  if(strcmp(confirmation, stored_confirmation)) {
+    error(0, "wrong confirmation string for user '%s'", user);
+    return -1;
+  }
+  /* 'sall good */
+  kvp_set(&k, "confirmation", 0);
+  return trackdb_putdata(trackdb_usersdb, user, k, tid, 0);
+}
+
+/** @brief Confirm a user registration
+ * @param user Username
+ * @param confirmation Confirmation string
+ * @return 0 on success, non-0 on error
+ */
+int trackdb_confirm(const char *user, const char *confirmation) {
+  int e;
+
+  WITH_TRANSACTION(trackdb_confirm_tid(user, confirmation, tid));
+  switch(e) {
+  case 0:
+    info("registration confirmed for user '%s'", user);
+    return 0;
+  case DB_NOTFOUND:
+    error(0, "confirmation for nonexistent user '%s'", user);
+    return -1;
+  default:                              /* already reported */
+    return -1;
+  }
+}
+
 /*
 Local Variables:
 c-basic-offset:2
index 2c57e86c9223709845784899ee12e09eff339ffe..d777146cea5f15d898f25c5193f88158c3f02b53 100644 (file)
@@ -162,12 +162,14 @@ const char *trackdb_get_password(const char *user);
 int trackdb_adduser(const char *user,
                     const char *password,
                     rights_type rights,
-                    const char *email);
+                    const char *email,
+                    const char *confirmation);
 int trackdb_deluser(const char *user);
 struct kvp *trackdb_getuserinfo(const char *user);
 int trackdb_edituserinfo(const char *user,
                          const char *key, const char *value);
 char **trackdb_listusers(void);
+int trackdb_confirm(const char *user, const char *confirmation);
 
 #endif /* TRACKDB_H */
 
index 650671d506c818ee248034ae2c5f922e2af49860..36157a0a8dbb39e065f10bca3ba91b5b7bd087ae 100644 (file)
@@ -881,6 +881,15 @@ class client:
     self._simple("users")
     return self._body()
 
+  def register(self, username, password, email):
+    """Register a user"""
+    res, details = self._simple("register", username, password, email)
+    return _split(details)[0]
+
+  def confirm(self, confirmation):
+    """Confirm a user registration"""
+    res, details = self._simple("confirm", confirmation)
+
   ########################################################################
   # I/O infrastructure
 
index 807a2766cd882f57d2efff6e0f96f1e5a6a65ded..22cf45401b160dce1c8eeded45e337ab2fd3a24e 100644 (file)
@@ -66,6 +66,7 @@
 #include "cache.h"
 #include "unicode.h"
 #include "cookies.h"
+#include "mime.h"
 
 #ifndef NONCE_SIZE
 # define NONCE_SIZE 16
@@ -1077,7 +1078,8 @@ static int c_revoke(struct conn *c,
 static int c_adduser(struct conn *c,
                     char **vec,
                     int attribute((unused)) nvec) {
-  if(trackdb_adduser(vec[0], vec[1], config->default_rights, 0))
+  if(trackdb_adduser(vec[0], vec[1], config->default_rights,
+                    0/*email*/, 0/*confirmation*/))
     sink_writes(ev_writer_sink(c->w), "550 Cannot create user\n");
   else
     sink_writes(ev_writer_sink(c->w), "250 User created\n");
@@ -1088,7 +1090,7 @@ static int c_deluser(struct conn *c,
                     char **vec,
                     int attribute((unused)) nvec) {
   if(trackdb_deluser(vec[0]))
-    sink_writes(ev_writer_sink(c->w), "550 Cannot deleted user\n");
+    sink_writes(ev_writer_sink(c->w), "550 Cannot delete user\n");
   else
     sink_writes(ev_writer_sink(c->w), "250 User deleted\n");
   return 1;
@@ -1152,6 +1154,45 @@ static int c_users(struct conn *c,
   return 1;                            /* completed */
 }
 
+static int c_register(struct conn *c,
+                     char **vec,
+                     int attribute((unused)) nvec) {
+  char *buf, *cs;
+  size_t bufsize;
+  int offset;
+
+  /* The confirmation string is base64(username;nonce) */
+  bufsize = strlen(vec[0]) + NONCE_SIZE + 2;
+  buf = xmalloc_noptr(bufsize);
+  offset = byte_snprintf(buf, bufsize, "%s;", vec[0]);
+  gcry_randomize(buf + offset, NONCE_SIZE, GCRY_STRONG_RANDOM);
+  cs = mime_to_base64((uint8_t *)buf, offset + NONCE_SIZE);
+  if(trackdb_adduser(vec[0], vec[1], config->default_rights, vec[2], cs))
+    sink_writes(ev_writer_sink(c->w), "550 Cannot create user\n");
+  else
+    sink_printf(ev_writer_sink(c->w), "252 %s\n", quoteutf8(cs));
+  return 1;
+}
+
+static int c_confirm(struct conn *c,
+                    char **vec,
+                    int attribute((unused)) nvec) {
+  size_t nuser;
+  char *user, *sep;
+
+  if(!(user = mime_base64(vec[0], &nuser))
+     || !(sep = memchr(user, ';', nuser))) {
+    sink_writes(ev_writer_sink(c->w), "550 Malformed confirmation string\n");
+    return 1;
+  }
+  *sep = 0;
+  if(trackdb_confirm(user, vec[0]))
+    sink_writes(ev_writer_sink(c->w), "550 Incorrect confirmation string\n");
+  else
+    sink_writes(ev_writer_sink(c->w), "250 OK\n");
+  return 1;
+}
 static const struct command {
   /** @brief Command name */
   const char *name;
@@ -1174,6 +1215,7 @@ static const struct command {
 } commands[] = {
   { "adduser",        2, 2,       c_adduser,        RIGHT_ADMIN|RIGHT__LOCAL },
   { "allfiles",       0, 2,       c_allfiles,       RIGHT_READ },
+  { "confirm",        1, 1,       c_confirm,        0 },
   { "cookie",         1, 1,       c_cookie,         0 },
   { "deluser",        1, 1,       c_deluser,        RIGHT_ADMIN|RIGHT__LOCAL },
   { "dirs",           0, 2,       c_dirs,           RIGHT_READ },
@@ -1203,6 +1245,7 @@ static const struct command {
   { "random-enabled", 0, 0,       c_random_enabled, RIGHT_READ },
   { "recent",         0, 0,       c_recent,         RIGHT_READ },
   { "reconfigure",    0, 0,       c_reconfigure,    RIGHT_ADMIN },
+  { "register",       3, 3,       c_register,       RIGHT_REGISTER|RIGHT__LOCAL },
   { "remove",         1, 1,       c_remove,         RIGHT_REMOVE__MASK },
   { "rescan",         0, 0,       c_rescan,         RIGHT_RESCAN },
   { "resolve",        1, 1,       c_resolve,        RIGHT_READ },
index b04e2a285fffe3cdeda9d2f76842ded0dc9bce0e..74f021296181e1d0e71fbb5c8ec7bfecbecf8c4b 100755 (executable)
@@ -55,6 +55,22 @@ def test():
     users = c.users()
     assert dtest.lists_have_same_contents(users,
                                           ["fred", "root"])
+    print " testing user registration"
+    cs = c.register("joe", "joepass", "joe@nowhere.invalid")
+    print " confirmation string is %s" % cs
+    print " checking unconfirmed user cannot log in"
+    jc = disorder.client(user="joe", password="joepass")
+    try:
+      jc.version()
+      print "*** should not be able to log in before confirmation ***"
+      assert False
+    except disorder.operationError:
+      pass                              # good
+    print " confirming user"
+    c.confirm(cs)
+    print " checking confirmed user can log in"
+    jc = disorder.client(user="joe", password="joepass")
+    jc.version()
 
 if __name__ == '__main__':
     dtest.run()