You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by jc...@apache.org on 2010/01/07 21:02:48 UTC

svn commit: r896989 - in /couchdb/trunk: ./ etc/couchdb/ etc/default/ share/ share/www/ share/www/dialog/ share/www/script/ share/www/script/test/ share/www/style/ src/couchdb/

Author: jchris
Date: Thu Jan  7 20:02:46 2010
New Revision: 896989

URL: http://svn.apache.org/viewvc?rev=896989&view=rev
Log:
merge account branch to trunk

Added:
    couchdb/trunk/share/www/dialog/_admin_party.html
      - copied unchanged from r896984, couchdb/branches/account/share/www/dialog/_admin_party.html
    couchdb/trunk/share/www/dialog/_create_admin.html
      - copied unchanged from r896984, couchdb/branches/account/share/www/dialog/_create_admin.html
    couchdb/trunk/share/www/dialog/_login.html
      - copied unchanged from r896984, couchdb/branches/account/share/www/dialog/_login.html
    couchdb/trunk/share/www/dialog/_signup.html
      - copied unchanged from r896984, couchdb/branches/account/share/www/dialog/_signup.html
    couchdb/trunk/share/www/script/test/users_db.js
      - copied unchanged from r896984, couchdb/branches/account/share/www/script/test/users_db.js
Modified:
    couchdb/trunk/   (props changed)
    couchdb/trunk/etc/couchdb/default.ini.tpl.in
    couchdb/trunk/etc/couchdb/local.ini
    couchdb/trunk/etc/default/couchdb   (props changed)
    couchdb/trunk/share/Makefile.am
    couchdb/trunk/share/www/_sidebar.html
    couchdb/trunk/share/www/config.html
    couchdb/trunk/share/www/couch_tests.html
    couchdb/trunk/share/www/custom_test.html
    couchdb/trunk/share/www/database.html
    couchdb/trunk/share/www/document.html
    couchdb/trunk/share/www/index.html
    couchdb/trunk/share/www/replicator.html
    couchdb/trunk/share/www/script/couch.js
    couchdb/trunk/share/www/script/couch_test_runner.js
    couchdb/trunk/share/www/script/couch_tests.js
    couchdb/trunk/share/www/script/futon.browse.js
    couchdb/trunk/share/www/script/futon.js
    couchdb/trunk/share/www/script/jquery.couch.js
    couchdb/trunk/share/www/script/test/cookie_auth.js
    couchdb/trunk/share/www/script/test/oauth.js
    couchdb/trunk/share/www/style/layout.css
    couchdb/trunk/src/couchdb/couch_db.hrl
    couchdb/trunk/src/couchdb/couch_httpd.erl
    couchdb/trunk/src/couchdb/couch_httpd_auth.erl
    couchdb/trunk/src/couchdb/couch_httpd_db.erl
    couchdb/trunk/src/couchdb/couch_httpd_oauth.erl

Propchange: couchdb/trunk/
------------------------------------------------------------------------------
--- svn:mergeinfo (original)
+++ svn:mergeinfo Thu Jan  7 20:02:46 2010
@@ -1,5 +1,6 @@
 /couchdb/branches/0.10.x:820161,823015
 /couchdb/branches/0.9.x:775634
+/couchdb/branches/account:896157-896984
 /couchdb/branches/design_resources:751716-751803
 /couchdb/branches/form:729440-730015
 /couchdb/branches/list-iterator:782292-784593

Modified: couchdb/trunk/etc/couchdb/default.ini.tpl.in
URL: http://svn.apache.org/viewvc/couchdb/trunk/etc/couchdb/default.ini.tpl.in?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/etc/couchdb/default.ini.tpl.in (original)
+++ couchdb/trunk/etc/couchdb/default.ini.tpl.in Thu Jan  7 20:02:46 2010
@@ -17,9 +17,8 @@
 [httpd]
 port = 5984
 bind_address = 127.0.0.1
-authentication_handlers = {couch_httpd_oauth, oauth_authentication_handler}, {couch_httpd_auth, default_authentication_handler}
+authentication_handlers = {couch_httpd_oauth, oauth_authentication_handler}, {couch_httpd_auth, cookie_authentication_handler}, {couch_httpd_auth, default_authentication_handler}
 default_handler = {couch_httpd_db, handle_request}
-WWW-Authenticate = Basic realm="administrator"
 
 [log]
 file = %localstatelogdir%/couch.log
@@ -27,7 +26,6 @@
 
 [couch_httpd_auth]
 authentication_db = users
-secret = replace this with a real secret in your local.ini file
 require_valid_user = false
 
 [query_servers]
@@ -70,7 +68,6 @@
 _sleep = {couch_httpd_misc_handlers, handle_sleep_req}
 _session = {couch_httpd_auth, handle_session_req}
 _oauth = {couch_httpd_oauth, handle_oauth_req}
-_user = {couch_httpd_auth, handle_user_req}
 
 [httpd_db_handlers]
 _view_cleanup = {couch_httpd_db, handle_view_cleanup_req}

Modified: couchdb/trunk/etc/couchdb/local.ini
URL: http://svn.apache.org/viewvc/couchdb/trunk/etc/couchdb/local.ini?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/etc/couchdb/local.ini (original)
+++ couchdb/trunk/etc/couchdb/local.ini Thu Jan  7 20:02:46 2010
@@ -10,14 +10,12 @@
 [httpd]
 ;port = 5984
 ;bind_address = 127.0.0.1
+; Uncomment next line to trigger basic-auth popup on unauthorized requests.
+;WWW-Authenticate = Basic realm="administrator"
 
 [log]
 ;level = debug
 
-[couch_httpd_auth]
-;secret = replace this with a real secret
-
-
 [update_notification]
 ;unique notifier name=/full/path/to/exe -with "cmd line arg"
 

Propchange: couchdb/trunk/etc/default/couchdb
------------------------------------------------------------------------------
--- svn:mergeinfo (original)
+++ svn:mergeinfo Thu Jan  7 20:02:46 2010
@@ -1,5 +1,6 @@
 /couchdb/branches/0.10.x/etc/default/couchdb:820161,823015
 /couchdb/branches/0.9.x/etc/default/couchdb:775634
+/couchdb/branches/account/etc/default/couchdb:896157-896984
 /couchdb/branches/design_resources/etc/default/couchdb:751716-751803
 /couchdb/branches/form/etc/default/couchdb:729440-730015
 /couchdb/branches/list-iterator/etc/default/couchdb:782292-784593

Modified: couchdb/trunk/share/Makefile.am
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/Makefile.am?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/share/Makefile.am (original)
+++ couchdb/trunk/share/Makefile.am Thu Jan  7 20:02:46 2010
@@ -39,8 +39,13 @@
     www/couch_tests.html \
     www/custom_test.html \
     www/database.html \
+    www/dialog/_admin_party.html \
     www/dialog/_compact_database.html \
+    www/dialog/_compact_view.html \
     www/dialog/_view_cleanup.html \
+    www/dialog/_create_admin.html \
+    www/dialog/_login.html \
+    www/dialog/_signup.html \
     www/dialog/_create_database.html \
     www/dialog/_delete_database.html \
     www/dialog/_delete_document.html \

Modified: couchdb/trunk/share/www/_sidebar.html
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/_sidebar.html?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/share/www/_sidebar.html (original)
+++ couchdb/trunk/share/www/_sidebar.html Thu Jan  7 20:02:46 2010
@@ -30,7 +30,31 @@
     </li>
   </ul>
   <div id="footer">
-    Futon on <a href="http://couchdb.apache.org/">Apache CouchDB</a>
-    <span id="version">?</span>
+    <span id="userCtx">
+      <span class="loggedout">
+        <a href="#" class="signup">Signup</a> or <a href="#" class="login">Login</a>
+      </span>
+      <span class="loggedin">
+        Welcome <a class="username">?</a>! 
+        <br/>
+        <a href="#" class="logout">Logout</a>
+      </span>
+      <span class="loggedinadmin">
+        Welcome <a class="username">?</a>! 
+        <br/>
+        <a href="#" class="createadmin">Setup more admins</a> or
+        <a href="#" class="logout">Logout</a> 
+      </span>
+      <span class="adminparty">
+        Welcome to Admin Party! 
+        <br/>
+        Everyone is admin. <a href="#" class="createadmin">Fix this</a>
+      </span>
+    </span>
+    <hr/>
+    <span class="couch">
+      Futon on <a href="http://couchdb.apache.org/">Apache CouchDB</a>
+      <span id="version">?</span>
+    </span>
   </div>
 </div>

Modified: couchdb/trunk/share/www/config.html
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/config.html?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/share/www/config.html (original)
+++ couchdb/trunk/share/www/config.html Thu Jan  7 20:02:46 2010
@@ -19,10 +19,12 @@
     <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
     <link rel="stylesheet" href="style/layout.css?0.11.0" type="text/css">
     <script src="script/json2.js"></script>
+    <script src="script/sha1.js"></script>
     <script src="script/jquery.js?1.3.2"></script>
     <script src="script/jquery.couch.js?0.11.0"></script>
-    <script src="script/jquery.editinline.js?0.11.0"></script>
+    <script src="script/jquery.dialog.js?0.11.0"></script>
     <script src="script/futon.js?0.11.0"></script>
+    <script src="script/jquery.editinline.js?0.11.0"></script>
     <script>
       $(function() {
         $.couch.config({

Modified: couchdb/trunk/share/www/couch_tests.html
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/couch_tests.html?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/share/www/couch_tests.html [utf-8] (original)
+++ couchdb/trunk/share/www/couch_tests.html [utf-8] Thu Jan  7 20:02:46 2010
@@ -19,15 +19,19 @@
     <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
     <link rel="stylesheet" href="style/layout.css?0.11.0" type="text/css">
     <script src="script/json2.js"></script>
+    <script src="script/sha1.js"></script>
     <script src="script/jquery.js?1.3.2"></script>
     <script src="script/jquery.couch.js?0.11.0"></script>
-    <script src="script/couch.js?0.11.0"></script>
+    <script src="script/jquery.dialog.js?0.11.0"></script>
     <script src="script/futon.js?0.11.0"></script>
+    <script src="script/couch.js?0.11.0"></script>
     <script src="script/couch_test_runner.js?0.11.0"></script>
     <script>
       $(function() {
         updateTestsListing();
-        $("#toolbar button.run").click(runAllTests);
+        $("#toolbar button.run").click(function() {
+          setupAdminParty(runAllTests) ;
+        });
         $("#toolbar button.load").click(function() {
           location.reload(true);
         });

Modified: couchdb/trunk/share/www/custom_test.html
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/custom_test.html?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/share/www/custom_test.html (original)
+++ couchdb/trunk/share/www/custom_test.html Thu Jan  7 20:02:46 2010
@@ -19,11 +19,13 @@
     <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
     <link rel="stylesheet" href="style/layout.css?0.11.0" type="text/css">
     <script src="script/json2.js"></script>
+    <script src="script/sha1.js"></script>
     <script src="script/jquery.js?1.3.2"></script>
     <script src="script/jquery.couch.js?0.11.0"></script>
+    <script src="script/jquery.dialog.js?0.11.0"></script>
+    <script src="script/futon.js?0.11.0"></script>
     <script src="script/jquery.resizer.js?0.11.0"></script>
     <script src="script/couch.js?0.11.0"></script>
-    <script src="script/futon.js?0.11.0"></script>
     <script>
       function T(arg, desc) {
         if(!arg) {

Modified: couchdb/trunk/share/www/database.html
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/database.html?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/share/www/database.html [utf-8] (original)
+++ couchdb/trunk/share/www/database.html [utf-8] Thu Jan  7 20:02:46 2010
@@ -19,12 +19,13 @@
     <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
     <link rel="stylesheet" href="style/layout.css?0.11.0" type="text/css">
     <script src="script/json2.js"></script>
+    <script src="script/sha1.js"></script>
     <script src="script/jquery.js?1.3.2"></script>
     <script src="script/jquery.couch.js?0.11.0"></script>
     <script src="script/jquery.dialog.js?0.11.0"></script>
+    <script src="script/futon.js?0.11.0"></script>
     <script src="script/jquery.resizer.js?0.11.0"></script>
     <script src="script/jquery.suggest.js?0.11.0"></script>
-    <script src="script/futon.js?0.11.0"></script>
     <script src="script/futon.browse.js?0.11.0"></script>
     <script src="script/futon.format.js?0.11.0"></script>
     <script>

Modified: couchdb/trunk/share/www/document.html
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/document.html?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/share/www/document.html [utf-8] (original)
+++ couchdb/trunk/share/www/document.html [utf-8] Thu Jan  7 20:02:46 2010
@@ -19,15 +19,16 @@
     <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
     <link rel="stylesheet" href="style/layout.css?0.11.0" type="text/css">
     <script src="script/json2.js"></script>
+    <script src="script/sha1.js"></script>
     <script src="script/jquery.js?1.3.2"></script>
     <script src="script/jquery.couch.js?0.11.0"></script>
     <script src="script/jquery.dialog.js?0.11.0"></script>
-    <script src="script/jquery.editinline.js?0.11.0"></script>
-    <script src="script/jquery.form.js?0.11.0"></script>
-    <script src="script/jquery.resizer.js?0.11.0"></script>
     <script src="script/futon.js?0.11.0"></script>
+    <script src="script/jquery.resizer.js?0.11.0"></script>
     <script src="script/futon.browse.js?0.11.0"></script>
-    <script src="script/futon.format.js?0.11.0"></script>
+    <script src="script/futon.format.js?0.11.0"></script>    
+    <script src="script/jquery.editinline.js?0.11.0"></script>
+    <script src="script/jquery.form.js?0.11.0"></script>
     <script>
       var page = new $.futon.CouchDocumentPage();
 

Modified: couchdb/trunk/share/www/index.html
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/index.html?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/share/www/index.html [utf-8] (original)
+++ couchdb/trunk/share/www/index.html [utf-8] Thu Jan  7 20:02:46 2010
@@ -19,6 +19,7 @@
     <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
     <link rel="stylesheet" href="style/layout.css?0.11.0" type="text/css">
     <script src="script/json2.js"></script>
+    <script src="script/sha1.js"></script>
     <script src="script/jquery.js?1.3.2"></script>
     <script src="script/jquery.couch.js?0.11.0"></script>
     <script src="script/jquery.dialog.js?0.11.0"></script>

Modified: couchdb/trunk/share/www/replicator.html
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/replicator.html?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/share/www/replicator.html [utf-8] (original)
+++ couchdb/trunk/share/www/replicator.html [utf-8] Thu Jan  7 20:02:46 2010
@@ -19,8 +19,10 @@
     <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
     <link rel="stylesheet" href="style/layout.css?0.11.0" type="text/css">
     <script src="script/json2.js"></script>
+    <script src="script/sha1.js"></script>
     <script src="script/jquery.js?1.3.2"></script>
     <script src="script/jquery.couch.js?0.11.0"></script>
+    <script src="script/jquery.dialog.js?0.11.0"></script>
     <script src="script/futon.js?0.11.0"></script>
     <script>
       $(document).ready(function() {

Modified: couchdb/trunk/share/www/script/couch.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/couch.js?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/share/www/script/couch.js [utf-8] (original)
+++ couchdb/trunk/share/www/script/couch.js [utf-8] Thu Jan  7 20:02:46 2010
@@ -340,60 +340,34 @@
   return JSON.parse(CouchDB.last_req.responseText);
 }
 
-CouchDB.createUser = function(username, password, email, roles, basicAuth) {
-  var roles_str = ""
-  if (roles) {
-    for (var i=0; i< roles.length; i++) {
-      roles_str += "&roles=" + encodeURIComponent(roles[i]);
-    }
-  }
-  var headers = {"Content-Type": "application/x-www-form-urlencoded"};
-  if (basicAuth) {
-    headers['Authorization'] = basicAuth
-  } else {
-    headers['X-CouchDB-WWW-Authenticate'] = 'Cookie';
-  }
-
-  CouchDB.last_req = CouchDB.request("POST", "/_user/", {
-    headers: headers,
-    body: "username=" + encodeURIComponent(username) + "&password="
-    + encodeURIComponent(password) + "&email="
-    + encodeURIComponent(email) + roles_str
-  });
+CouchDB.session = function(options) {
+  options = options || {};
+  CouchDB.last_req = CouchDB.request("GET", "/_session", options);
+  CouchDB.maybeThrowError(CouchDB.last_req);
   return JSON.parse(CouchDB.last_req.responseText);
-}
-
-CouchDB.updateUser = function(username, email, roles, password, old_password) {
-  var roles_str = ""
-  if (roles) {
-    for (var i=0; i< roles.length; i++) {
-      roles_str += "&roles=" + encodeURIComponent(roles[i]);
-    }
-  }
-
-  var body = "email="+ encodeURIComponent(email)+ roles_str;
+};
 
-  if (typeof(password) != "undefined" && password) {
-    body += "&password=" + password;
-  }
+CouchDB.user_prefix = "org.couchdb.user:";
 
-  if (typeof(old_password) != "undefined" && old_password) {
-    body += "&old_password=" + old_password;
+CouchDB.prepareUserDoc = function(user_doc, new_password) {
+  user_doc._id = user_doc._id || CouchDB.user_prefix + user_doc.username;
+  if (new_password) {
+    // handle the password crypto
+    user_doc.salt = CouchDB.newUuids(1)[0];
+    user_doc.password_sha = hex_sha1(new_password + user_doc.salt);
+  }
+  user_doc.type = "user";
+  if (!user_doc.roles) {
+    user_doc.roles = []
   }
-
-  CouchDB.last_req = CouchDB.request("PUT", "/_user/"+encodeURIComponent(username), {
-    headers: {"Content-Type": "application/x-www-form-urlencoded",
-      "X-CouchDB-WWW-Authenticate": "Cookie"},
-    body: body
-  });
-  return JSON.parse(CouchDB.last_req.responseText);
-}
+  return user_doc;
+};
 
 CouchDB.allDbs = function() {
   CouchDB.last_req = CouchDB.request("GET", "/_all_dbs");
   CouchDB.maybeThrowError(CouchDB.last_req);
   return JSON.parse(CouchDB.last_req.responseText);
-}
+};
 
 CouchDB.allDesignDocs = function() {
   var ddocs = {}, dbs = CouchDB.allDbs();

Modified: couchdb/trunk/share/www/script/couch_test_runner.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/couch_test_runner.js?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/share/www/script/couch_test_runner.js (original)
+++ couchdb/trunk/share/www/script/couch_test_runner.js Thu Jan  7 20:02:46 2010
@@ -63,6 +63,8 @@
 var currentRow = null;
 
 function runTest(button, callback, debug, noSave) {
+
+  // offer to save admins
   if (currentRow != null) {
     alert("Can not run multiple tests simultaneously.");
     return;
@@ -116,6 +118,65 @@
   win.document.location = "script/test/" + name + ".js";
 }
 
+var readyToRun;
+function setupAdminParty(fun) {
+  if (readyToRun) {
+    fun();
+  } else {
+    function removeAdmins(confs, doneFun) {
+      // iterate through the config and remove current user last
+      // current user is at front of list
+      var remove = confs.pop();
+      if (remove) {
+        $.couch.config({
+          success : function() {
+            removeAdmins(confs, doneFun);
+          }
+        }, "admins", remove[0], null);        
+      } else {
+        doneFun();
+      }
+    };
+    $.couch.session({
+      success : function(userCtx) {
+        if (userCtx.name && userCtx.roles.indexOf("_admin") != -1) {
+          // admin but not admin party. dialog offering to make admin party
+          $.showDialog("dialog/_admin_party.html", {
+            submit: function(data, callback) {
+              $.couch.config({
+                success : function(conf) {
+                  var meAdmin, adminConfs = [];
+                  for (var name in conf) {
+                    if (name == userCtx.name) {
+                      meAdmin = [name, conf[name]];
+                    } else {
+                      adminConfs.push([name, conf[name]]);
+                    }
+                  }
+                  adminConfs.unshift(meAdmin);
+                  removeAdmins(adminConfs, function() {
+                    callback();
+                    $.futon.session.sidebar();
+                    readyToRun = true;
+                    setTimeout(fun, 500);
+                  });
+                }
+              }, "admins");
+            }
+          });
+        } else if (userCtx.roles.indexOf("_admin") != -1) {
+          // admin party!
+          readyToRun = true;
+          fun();
+        } else {
+          // not an admin
+          alert("Error: You need to be an admin to run the tests.");
+        };
+      }
+    });
+  }
+};
+
 function updateTestsListing() {
   for (var name in couchTests) {
     var testFunction = couchTests[name];
@@ -128,7 +189,11 @@
       .find("td:nth(2)").addClass("details").end();
     $("<button type='button' class='run' title='Run test'></button>").click(function() {
       this.blur();
-      runTest(this);
+      var self = this;
+      // check for admin party
+      setupAdminParty(function() {
+        runTest(self);
+      });
       return false;
     }).prependTo(row.find("th"));
     row.attr("id", name).appendTo("#tests tbody.content");

Modified: couchdb/trunk/share/www/script/couch_tests.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/couch_tests.js?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/share/www/script/couch_tests.js [utf-8] (original)
+++ couchdb/trunk/share/www/script/couch_tests.js [utf-8] Thu Jan  7 20:02:46 2010
@@ -73,6 +73,7 @@
 loadTest("show_documents.js");
 loadTest("stats.js");
 loadTest("update_documents.js");
+loadTest("users_db.js");
 loadTest("utf8.js");
 loadTest("uuids.js");
 loadTest("view_collation.js");

Modified: couchdb/trunk/share/www/script/futon.browse.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/futon.browse.js?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/share/www/script/futon.browse.js [utf-8] (original)
+++ couchdb/trunk/share/www/script/futon.browse.js [utf-8] Thu Jan  7 20:02:46 2010
@@ -294,7 +294,8 @@
                 fill_language();
               }
             }, "native_query_servers");
-          }
+          },
+          error : function() {}
         }, "query_servers");
       }
 
@@ -727,7 +728,7 @@
 
     },
 
-    // Page class for browse/database.html
+    // Page class for browse/document.html
     CouchDocumentPage: function() {
       var urlParts = location.search.substr(1).split("/");
       var dbName = decodeURIComponent(urlParts.shift());
@@ -1169,7 +1170,7 @@
           return false;
         }).prependTo($("a", li));
       }
-    }
+    },
 
   });
 

Modified: couchdb/trunk/share/www/script/futon.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/futon.js?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/share/www/script/futon.js (original)
+++ couchdb/trunk/share/www/script/futon.js Thu Jan  7 20:02:46 2010
@@ -12,6 +12,130 @@
 
 (function($) {
 
+  function Session() {
+    
+    function doLogin(username, password, callback) {
+      $.couch.login({
+        username : username,
+        password : password,
+        success : function() {
+          $.futon.session.sidebar();
+          callback();
+        },
+        error : function(code, error, reason) {
+          $.futon.session.sidebar();
+          callback({username : "Error logging in: "+reason});
+        }
+      });
+    };
+    
+    function doSignup(username, password, callback, runLogin) {
+      $.couch.signup({
+        username : username
+      }, password, {
+        success : function() {
+          if (runLogin) {
+            doLogin(username, password, callback);            
+          } else {
+            callback();
+          }
+        },
+        error : function(status, error, reason) {
+          $.futon.session.sidebar();
+          if (error = "conflict") {
+            callback({username : "Name '"+username+"' is taken"});
+          } else {
+            callback({username : "Signup error:  "+reason});
+          }
+        }
+      });
+    };
+    
+    function validateUsernameAndPassword(data, callback) {
+      if (!data.username || data.username.length == 0) {
+        callback({username: "Please enter a username."});
+        return false;
+      };
+      if (!data.password || data.password.length == 0) {
+        callback({password: "Please enter a password."});
+        return false;
+      };
+      return true;
+    };
+    
+    function createAdmin() {
+      $.showDialog("dialog/_create_admin.html", {
+        submit: function(data, callback) {
+          if (!validateUsernameAndPassword(data, callback)) return;
+          $.couch.config({
+            success : function() {
+              callback();
+              doLogin(data.username, data.password, callback);            
+              doSignup(data.username, null, callback, false);
+            }
+          }, "admins", data.username, data.password);
+        }
+      });
+      return false;
+    };
+
+    function login() {
+      $.showDialog("dialog/_login.html", {
+        submit: function(data, callback) {
+          if (!validateUsernameAndPassword(data, callback)) return;
+          doLogin(data.username, data.password, callback);
+        }
+      });
+      return false;
+    };
+
+    function logout() {
+      $.couch.logout({
+        success : function(resp) {
+          $.futon.session.sidebar();
+        }
+      })
+    };
+
+    function signup() {
+      $.showDialog("dialog/_signup.html", {
+        submit: function(data, callback) {
+          if (!validateUsernameAndPassword(data, callback)) return;
+          doSignup(data.username, data.password, callback, true);
+        }
+      });
+      return false;
+    };
+
+    this.setupSidebar = function() {
+      $("#userCtx .login").click(login);
+      $("#userCtx .logout").click(logout);
+      $("#userCtx .signup").click(signup);
+      $("#userCtx .createadmin").click(createAdmin);
+    };
+    
+    this.sidebar = function() {
+      // get users db info?
+      $("#userCtx span").hide();
+      $.couch.session({
+        success : function(userCtx) {
+          if (userCtx.name) {
+            $("#userCtx .username").text(userCtx.name).attr({href : "/_utils/document.html?users/org.couchdb.user%3A"+userCtx.name});
+            if (userCtx.roles.indexOf("_admin") != -1) {
+              $("#userCtx .loggedinadmin").show();
+            } else {
+              $("#userCtx .loggedin").show();
+            }
+          } else if (userCtx.roles.indexOf("_admin") != -1) {
+            $("#userCtx .adminparty").show();
+          } else {
+            $("#userCtx .loggedout").show();
+          };
+        }
+      })
+    };
+  };
+
   function Navigation() {
     var nav = this;
     this.loaded = false;
@@ -233,6 +357,7 @@
   $.futon = $.futon || {};
   $.extend($.futon, {
     navigation: new Navigation(),
+    session : new Session(),
     storage: new Storage()
   });
 
@@ -309,12 +434,15 @@
       $.futon.navigation.updateDatabases();
       $.futon.navigation.updateSelection();
       $.futon.navigation.ready();
+      $.futon.session.setupSidebar();
+      $.futon.session.sidebar();
 
       $.couch.info({
         success: function(info, status) {
           $("#version").text(info.version);
         }
       });
+      
     });
   });
 

Modified: couchdb/trunk/share/www/script/jquery.couch.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/jquery.couch.js?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/share/www/script/jquery.couch.js [utf-8] (original)
+++ couchdb/trunk/share/www/script/jquery.couch.js [utf-8] Thu Jan  7 20:02:46 2010
@@ -20,7 +20,26 @@
       return "_design/" + encodeURIComponent(parts.join('/'));
     }
     return encodeURIComponent(docID);
-  }
+  };
+
+  function prepareUserDoc(user_doc, new_password) {    
+    if (typeof hex_sha1 == "undefined") {
+      alert("creating a user doc requires sha1.js to be loaded in the page");
+      return;
+    }
+    var user_prefix = "org.couchdb.user:";
+    user_doc._id = user_doc._id || user_prefix + user_doc.username;
+    if (new_password) {
+      // handle the password crypto
+      user_doc.salt = $.couch.newUUID();
+      user_doc.password_sha = hex_sha1(new_password + user_doc.salt);
+    }
+    user_doc.type = "user";
+    if (!user_doc.roles) {
+      user_doc.roles = []
+    }
+    return user_doc;
+  };
 
   uuidCache = [];
 
@@ -49,7 +68,9 @@
           req.url += encodeURIComponent(option);
         }
       }
-      if (value !== undefined) {
+      if (value === null) {
+        req.type = "DELETE";        
+      } else if (value !== undefined) {
         req.type = "PUT";
         req.data = toJSON(value);
         req.contentType = "application/json";
@@ -60,12 +81,46 @@
         "An error occurred retrieving/updating the server configuration"
       );
     },
+    
+    session: function(options) {
+      options = options || {};
+      $.ajax({
+        type: "GET", url: "/_session",
+        complete: function(req) {
+          var resp = $.httpData(req, "json");
+          if (req.status == 200) {
+            if (options.success) options.success(resp);
+          } else if (options.error) {
+            options.error(req.status, resp.error, resp.reason);
+          } else {
+            alert("An error occurred getting session info: " + resp.reason);
+          }
+        }
+      });
+    },
 
-    // TODO make login/logout and db.login/db.logout DRY
+    userDb : function(callback) {
+      $.couch.session({
+        success : function(resp) {
+          var userDb = $.couch.db(resp.info.user_db);
+          callback(userDb);
+        }
+      });
+    },
+
+    signup: function(user_doc, password, options) {      
+      options = options || {};
+      // prepare user doc based on name and password
+      user_doc = prepareUserDoc(user_doc, password);
+      $.couch.userDb(function(db) {
+        db.saveDoc(user_doc, options);
+      })
+    },
+    
     login: function(options) {
       options = options || {};
       $.ajax({
-        type: "POST", url: "/_login", dataType: "json",
+        type: "POST", url: "/_session", dataType: "json",
         data: {username: options.username, password: options.password},
         complete: function(req) {
           var resp = $.httpData(req, "json");
@@ -82,7 +137,8 @@
     logout: function(options) {
       options = options || {};
       $.ajax({
-        type: "POST", url: "/_logout", dataType: "json",
+        type: "DELETE", url: "/_session", dataType: "json",
+        username : "_", password : "_",
         complete: function(req) {
           var resp = $.httpData(req, "json");
           if (req.status == 200) {
@@ -304,7 +360,6 @@
             var keys = options["keys"];
             delete options["keys"];
             data = toJSON({ "keys": keys });
-            console.log(data);
           }
           ajax({
               type: type,

Modified: couchdb/trunk/share/www/script/test/cookie_auth.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/test/cookie_auth.js?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/share/www/script/test/cookie_auth.js (original)
+++ couchdb/trunk/share/www/script/test/cookie_auth.js Thu Jan  7 20:02:46 2010
@@ -36,117 +36,192 @@
       usersDb.deleteDb();
       usersDb.createDb();
       
+      // test that the users db is born with the auth ddoc
+      var ddoc = usersDb.open("_design/_auth");
+      T(ddoc.validate_doc_update);
+      
+      // TODO test that changing the config so an existing db becomes the users db installs the ddoc also
+      
       var password = "3.141592653589";
 
       // Create a user
-      T(usersDb.save({
-        _id: "a1",
-        salt: "123",
-        password_sha: hex_sha1(password + "123"),
+      var jasonUserDoc = CouchDB.prepareUserDoc({
         username: "Jason Davies",
-        author: "Jason Davies",
-        type: "user",
-        roles: ["_admin"]
-      }).ok);
-
-      var validationDoc = {
-        _id : "_design/validate",
-        validate_doc_update: "(" + (function (newDoc, oldDoc, userCtx) {
-          // docs should have an author field.
-          if (!newDoc._deleted && !newDoc.author) {
-            throw {forbidden:
-                "Documents must have an author field"};
-          }
-          if (oldDoc && oldDoc.author != userCtx.name) {
-              throw {unauthorized:
-                  "You are not the author of this document. You jerk."+userCtx.name};
-          }
-        }).toString() + ")"
-      };
+        roles: ["dev"]
+      }, password);
+      T(usersDb.save(jasonUserDoc).ok);      
+      
+      var checkDoc = usersDb.open(jasonUserDoc._id);
+      T(checkDoc.username == "Jason Davies");
+      
+      var jchrisUserDoc = CouchDB.prepareUserDoc({
+        username: "jchris@apache.org"
+      }, "funnybone");
+      T(usersDb.save(jchrisUserDoc).ok);
+
+      // make sure we cant create duplicate users
+      var duplicateJchrisDoc = CouchDB.prepareUserDoc({
+        username: "jchris@apache.org"
+      }, "eh, Boo-Boo?");
 
-      T(db.save(validationDoc).ok);
+      try {
+        usersDb.save(duplicateJchrisDoc)
+        T(false && "Can't create duplicate user names. Should have thrown an error.");
+      } catch (e) {
+        T(e.error == "conflict");
+        T(usersDb.last_req.status == 409);
+      }
+      
+      // we can't create _usernames
+      var underscoreUserDoc = CouchDB.prepareUserDoc({
+        username: "_why"
+      }, "copperfield");
 
+      try {
+        usersDb.save(underscoreUserDoc)
+        T(false && "Can't create underscore user names. Should have thrown an error.");
+      } catch (e) {
+        T(e.error == "forbidden");
+        T(usersDb.last_req.status == 403);
+      }
+      
+      // we can't create docs with malformed ids
+      var badIdDoc = CouchDB.prepareUserDoc({
+        username: "foo"
+      }, "bar");
       
+      badIdDoc._id = "org.apache.couchdb:w00x";
 
-      T(CouchDB.login('Jason Davies', password).ok);
-      // update the credentials document
-      var doc = usersDb.open("a1");
-      doc.foo=2;
-      T(usersDb.save(doc).ok);
+      try {
+        usersDb.save(badIdDoc)
+        T(false && "Can't create malformed docids. Should have thrown an error.");
+      } catch (e) {
+        T(e.error == "forbidden");
+        T(usersDb.last_req.status == 403);
+      }
 
-      // Save a document that's missing an author field.
       try {
-        // db has a validation function
-        db.save({foo:1});
-        T(false && "Can't get here. Should have thrown an error 2");
+        usersDb.save(underscoreUserDoc)
+        T(false && "Can't create underscore user names. Should have thrown an error.");
       } catch (e) {
         T(e.error == "forbidden");
-        T(db.last_req.status == 403);
+        T(usersDb.last_req.status == 403);
       }
+      
+      // login works
+      T(CouchDB.login('Jason Davies', password).ok);
+      T(CouchDB.session().name == 'Jason Davies');
+      
+      // update one's own credentials document
+      jasonUserDoc.foo=2;
+      T(usersDb.save(jasonUserDoc).ok);
 
       // TODO should login() throw an exception here?
-      T(!CouchDB.login('Jason Davies', "2.71828").ok);
-      T(!CouchDB.login('Robert Allen Zimmerman', 'd00d').ok);
+       T(!CouchDB.login('Jason Davies', "2.71828").ok);
+       T(!CouchDB.login('Robert Allen Zimmerman', 'd00d').ok);
 
-      // test redirect
-      xhr = CouchDB.request("POST", "/_session?next=/", {
-        headers: {"Content-Type": "application/x-www-form-urlencoded"},
-        body: "username=Jason%20Davies&password="+encodeURIComponent(password)
-      });
-      // should this be a redirect code instead of 200?
-      // The cURL adapter is returning the expected 302 here.
-      // I imagine this has to do with whether the client is willing
-      // to follow the redirect, ie, the browser follows and does a
-      // GET on the returned Location
-      T(xhr.status == 200 || xhr.status == 302);
-      
-      usersDb.deleteDb();
-      // test user creation
-      T(CouchDB.createUser("test", "testpassword", "test@somemail.com", ['read', 'write']).ok);
-      
-      // make sure we create a unique user
-      T(!CouchDB.createUser("test", "testpassword2", "test2@somemail.com", ['read', 'write']).ok);
+       // a failed login attempt should log you out
+       T(CouchDB.session().name != 'Jason Davies');
+
+       // test redirect
+       xhr = CouchDB.request("POST", "/_session?next=/", {
+         headers: {"Content-Type": "application/x-www-form-urlencoded"},
+         body: "username=Jason%20Davies&password="+encodeURIComponent(password)
+       });
+       // should this be a redirect code instead of 200?
+       // The cURL adapter is returning the expected 302 here.
+       // I imagine this has to do with whether the client is willing
+       // to follow the redirect, ie, the browser follows and does a
+       // GET on the returned Location
+       if (xhr.status == 200) {
+         T(/Welcome/.test(xhr.responseText))
+       } else {
+         T(xhr.status == 302)
+         T(xhr.getResponseHeader("Location"))
+       }
+
+      // test users db validations
+      // 
+      // test that you can't update docs unless you are logged in as the user (or are admin)
+      T(CouchDB.login("jchris@apache.org", "funnybone").ok);
+      T(CouchDB.session().name == "jchris@apache.org");
+      T(CouchDB.session().roles.length == 0);
       
-      // test login
-      T(CouchDB.login("test", "testpassword").ok);
-      T(!CouchDB.login('test', "testpassword2").ok);
-      
-      // test update user without changing password
-      T(CouchDB.updateUser("test", "test2@somemail.com").ok);
-      result = usersDb.view("_auth/users", {key: "test"});
-      T(result.rows[0].value['email'] == "test2@somemail.com");
-       
-       
-      // test changing password
-      result = usersDb.view("_auth/users", {key: "test"});
-      T(CouchDB.updateUser("test", "test2@somemail.com", [], "testpassword2", "testpassword").ok);
-      result1 = usersDb.view("_auth/users", {key: "test"});
-      T(result.rows[0].value['password_sha'] != result1.rows[0].value['password_sha']);
+      jasonUserDoc.foo=3;
+
+      try {
+        usersDb.save(jasonUserDoc)
+        T(false && "Can't update someone else's user doc. Should have thrown an error.");
+      } catch (e) {
+        T(e.error == "forbidden");
+        T(usersDb.last_req.status == 403);
+      }
+
+      // test that you can't edit roles unless you are admin
+      jchrisUserDoc.roles = ["foo"];
       
+      try {
+        usersDb.save(jchrisUserDoc)
+        T(false && "Can't set roles unless you are admin. Should have thrown an error.");
+      } catch (e) {
+        T(e.error == "forbidden");
+        T(usersDb.last_req.status == 403);
+      }
       
-      // test changing password with passing old password
-      T(!CouchDB.updateUser("test", "test2@somemail.com", [], "testpassword2").ok);
+      T(CouchDB.logout().ok);
+      T(CouchDB.session().roles[0] == "_admin");      
+
+      jchrisUserDoc.foo = ["foo"];
+      T(usersDb.save(jchrisUserDoc).ok);
+
+      // test that you can't save system (underscore) roles even if you are admin
+      jchrisUserDoc.roles = ["_bar"];
       
-      // test changing password whith bad old password
-      T(!CouchDB.updateUser("test", "test2@somemail.com", [], "testpassword2", "badpasswword").ok);
+      try {
+        usersDb.save(jchrisUserDoc)
+        T(false && "Can't add system roles to user's db. Should have thrown an error.");
+      } catch (e) {
+        T(e.error == "forbidden");
+        T(usersDb.last_req.status == 403);
+      }
       
-      // Only admins can change roles
-      T(!CouchDB.updateUser("test", "test2@somemail.com", ['read', 'write']).ok);
+      // make sure the foo role has been applied
+      T(CouchDB.login("jchris@apache.org", "funnybone").ok);
+      T(CouchDB.session().name == "jchris@apache.org");
+      T(CouchDB.session().roles.indexOf("_admin") == -1);
+      T(CouchDB.session().roles.indexOf("foo") != -1);
       
+      // now let's make jchris a server admin
       T(CouchDB.logout().ok);
+      T(CouchDB.session().roles[0] == "_admin");
+      T(CouchDB.session().name == null);
       
-      T(CouchDB.updateUser("test", "test2@somemail.com").ok);
-      result = usersDb.view("_auth/users", {key: "test"});
-      T(result.rows[0].value['email'] == "test2@somemail.com");
-
-      // test changing password, we don't need to set old password when we are admin
-      result = usersDb.view("_auth/users", {key: "test"});
-      T(CouchDB.updateUser("test", "test2@somemail.com", [], "testpassword3").ok);
-      result1 = usersDb.view("_auth/users", {key: "test"});
-      T(result.rows[0].value['password_sha'] != result1.rows[0].value['password_sha']);
-
-      // Only admins can change roles
-      T(CouchDB.updateUser("test", "test2@somemail.com", ['read']).ok);
+      // set the -hashed- password so the salt matches
+      // todo ask on the ML about this
+      run_on_modified_server([{section: "admins",
+        key: "jchris@apache.org", value: "funnybone"}], function() {
+          T(CouchDB.login("jchris@apache.org", "funnybone").ok);
+          T(CouchDB.session().name == "jchris@apache.org");
+          T(CouchDB.session().roles.indexOf("_admin") != -1);
+          // test that jchris still has the foo role
+          T(CouchDB.session().roles.indexOf("foo") != -1);
+
+          // should work even when user doc has no password
+          jchrisUserDoc = usersDb.open(jchrisUserDoc._id);
+          delete jchrisUserDoc.salt;
+          delete jchrisUserDoc.password_sha;
+          T(usersDb.save(jchrisUserDoc).ok);
+          T(CouchDB.logout().ok);
+          T(CouchDB.login("jchris@apache.org", "funnybone").ok);
+          var s = CouchDB.session();
+          T(s.name == "jchris@apache.org");
+          T(s.roles.indexOf("_admin") != -1);
+          // test session info
+          T(s.info.authenticated == "{couch_httpd_auth, cookie_authentication_handler}");
+          T(s.info.user_db == "test_suite_users");
+          // test that jchris still has the foo role
+          T(CouchDB.session().roles.indexOf("foo") != -1);
+        });      
 
     } finally {
       // Make sure we erase any auth cookies so we don't affect other tests
@@ -157,7 +232,7 @@
   run_on_modified_server(
     [{section: "httpd",
       key: "authentication_handlers",
-      value: "{couch_httpd_auth, cookie_authentication_handler}"},
+      value: "{couch_httpd_auth, cookie_authentication_handler}, {couch_httpd_auth, default_authentication_handler}"},
      {section: "couch_httpd_auth",
       key: "secret", value: generateSecret(64)},
      {section: "couch_httpd_auth",

Modified: couchdb/trunk/share/www/script/test/oauth.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/test/oauth.js?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/share/www/script/test/oauth.js (original)
+++ couchdb/trunk/share/www/script/test/oauth.js Thu Jan  7 20:02:46 2010
@@ -97,6 +97,8 @@
 
       CouchDB.request("GET", "/_sleep?time=50");
 
+      CouchDB.newUuids(2); // so we have one to make the salt
+
       CouchDB.request("PUT", "http://" + host + "/_config/couch_httpd_auth/require_valid_user", {
         headers: {
           "X-Couch-Persist": "false",
@@ -113,7 +115,12 @@
       usersDb.createDb();
         
       // Create a user
-      T(CouchDB.createUser("jason", "testpassword", "test@somemail.com", ['test'], adminBasicAuthHeaderValue()).ok);
+      var jasonUserDoc = CouchDB.prepareUserDoc({
+        username: "jason",
+        roles: ["test"]
+      }, "testpassword");
+      T(usersDb.save(jasonUserDoc).ok);
+
 
       var accessor = {
         consumerSecret: consumerSecret,
@@ -227,7 +234,7 @@
   run_on_modified_server(
     [
      {section: "httpd",
-      key: "WWW-Authenticate", value: 'Basic realm="administrator",OAuth'},
+      key: "WWW-Authenticate", value: 'OAuth'},
      {section: "couch_httpd_auth",
       key: "secret", value: generateSecret(64)},
      {section: "couch_httpd_auth",

Modified: couchdb/trunk/share/www/style/layout.css
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/style/layout.css?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/share/www/style/layout.css (original)
+++ couchdb/trunk/share/www/style/layout.css Thu Jan  7 20:02:46 2010
@@ -211,7 +211,9 @@
   font-size: 80%; opacity: .7; padding: 5px 10px; position: absolute; right: 0;
   bottom: 0; min-height: 1.3em; width: 190px; text-align: right;
 }
-#footer :link, #footer :visited { color: #000; }
+#footer .couch :link, #footer .couch :visited { color: #000; }
+
+#userCtx span { display:none; }
 
 #wrap { background: #fff url(../image/bg.png) 100% 0 repeat-y;
   height: 100%; margin-right: 210px; position: relative;
@@ -258,12 +260,19 @@
   -webkit-box-shadow: 4px 4px 6px #333;
 }
 *html #dialog { width: 33em; }
+body.loading #dialog h2 {
+  background: url(../image/spinner.gif) center no-repeat;
+}
 #dialog.loading { width: 220px; height: 80px; }
 #dialog.loaded { background-image: none; }
 #dialog h2 { background: #666; border-top: 1px solid #555;
   border-bottom: 1px solid #777; color: #ccc; font-size: 110%;
   font-weight: bold; margin: 0 -2em; padding: .35em 2em;
 }
+#dialog h3 {
+      color: #ccc; font-size: 110%;
+      font-weight: bold; margin: 0 -2em; padding: .35em 2em;
+}
 #dialog fieldset { background: #222; border-top: 1px solid #111;
   margin: 0 0 1em; padding: .5em 1em 1em;
   -moz-border-radius-bottomleft: 7px; -moz-border-radius-bottomright: 7px;

Modified: couchdb/trunk/src/couchdb/couch_db.hrl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_db.hrl?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_db.hrl (original)
+++ couchdb/trunk/src/couchdb/couch_db.hrl Thu Jan  7 20:02:46 2010
@@ -107,8 +107,11 @@
 
 
 -record(user_ctx,
-    {name=null,
-    roles=[]
+    {
+    name=null,
+    roles=[],
+    handler,
+    user_doc
     }).
 
 % This should be updated anytime a header change happens that requires more

Modified: couchdb/trunk/src/couchdb/couch_httpd.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_httpd.erl?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_httpd.erl (original)
+++ couchdb/trunk/src/couchdb/couch_httpd.erl Thu Jan  7 20:02:46 2010
@@ -17,6 +17,7 @@
 
 -export([header_value/2,header_value/3,qs_value/2,qs_value/3,qs/1,path/1,absolute_uri/2,body_length/1]).
 -export([verify_is_server_admin/1,unquote/1,quote/1,recv/2,recv_chunked/4,error_info/1]).
+-export([make_fun_spec_strs/1]).
 -export([parse_form/1,json_body/1,json_body_obj/1,body/1,doc_etag/1, make_etag/1, etag_respond/3]).
 -export([primary_header_value/2,partition/1,serve_file/3,serve_file/4, server_header/0]).
 -export([start_chunked_response/3,send_chunk/2,log_request/2]).
@@ -119,8 +120,8 @@
     end.
 
 % SpecStr is "{my_module, my_fun}, {my_module2, my_fun2}"
-make_arity_1_fun_list(SpecStr) ->
-    [make_arity_1_fun(FunSpecStr) || FunSpecStr <- re:split(SpecStr, "(?<=})\\s*,\\s*(?={)", [{return, list}])].
+make_fun_spec_strs(SpecStr) ->
+    [FunSpecStr || FunSpecStr <- re:split(SpecStr, "(?<=})\\s*,\\s*(?={)", [{return, list}])].
 
 stop() ->
     mochiweb_http:stop(?MODULE).
@@ -129,7 +130,7 @@
 handle_request(MochiReq, DefaultFun,
         UrlHandlers, DbUrlHandlers, DesignUrlHandlers) ->
     Begin = now(),
-    AuthenticationFuns = make_arity_1_fun_list(
+    AuthenticationSrcs = make_fun_spec_strs(
             couch_config:get("httpd", "authentication_handlers")),
     % for the path, use the raw path with the query string and fragment
     % removed, but URL quoting left intact
@@ -180,7 +181,7 @@
 
     {ok, Resp} =
     try
-        case authenticate_request(HttpReq, AuthenticationFuns) of
+        case authenticate_request(HttpReq, AuthenticationSrcs) of
         #httpd{} = Req ->
             HandlerFun(Req);
         Response ->
@@ -216,8 +217,10 @@
     couch_stats_collector:increment({httpd, requests}),
     {ok, Resp}.
 
-% Try authentication handlers in order until one returns a result
-authenticate_request(#httpd{user_ctx=#user_ctx{}} = Req, _AuthFuns) ->
+% Try authentication handlers in order until one sets a user_ctx
+% the auth funs also have the option of returning a response
+% move this to couch_httpd_auth?
+authenticate_request(#httpd{user_ctx=#user_ctx{}} = Req, _AuthSrcs) ->
     Req;
 authenticate_request(#httpd{} = Req, []) ->
     case couch_config:get("couch_httpd_auth", "require_valid_user", "false") of
@@ -226,9 +229,15 @@
     "false" ->
         Req#httpd{user_ctx=#user_ctx{}}
     end;
-authenticate_request(#httpd{} = Req, [AuthFun|Rest]) ->
-    authenticate_request(AuthFun(Req), Rest);
-authenticate_request(Response, _AuthFuns) ->
+authenticate_request(#httpd{} = Req, [AuthSrc|Rest]) ->
+    AuthFun = make_arity_1_fun(AuthSrc),
+    R = case AuthFun(Req) of
+        #httpd{user_ctx=#user_ctx{}=UserCtx}=Req2 ->
+            Req2#httpd{user_ctx=UserCtx#user_ctx{handler=?l2b(AuthSrc)}};
+        Else -> Else
+    end,
+    authenticate_request(R, Rest);
+authenticate_request(Response, _AuthSrcs) ->
     Response.
 
 increment_method_stats(Method) ->
@@ -586,6 +595,7 @@
 send_error(#httpd{mochi_req=MochiReq}=Req, Error) ->
     {Code, ErrorStr, ReasonStr} = error_info(Error),
     Headers = if Code == 401 ->
+        % this is where the basic auth popup is triggered
         case MochiReq:get_header_value("X-CouchDB-WWW-Authenticate") of
         undefined ->
             case couch_config:get("httpd", "WWW-Authenticate", nil) of

Modified: couchdb/trunk/src/couchdb/couch_httpd_auth.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_httpd_auth.erl?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_httpd_auth.erl (original)
+++ couchdb/trunk/src/couchdb/couch_httpd_auth.erl Thu Jan  7 20:02:46 2010
@@ -18,7 +18,6 @@
 -export([null_authentication_handler/1]).
 -export([cookie_auth_header/2]).
 -export([handle_session_req/1]).
--export([handle_user_req/1]).
 -export([ensure_users_db_exists/1, get_user/1]).
 
 -import(couch_httpd, [header_value/2, send_json/2,send_json/4, send_method_not_allowed/2]).
@@ -49,6 +48,9 @@
     case AuthorizationHeader of
     "Basic " ++ Base64Value ->
         case string:tokens(?b2l(couch_util:decodeBase64(Base64Value)),":") of
+        ["_", "_"] ->
+            % special name and pass to be logged out
+            nil;
         [User, Pass] ->
             {User, Pass};
         [User] ->
@@ -63,11 +65,22 @@
 default_authentication_handler(Req) ->
     case basic_username_pw(Req) of
     {User, Pass} ->
-        case couch_server:is_admin(User, Pass) of
-        true ->
-            Req#httpd{user_ctx=#user_ctx{name=?l2b(User), roles=[<<"_admin">>]}};
-        false ->
-            throw({unauthorized, <<"Name or password is incorrect.">>})
+        case get_user(?l2b(User)) of
+            nil ->
+                throw({unauthorized, <<"Name or password is incorrect.">>});
+            UserProps ->
+                UserSalt = proplists:get_value(<<"salt">>, UserProps, <<>>),
+                PasswordHash = hash_password(?l2b(Pass), UserSalt),
+                case proplists:get_value(<<"password_sha">>, UserProps, nil) of
+                    ExpectedHash when ExpectedHash == PasswordHash ->                        
+                        Req#httpd{user_ctx=#user_ctx{
+                            name=?l2b(User),
+                            roles=proplists:get_value(<<"roles">>, UserProps, []),
+                            user_doc={UserProps}
+                        }};
+                    _Else ->
+                        throw({unauthorized, <<"Name or password is incorrect.">>})
+                end
         end;
     nil ->
         case couch_server:has_admins() of
@@ -86,138 +99,161 @@
 null_authentication_handler(Req) ->
     Req#httpd{user_ctx=#user_ctx{roles=[<<"_admin">>]}}.
 
-% Cookie auth handler using per-node user db
-cookie_authentication_handler(Req) ->
-    case cookie_auth_user(Req) of
-    % Fall back to default authentication handler
-    nil -> default_authentication_handler(Req);
-    Req2 -> Req2
-    end.
-
-% Cookie auth handler using per-db user db
-% cookie_authentication_handler(#httpd{path_parts=Path}=Req) ->
-%     case Path of
-%     [DbName|_] ->
-%         case cookie_auth_user(Req, DbName) of
-%         nil -> default_authentication_handler(Req);
-%         Req2 -> Req2
-%         end;
-%     _Else ->
-%         % Fall back to default authentication handler
-%         default_authentication_handler(Req)
-%     end.
-
 % maybe we can use hovercraft to simplify running this view query
+% rename to get_user_from_users_db
 get_user(UserName) ->
-    % In the future this will be pluggable. For now we check the .ini first,
-    % then fall back to querying the db.
     case couch_config:get("admins", ?b2l(UserName)) of
     "-hashed-" ++ HashedPwdAndSalt ->
+        % the username is an admin, now check to see if there is a user doc
+        % which has a matching username, salt, and password_sha
         [HashedPwd, Salt] = string:tokens(HashedPwdAndSalt, ","),
-        [{<<"roles">>, [<<"_admin">>]},
-          {<<"salt">>, ?l2b(Salt)},
-          {<<"password_sha">>, ?l2b(HashedPwd)}];
-    _ ->
-        DesignId = <<"_design/_auth">>,
-        ViewName = <<"users">>,
-        % if the design doc or the view doesn't exist, then make it
-        DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
-        {ok, Db} = ensure_users_db_exists(?l2b(DbName)),
-
-        ensure_users_view_exists(Db, DesignId, ViewName),
-
-        case (catch couch_view:get_map_view(Db, DesignId, ViewName, nil)) of
-        {ok, View, _Group} ->
-            FoldFun = fun({_, Value}, _, {_}) -> {stop, Value} end,
-            {ok, _, {Result}} = couch_view:fold(View, FoldFun, {nil},
-                    [{start_key, {UserName, ?MIN_STR}},{end_key, {UserName, ?MAX_STR}}]),
-            Result;
-        {not_found, _Reason} ->
-            nil
-            % case (catch couch_view:get_reduce_view(Db, DesignId, ViewName, nil)) of
-            % {ok, _ReduceView, _Group} ->
-            %     not_implemented;
-            % {not_found, _Reason} ->
-            %     nil
-            % end
-        end
+        case get_user_props_from_db(UserName) of
+            nil ->        
+                [{<<"roles">>, [<<"_admin">>]},
+                  {<<"salt">>, ?l2b(Salt)},
+                  {<<"password_sha">>, ?l2b(HashedPwd)}];
+            UserProps when is_list(UserProps) ->
+                DocRoles = proplists:get_value(<<"roles">>, UserProps),
+                [{<<"roles">>, [<<"_admin">> | DocRoles]},
+                  {<<"salt">>, ?l2b(Salt)},
+                  {<<"password_sha">>, ?l2b(HashedPwd)},
+                  {<<"user_doc">>, {UserProps}}]
+        end;
+    Else ->
+        get_user_props_from_db(UserName)
     end.
-    
+
+get_user_props_from_db(UserName) ->
+    DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
+    {ok, Db} = ensure_users_db_exists(?l2b(DbName)),
+    DocId = <<"org.couchdb.user:", UserName/binary>>,
+    try couch_httpd_db:couch_doc_open(Db, DocId, nil, []) of
+        #doc{}=Doc ->
+            {DocProps} = couch_query_servers:json_doc(Doc),
+            DocProps
+    catch
+        throw:Throw ->
+            nil        
+    end.
+
+% this should handle creating the ddoc
 ensure_users_db_exists(DbName) ->
     case couch_db:open(DbName, [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of
     {ok, Db} ->
+        ensure_auth_ddoc_exists(Db, <<"_design/_auth">>),
         {ok, Db};
     _Error -> 
-        ?LOG_ERROR("Create the db ~p", [DbName]),
         {ok, Db} = couch_db:create(DbName, [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]),
-        ?LOG_ERROR("Created the db ~p", [DbName]),
+        ensure_auth_ddoc_exists(Db, <<"_design/_auth">>),
         {ok, Db}
     end.
     
-ensure_users_view_exists(Db, DDocId, VName) -> 
+ensure_auth_ddoc_exists(Db, DDocId) -> 
     try couch_httpd_db:couch_doc_open(Db, DDocId, nil, []) of
         _Foo -> ok
     catch 
         _:Error -> 
-            ?LOG_ERROR("create the design document ~p : ~p", [DDocId, Error]),
             % create the design document
-            {ok, AuthDesign} = auth_design_doc(DDocId, VName),
+            {ok, AuthDesign} = auth_design_doc(DDocId),
             {ok, _Rev} = couch_db:update_doc(Db, AuthDesign, []),
-            ?LOG_ERROR("created the design document", []),
             ok
     end.
 
-auth_design_doc(DocId, VName) ->
+% add the validation function here
+auth_design_doc(DocId) ->
     DocProps = [
         {<<"_id">>, DocId},
         {<<"language">>,<<"javascript">>},
         {<<"views">>,
-            {[{VName,
+            {[{<<"users">>,
                 {[{<<"map">>,
                     <<"function (doc) {\n if (doc.type == \"user\") {\n        emit(doc.username, doc);\n}\n}">>
                 }]}
             }]}
+        },
+        {
+            <<"validate_doc_update">>,
+            <<"function(newDoc, oldDoc, userCtx) {
+                if (newDoc.type != 'user') {
+                    return;
+                } // we only validate user docs for now
+                if (!newDoc.username) {
+                    throw({forbidden : 'doc.username is required'});
+                }
+                if (!(newDoc.roles && (typeof newDoc.roles.length != 'undefined') )) {
+                    throw({forbidden : 'doc.roles must be an array'});
+                }
+                if (newDoc._id != 'org.couchdb.user:'+newDoc.username) {
+                    throw({forbidden : 'Docid must be of the form org.couchdb.user:username'});
+                }
+                if (oldDoc) { // validate all updates
+                    if (oldDoc.username != newDoc.username) {
+                      throw({forbidden : 'Usernames may not be changed.'});
+                    }
+                }
+                if (newDoc.password_sha && !newDoc.salt) {
+                    throw({forbidden : 'Users with password_sha must have a salt. See /_utils/script/couch.js for example code.'});
+                }
+                if (userCtx.roles.indexOf('_admin') == -1) { // not an admin
+                    if (oldDoc) { // validate non-admin updates
+                        if (userCtx.name != newDoc.username) {
+                          throw({forbidden : 'You may only update your own user document.'});
+                        }
+                        // validate role updates
+                        var oldRoles = oldDoc.roles.sort();
+                        var newRoles = newDoc.roles.sort();
+                        if (oldRoles.length != newRoles.length) {
+                            throw({forbidden : 'Only _admin may edit roles'});
+                        }
+                        for (var i=0; i < oldRoles.length; i++) {
+                            if (oldRoles[i] != newRoles[i]) {
+                                throw({forbidden : 'Only _admin may edit roles'});
+                            }
+                        };
+                    } else if (newDoc.roles.length > 0) {
+                        throw({forbidden : 'Only _admin may set roles'});
+                    }
+                }
+                // no system roles in users db
+                for (var i=0; i < newDoc.roles.length; i++) {
+                    if (newDoc.roles[i][0] == '_') {
+                        throw({forbidden : 'No system roles (starting with underscore) in users db.'});
+                    }
+                };
+                // no system names as usernames
+                if (newDoc.username[0] == '_') {
+                    throw({forbidden : 'Username may not start with underscore.'});
+                }
+            }">>
         }],
     {ok, couch_doc:from_json_obj({DocProps})}.
-    
-
-user_doc(DocId, Username, UserSalt, PasswordHash, Email, Active, Roles) ->
-    user_doc(DocId, Username, UserSalt, PasswordHash, Email, Active, Roles, nil).
-user_doc(DocId, Username, UserSalt, PasswordHash, Email, Active, Roles, Rev) ->
-    DocProps = [
-        {<<"_id">>, DocId},
-        {<<"type">>, <<"user">>},
-        {<<"username">>, Username},
-        {<<"password_sha">>, PasswordHash},
-        {<<"salt">>, UserSalt},
-        {<<"email">>, Email},
-        {<<"active">>, Active},
-        {<<"roles">>, Roles}],
-    DocProps1 = case Rev of
-    nil -> DocProps;
-    _Rev -> 
-        [{<<"_rev">>, Rev}] ++ DocProps
-    end,
-    {ok, couch_doc:from_json_obj({DocProps1})}.
 
-cookie_auth_user(#httpd{mochi_req=MochiReq}=Req) ->
+cookie_authentication_handler(#httpd{mochi_req=MochiReq}=Req) ->
     case MochiReq:get_cookie_value("AuthSession") of
-    undefined -> nil;
-    [] -> nil;
+    undefined -> Req;
+    [] -> Req;
     Cookie -> 
-        AuthSession = couch_util:decodeBase64Url(Cookie),
-        [User, TimeStr | HashParts] = string:tokens(?b2l(AuthSession), ":"),
+        [User, TimeStr | HashParts] = try
+            AuthSession = couch_util:decodeBase64Url(Cookie),
+            [A, B | Cs] = string:tokens(?b2l(AuthSession), ":")
+        catch
+            _:Error ->
+                Reason = <<"Malformed AuthSession cookie. Please clear your cookies.">>,
+                throw({bad_request, Reason})
+        end,
         % Verify expiry and hash
         {NowMS, NowS, _} = erlang:now(),
         CurrentTime = NowMS * 1000000 + NowS,
         case couch_config:get("couch_httpd_auth", "secret", nil) of
-        nil -> nil;
+        nil -> 
+            ?LOG_ERROR("cookie auth secret is not set",[]),
+            Req;
         SecretStr ->
             Secret = ?l2b(SecretStr),
             case get_user(?l2b(User)) of
-            nil -> nil;
-            Result ->
-                UserSalt = proplists:get_value(<<"salt">>, Result, <<"">>),
+            nil -> Req;
+            UserProps ->
+                UserSalt = proplists:get_value(<<"salt">>, UserProps, <<"">>),
                 FullSecret = <<Secret/binary, UserSalt/binary>>,
                 ExpectedHash = crypto:sha_mac(FullSecret, User ++ ":" ++ TimeStr),
                 Hash = ?l2b(string:join(HashParts, ":")),
@@ -230,10 +266,11 @@
                         ?LOG_DEBUG("Successful cookie auth as: ~p", [User]),
                         Req#httpd{user_ctx=#user_ctx{
                             name=?l2b(User),
-                            roles=proplists:get_value(<<"roles">>, Result, [])
+                            roles=proplists:get_value(<<"roles">>, UserProps, []),
+                            user_doc=proplists:get_value(<<"user_doc">>, UserProps, null)
                         }, auth={FullSecret, TimeLeft < Timeout*0.9}};
                     _Else ->
-                        nil
+                        Req
                 end
             end
         end
@@ -270,8 +307,19 @@
 hash_password(Password, Salt) ->
     ?l2b(couch_util:to_hex(crypto:sha(<<Password/binary, Salt/binary>>))).
 
+ensure_cookie_auth_secret() ->
+    case couch_config:get("couch_httpd_auth", "secret", nil) of
+        nil ->
+            NewSecret = ?b2l(couch_uuids:random()),
+            couch_config:set("couch_httpd_auth", "secret", NewSecret),
+            NewSecret;
+        Secret -> Secret
+    end.
+
+% session handlers
 % Login handler with user db
-handle_login_req(#httpd{method='POST', mochi_req=MochiReq}=Req) ->
+% TODO this should also allow a JSON POST
+handle_session_req(#httpd{method='POST', mochi_req=MochiReq}=Req) ->
     ReqBody = MochiReq:recv_body(),
     Form = case MochiReq:get_primary_header_value("content-type") of
         "application/x-www-form-urlencoded" ++ _ ->
@@ -281,6 +329,7 @@
     end,
     UserName = ?l2b(proplists:get_value("username", Form, "")),
     Password = ?l2b(proplists:get_value("password", Form, "")),
+    ?LOG_DEBUG("Attempt Login: ~s",[UserName]),
     User = case get_user(UserName) of
         nil -> [];
         Result -> Result
@@ -289,10 +338,12 @@
     PasswordHash = hash_password(Password, UserSalt),
     case proplists:get_value(<<"password_sha">>, User, nil) of
         ExpectedHash when ExpectedHash == PasswordHash ->
-            Secret = ?l2b(couch_config:get("couch_httpd_auth", "secret", nil)),
+            % setup the session cookie
+            Secret = ?l2b(ensure_cookie_auth_secret()),
             {NowMS, NowS, _} = erlang:now(),
             CurrentTime = NowMS * 1000000 + NowS,
             Cookie = cookie_auth_cookie(?b2l(UserName), <<Secret/binary, UserSalt/binary>>, CurrentTime),
+            % TODO document the "next" feature in Futon
             {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
                 nil ->
                     {200, [Cookie]};
@@ -300,32 +351,38 @@
                     {302, [Cookie, {"Location", couch_httpd:absolute_uri(Req, Redirect)}]}
             end,
             send_json(Req#httpd{req_body=ReqBody}, Code, Headers,
-                {[{ok, true}]});
+                {[
+                    {ok, true},
+                    {name, proplists:get_value(<<"username">>, User, null)},
+                    {roles, proplists:get_value(<<"roles">>, User, [])},
+                    {user_doc, proplists:get_value(<<"user_doc">>, User, null)}
+                ]});
         _Else ->
-            throw({unauthorized, <<"Name or password is incorrect.">>})
-    end.
-
-% Session Handler
-
-handle_session_req(#httpd{method='POST'}=Req) ->
-    handle_login_req(Req);
+            % clear the session
+            Cookie = mochiweb_cookies:cookie("AuthSession", "", [{path, "/"}, {http_only, true}]),
+            send_json(Req, 401, [Cookie], {[{error, <<"unauthorized">>},{reason, <<"Name or password is incorrect.">>}]})
+    end;
+% get user info
 handle_session_req(#httpd{method='GET', user_ctx=UserCtx}=Req) ->
-    % whoami
     Name = UserCtx#user_ctx.name,
-    Roles = UserCtx#user_ctx.roles,
     ForceLogin = couch_httpd:qs_value(Req, "basic", "false"),
     case {Name, ForceLogin} of
         {null, "true"} ->
             throw({unauthorized, <<"Please login.">>});
-        _False -> ok
-    end,
-    send_json(Req, {[
-        {ok, true},
-        {name, Name},
-        {roles, Roles}
-    ]});
+        {Name, _} ->
+            send_json(Req, {[
+                {ok, true},
+                {name, Name},
+                {roles, UserCtx#user_ctx.roles},
+                {info, {[
+                    {user_db, ?l2b(couch_config:get("couch_httpd_auth", "authentication_db"))},
+                    {handlers, [?l2b(H) || H <- couch_httpd:make_fun_spec_strs(
+                            couch_config:get("httpd", "authentication_handlers"))]}
+                ] ++ maybe_value(authenticated, UserCtx#user_ctx.handler)}}
+            ] ++ maybe_value(user_doc, UserCtx#user_ctx.user_doc)})
+    end;
+% logout by deleting the session
 handle_session_req(#httpd{method='DELETE'}=Req) ->
-    % logout
     Cookie = mochiweb_cookies:cookie("AuthSession", "", [{path, "/"}, {http_only, true}]),
     {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
         nil ->
@@ -336,135 +393,9 @@
     send_json(Req, Code, Headers, {[{ok, true}]});
 handle_session_req(Req) ->
     send_method_not_allowed(Req, "GET,HEAD,POST,DELETE").
-    
-create_user_req(#httpd{method='POST', mochi_req=MochiReq}=Req, Db) ->
-    ReqBody = MochiReq:recv_body(),
-    Form = case MochiReq:get_primary_header_value("content-type") of
-        "application/x-www-form-urlencoded" ++ _ ->
-            ?LOG_INFO("body parsed ~p", [mochiweb_util:parse_qs(ReqBody)]),
-            mochiweb_util:parse_qs(ReqBody);
-        _ ->
-            []
-    end,
-    Roles = proplists:get_all_values("roles", Form),
-    UserName = ?l2b(proplists:get_value("username", Form, "")),
-    Password = ?l2b(proplists:get_value("password", Form, "")),
-    Email = ?l2b(proplists:get_value("email", Form, "")),
-    Active = couch_httpd_view:parse_bool_param(proplists:get_value("active", Form, "true")),
-    case get_user(UserName) of
-    nil -> 
-        Roles1 = case Roles of
-        [] -> Roles;
-        _ ->
-            ok = couch_httpd:verify_is_server_admin(Req),
-            [?l2b(R) || R <- Roles]
-        end,
-            
-        UserSalt = couch_uuids:random(),
-        PasswordHash = hash_password(Password, UserSalt),
-        DocId = couch_uuids:random(),
-        {ok, UserDoc} = user_doc(DocId, UserName, UserSalt, PasswordHash, Email, Active, Roles1),
-        {ok, _Rev} = couch_db:update_doc(Db, UserDoc, []),
-        ?LOG_DEBUG("User ~s (~s) with password, ~s created.", [?b2l(UserName), ?b2l(DocId), ?b2l(Password)]),
-        {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
-            nil ->
-                {200, []};
-            Redirect ->
-                {302, [{"Location", couch_httpd:absolute_uri(Req, Redirect)}]}
-        end,
-        send_json(Req, Code, Headers, {[{ok, true}]});
-    _Result -> 
-        ?LOG_DEBUG("Can't create ~s: already exists", [?b2l(UserName)]),
-         throw({forbidden, <<"User already exists.">>})
-    end.
-
-update_user_req(#httpd{method='PUT', mochi_req=MochiReq, user_ctx=UserCtx}=Req, Db, UserName) ->
-    Name = UserCtx#user_ctx.name,
-    UserRoles = UserCtx#user_ctx.roles,
-    case User = get_user(UserName) of
-    nil ->
-        throw({not_found, <<"User doesn't exist">>});
-    _Result ->
-        ReqBody = MochiReq:recv_body(),
-        Form = case MochiReq:get_primary_header_value("content-type") of
-            "application/x-www-form-urlencoded" ++ _ ->
-                mochiweb_util:parse_qs(ReqBody);
-            _ ->
-                []
-        end,
-        Roles = proplists:get_all_values("roles", Form),
-        Password = ?l2b(proplists:get_value("password", Form, "")),
-        Email = ?l2b(proplists:get_value("email", Form, "")),
-        Active = couch_httpd_view:parse_bool_param(proplists:get_value("active", Form, "true")),
-        OldPassword = proplists:get_value("old_password", Form, ""),
-        OldPassword1 = ?l2b(OldPassword),
-        UserSalt = proplists:get_value(<<"salt">>, User, <<>>),
-        OldRev = proplists:get_value(<<"_rev">>, User, <<>>),
-        DocId = proplists:get_value(<<"_id">>, User, <<>>),
-        CurrentPasswordHash = proplists:get_value(<<"password_sha">>, User, nil),
-        
-        
-        Roles1 = case Roles of
-        [] -> Roles;
-        _ ->
-            ok = couch_httpd:verify_is_server_admin(Req),
-            [?l2b(R) || R <- Roles]
-        end,
-        
-        PasswordHash = case lists:member(<<"_admin">>, UserRoles) of
-        true ->
-            case Password of
-                <<>> -> CurrentPasswordHash;
-                _Else ->
-                    hash_password(Password, UserSalt)
-	    end;
-        false when Name =:= UserName ->
-            %% for user we test old password before allowing change
-            case Password of
-                <<>> -> 
-                    CurrentPasswordHash;
-                _P when OldPassword =:= [] ->
-                    throw({forbidden, <<"Old password is incorrect.">>});
-                _Else ->
-                    OldPasswordHash = hash_password(OldPassword1, UserSalt),
-                    ?LOG_DEBUG("~p == ~p", [CurrentPasswordHash, OldPasswordHash]),
-                    case CurrentPasswordHash of
-                        ExpectedHash when ExpectedHash =:= OldPasswordHash ->
-                            hash_password(Password, UserSalt);
-                        _ ->
-                            throw({forbidden, <<"Old password is incorrect.">>})
-		    end
-                end;
-        _ ->
-            throw({forbidden, <<"You aren't allowed to change this password.">>})
-        end, 
-        {ok, UserDoc} = user_doc(DocId, UserName, UserSalt, PasswordHash, Email, Active, Roles1, OldRev),
-        {ok, _Rev} = couch_db:update_doc(Db, UserDoc, []),
-        ?LOG_DEBUG("User ~s (~s)updated.", [?b2l(UserName), ?b2l(DocId)]),
-        {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
-        nil -> {200, []};
-        Redirect ->
-            {302, [{"Location", couch_httpd:absolute_uri(Req, Redirect)}]}
-        end,
-        send_json(Req, Code, Headers, {[{ok, true}]})
-    end.
 
-handle_user_req(#httpd{method='POST'}=Req) ->
-    DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
-    ensure_users_db_exists(?l2b(DbName)),
-    {ok, Db} = couch_db:open(?l2b(DbName),
-			     [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]),
-    create_user_req(Req, Db);
-handle_user_req(#httpd{method='PUT', path_parts=[_]}=_Req) ->
-    throw({bad_request, <<"Username is missing">>});
-handle_user_req(#httpd{method='PUT', path_parts=[_, UserName]}=Req) ->
-    DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
-    ensure_users_db_exists(?l2b(DbName)),
-    {ok, Db} = couch_db:open(?l2b(DbName),
-			     [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]),
-    update_user_req(Req, Db, UserName);
-handle_user_req(Req) ->
-    couch_httpd:send_method_not_allowed(Req, "POST,PUT").
+maybe_value(Key, undefined) -> [];
+maybe_value(Key, Else) -> [{Key, Else}].
 
 to_int(Value) when is_binary(Value) ->
     to_int(?b2l(Value)); 
@@ -472,25 +403,3 @@
     list_to_integer(Value);
 to_int(Value) when is_integer(Value) ->
     Value.
-
-% % Login handler
-% handle_login_req(#httpd{method='POST'}=Req) ->
-%     DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
-%     case couch_db:open(?l2b(DbName), [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of
-%         {ok, Db} -> handle_login_req(Req, Db)
-%     end;
-% handle_login_req(Req) ->
-%     send_method_not_allowed(Req, "POST").
-% 
-% % Logout handler
-% handle_logout_req(#httpd{method='POST'}=Req) ->
-%     Cookie = mochiweb_cookies:cookie("AuthSession", "", [{path, "/"}, {http_only, true}]),
-%     {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
-%         nil ->
-%             {200, [Cookie]};
-%         Redirect ->
-%             {302, [Cookie, {"Location", couch_httpd:absolute_uri(Req, Redirect)}]}
-%     end,
-%     send_json(Req, Code, Headers, {[{ok, true}]});
-% handle_logout_req(Req) ->
-%     send_method_not_allowed(Req, "POST").

Modified: couchdb/trunk/src/couchdb/couch_httpd_db.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_httpd_db.erl?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_httpd_db.erl (original)
+++ couchdb/trunk/src/couchdb/couch_httpd_db.erl Thu Jan  7 20:02:46 2010
@@ -298,13 +298,23 @@
 
 create_db_req(#httpd{user_ctx=UserCtx}=Req, DbName) ->
     ok = couch_httpd:verify_is_server_admin(Req),
-    case couch_server:create(DbName, [{user_ctx, UserCtx}]) of
-    {ok, Db} ->
-        couch_db:close(Db),
-        DocUrl = absolute_uri(Req, "/" ++ couch_util:url_encode(DbName)),
-        send_json(Req, 201, [{"Location", DocUrl}], {[{ok, true}]});
-    Error ->
-        throw(Error)
+    LDbName = ?b2l(DbName),
+    case couch_config:get("couch_httpd_auth", "authentication_db") of
+        LDbName -> 
+            % make sure user's db always has the auth ddoc
+            {ok, Db} = couch_httpd_auth:ensure_users_db_exists(DbName),
+            couch_db:close(Db),
+            DbUrl = absolute_uri(Req, "/" ++ couch_util:url_encode(DbName)),
+            send_json(Req, 201, [{"Location", DbUrl}], {[{ok, true}]});
+        _Else ->
+            case couch_server:create(DbName, [{user_ctx, UserCtx}]) of
+            {ok, Db} ->
+                couch_db:close(Db),
+                DbUrl = absolute_uri(Req, "/" ++ couch_util:url_encode(DbName)),
+                send_json(Req, 201, [{"Location", DbUrl}], {[{ok, true}]});
+            Error ->
+                throw(Error)
+            end
     end.
 
 delete_db_req(#httpd{user_ctx=UserCtx}=Req, DbName) ->
@@ -317,6 +327,15 @@
     end.
 
 do_db_req(#httpd{user_ctx=UserCtx,path_parts=[DbName|_]}=Req, Fun) ->
+    LDbName = ?b2l(DbName),
+    % I hope this lookup is cheap.
+    case couch_config:get("couch_httpd_auth", "authentication_db") of
+        LDbName -> 
+            % make sure user's db always has the auth ddoc
+            {ok, ADb} = couch_httpd_auth:ensure_users_db_exists(DbName),
+            couch_db:close(ADb);
+        _Else -> ok
+    end,
     case couch_db:open(DbName, [{user_ctx, UserCtx}]) of
     {ok, Db} ->
         try
@@ -553,7 +572,7 @@
 % as slashes in document IDs must otherwise be URL encoded.
 db_req(#httpd{method='GET',mochi_req=MochiReq, path_parts=[DbName,<<"_design/",_/binary>>|_]}=Req, _Db) ->
     PathFront = "/" ++ couch_httpd:quote(binary_to_list(DbName)) ++ "/",
-    [PathFront|PathTail] = re:split(MochiReq:get(raw_path), "_design%2F",
+    [_|PathTail] = re:split(MochiReq:get(raw_path), "_design%2F",
         [{return, list}]),
     couch_httpd:send_redirect(Req, PathFront ++ "_design/" ++
         mochiweb_util:join(PathTail, "_design%2F"));

Modified: couchdb/trunk/src/couchdb/couch_httpd_oauth.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_httpd_oauth.erl?rev=896989&r1=896988&r2=896989&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_httpd_oauth.erl (original)
+++ couchdb/trunk/src/couchdb/couch_httpd_oauth.erl Thu Jan  7 20:02:46 2010
@@ -36,8 +36,7 @@
 
 % Look up the consumer key and get the roles to give the consumer
 set_user_ctx(Req, AccessToken) ->
-    DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
-    {ok, _Db} = couch_httpd_auth:ensure_users_db_exists(?l2b(DbName)),
+    % TODO move to db storage
     Name = case couch_config:get("oauth_token_users", AccessToken) of
         undefined -> throw({bad_request, unknown_oauth_token});
         Value -> ?l2b(Value)