couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From dam...@apache.org
Subject svn commit: r719160 - in /incubator/couchdb/trunk: etc/couchdb/ share/www/script/ src/couchdb/
Date Thu, 20 Nov 2008 04:42:45 GMT
Author: damien
Date: Wed Nov 19 20:42:43 2008
New Revision: 719160

URL: http://svn.apache.org/viewvc?rev=719160&view=rev
Log:
Nearly completed security/validation work. Still needs replication testing.

Modified:
    incubator/couchdb/trunk/etc/couchdb/default.ini.tpl.in
    incubator/couchdb/trunk/share/www/script/couch.js
    incubator/couchdb/trunk/share/www/script/couch_tests.js
    incubator/couchdb/trunk/src/couchdb/couch_btree.erl
    incubator/couchdb/trunk/src/couchdb/couch_config.erl
    incubator/couchdb/trunk/src/couchdb/couch_db.erl
    incubator/couchdb/trunk/src/couchdb/couch_db.hrl
    incubator/couchdb/trunk/src/couchdb/couch_db_updater.erl
    incubator/couchdb/trunk/src/couchdb/couch_httpd.erl
    incubator/couchdb/trunk/src/couchdb/couch_httpd_db.erl
    incubator/couchdb/trunk/src/couchdb/couch_httpd_misc_handlers.erl
    incubator/couchdb/trunk/src/couchdb/couch_query_servers.erl
    incubator/couchdb/trunk/src/couchdb/couch_rep.erl
    incubator/couchdb/trunk/src/couchdb/couch_server.erl
    incubator/couchdb/trunk/src/couchdb/couch_server_sup.erl
    incubator/couchdb/trunk/src/couchdb/couch_util.erl

Modified: incubator/couchdb/trunk/etc/couchdb/default.ini.tpl.in
URL: http://svn.apache.org/viewvc/incubator/couchdb/trunk/etc/couchdb/default.ini.tpl.in?rev=719160&r1=719159&r2=719160&view=diff
==============================================================================
--- incubator/couchdb/trunk/etc/couchdb/default.ini.tpl.in (original)
+++ incubator/couchdb/trunk/etc/couchdb/default.ini.tpl.in Wed Nov 19 20:42:43 2008
@@ -11,6 +11,7 @@
 [httpd]
 port = 5984
 bind_address = 127.0.0.1
+authentication_handler = {couch_httpd, default_authentication_handler}
 
 [log]
 file = %localstatelogdir%/couch.log

Modified: incubator/couchdb/trunk/share/www/script/couch.js
URL: http://svn.apache.org/viewvc/incubator/couchdb/trunk/share/www/script/couch.js?rev=719160&r1=719159&r2=719160&view=diff
==============================================================================
--- incubator/couchdb/trunk/share/www/script/couch.js [utf-8] (original)
+++ incubator/couchdb/trunk/share/www/script/couch.js [utf-8] Wed Nov 19 20:42:43 2008
@@ -21,7 +21,7 @@
   // use this to check result http status and headers.
   this.last_req = null;
   
-  request = function(method, uri, requestOptions) {
+  this.request = function(method, uri, requestOptions) {
       requestOptions = requestOptions || {}
       requestOptions.headers = combine(requestOptions.headers, httpHeaders)
       return CouchDB.request(method, uri, requestOptions);
@@ -29,14 +29,14 @@
 
   // Creates the database on the server
   this.createDb = function() {
-    this.last_req = request("PUT", this.uri);
+    this.last_req = this.request("PUT", this.uri);
     CouchDB.maybeThrowError(this.last_req);
     return JSON.parse(this.last_req.responseText);
   }
 
   // Deletes the database on the server
   this.deleteDb = function() {
-    this.last_req = request("DELETE", this.uri);
+    this.last_req = this.request("DELETE", this.uri);
     if (this.last_req.status == 404)
       return false;
     CouchDB.maybeThrowError(this.last_req);
@@ -48,7 +48,7 @@
     if (doc._id == undefined)
       doc._id = CouchDB.newUuids(1)[0];
 
-    this.last_req = request("PUT", this.uri  + 
+    this.last_req = this.request("PUT", this.uri  + 
         encodeURIComponent(doc._id) + encodeOptions(options),
         {body: JSON.stringify(doc)});
     CouchDB.maybeThrowError(this.last_req);
@@ -59,7 +59,7 @@
 
   // Open a document from the database
   this.open = function(docId, options) {
-    this.last_req = request("GET", this.uri + encodeURIComponent(docId) + encodeOptions(options));
+    this.last_req = this.request("GET", this.uri + encodeURIComponent(docId) + encodeOptions(options));
     if (this.last_req.status == 404)
       return null;
     CouchDB.maybeThrowError(this.last_req);
@@ -68,7 +68,7 @@
 
   // Deletes a document from the database
   this.deleteDoc = function(doc) {
-    this.last_req = request("DELETE", this.uri + encodeURIComponent(doc._id) + "?rev=" + doc._rev);
+    this.last_req = this.request("DELETE", this.uri + encodeURIComponent(doc._id) + "?rev=" + doc._rev);
     CouchDB.maybeThrowError(this.last_req);
     var result = JSON.parse(this.last_req.responseText);
     doc._rev = result.rev; //record rev in input document
@@ -78,7 +78,7 @@
 
   // Deletes an attachment from a document
   this.deleteDocAttachment = function(doc, attachment_name) {
-    this.last_req = request("DELETE", this.uri + encodeURIComponent(doc._id) + "/" + attachment_name + "?rev=" + doc._rev);
+    this.last_req = this.request("DELETE", this.uri + encodeURIComponent(doc._id) + "/" + attachment_name + "?rev=" + doc._rev);
     CouchDB.maybeThrowError(this.last_req);
     var result = JSON.parse(this.last_req.responseText);
     doc._rev = result.rev; //record rev in input document
@@ -98,7 +98,7 @@
       if (docs[i]._id == undefined)
         docs[i]._id = newUuids.pop();
     }
-    this.last_req = request("POST", this.uri + "_bulk_docs" + encodeOptions(options), {
+    this.last_req = this.request("POST", this.uri + "_bulk_docs" + encodeOptions(options), {
       body: JSON.stringify({"docs": docs})
     });
     CouchDB.maybeThrowError(this.last_req);
@@ -123,7 +123,7 @@
         reduceFun = reduceFun.toSource ? reduceFun.toSource() : "(" + reduceFun.toString() + ")";
       body.reduce = reduceFun;
     }
-    this.last_req = request("POST", this.uri + "_temp_view" + encodeOptions(options), {
+    this.last_req = this.request("POST", this.uri + "_temp_view" + encodeOptions(options), {
       headers: {"Content-Type": "application/json"},
       body: JSON.stringify(body)
     });
@@ -133,10 +133,10 @@
 
   this.view = function(viewname, options, keys) {
     if(!keys) {
-      this.last_req = request("GET", this.uri + "_view/" +
+      this.last_req = this.request("GET", this.uri + "_view/" +
           viewname + encodeOptions(options));      
     } else {
-      this.last_req = request("POST", this.uri + "_view/" + 
+      this.last_req = this.request("POST", this.uri + "_view/" + 
         viewname + encodeOptions(options), {
         headers: {"Content-Type": "application/json"},
         body: JSON.stringify({keys:keys})
@@ -150,16 +150,16 @@
 
   // gets information about the database
   this.info = function() {
-    this.last_req = request("GET", this.uri);
+    this.last_req = this.request("GET", this.uri);
     CouchDB.maybeThrowError(this.last_req);
     return JSON.parse(this.last_req.responseText);
   }
 
   this.allDocs = function(options,keys) {
     if(!keys) {
-      this.last_req = request("GET", this.uri + "_all_docs" + encodeOptions(options));      
+      this.last_req = this.request("GET", this.uri + "_all_docs" + encodeOptions(options));      
     } else {
-      this.last_req = request("POST", this.uri + "_all_docs" + encodeOptions(options), {
+      this.last_req = this.request("POST", this.uri + "_all_docs" + encodeOptions(options), {
         headers: {"Content-Type": "application/json"},
         body: JSON.stringify({keys:keys})
       });      
@@ -171,9 +171,9 @@
   this.allDocsBySeq = function(options,keys) {
     var req = null;
     if(!keys) {
-      req = request("GET", this.uri + "_all_docs_by_seq" + encodeOptions(options));      
+      req = this.request("GET", this.uri + "_all_docs_by_seq" + encodeOptions(options));      
     } else {
-      req = request("POST", this.uri + "_all_docs_by_seq" + encodeOptions(options), {
+      req = this.request("POST", this.uri + "_all_docs_by_seq" + encodeOptions(options), {
         headers: {"Content-Type": "application/json"},
         body: JSON.stringify({keys:keys})
       });      
@@ -183,11 +183,25 @@
   }
 
   this.compact = function() {
-    this.last_req = request("POST", this.uri + "_compact");
+    this.last_req = this.request("POST", this.uri + "_compact");
     CouchDB.maybeThrowError(this.last_req);
     return JSON.parse(this.last_req.responseText);
   }
-
+  
+  this.setAdmins = function(adminsArray) {
+    this.last_req = this.request("PUT", this.uri + "_admins",{
+      body:JSON.stringify(adminsArray)
+    });
+    CouchDB.maybeThrowError(this.last_req);
+    return JSON.parse(this.last_req.responseText);
+  }
+  
+  this.getAdmins = function() {
+    this.last_req = this.request("GET", this.uri + "_admins");
+    CouchDB.maybeThrowError(this.last_req);
+    return JSON.parse(this.last_req.responseText);
+  }
+  
   // Convert a options object to an url query string.
   // ex: {key:'value',key2:'value2'} becomes '?key="value"&key2="value2"'
   function encodeOptions(options) {

Modified: incubator/couchdb/trunk/share/www/script/couch_tests.js
URL: http://svn.apache.org/viewvc/incubator/couchdb/trunk/share/www/script/couch_tests.js?rev=719160&r1=719159&r2=719160&view=diff
==============================================================================
--- incubator/couchdb/trunk/share/www/script/couch_tests.js [utf-8] (original)
+++ incubator/couchdb/trunk/share/www/script/couch_tests.js [utf-8] Wed Nov 19 20:42:43 2008
@@ -130,7 +130,7 @@
     T(db.open(existingDoc._id, {rev: existingDoc._rev}) != null);
     
     // make sure restart works
-    T(restartServer().ok);
+    restartServer();
   },
   all_docs: function(debug) {
     var db = new CouchDB("test_suite_db");
@@ -1845,6 +1845,7 @@
     for(var i in docs) {
         db.deleteDoc(docs[i]);
     }
+    db.setAdmins(["Foo bar"]);
     var deletesize = db.info().disk_size;
     T(deletesize > originalsize);
 
@@ -1859,6 +1860,7 @@
     T(xhr.getResponseHeader("Content-Type") == "text/plain")
     T(db.info().doc_count == 1);
     T(db.info().disk_size < deletesize);
+    
   },
   
   purge: function(debug) {
@@ -1963,7 +1965,7 @@
     
     // test that settings can be altered
     xhr = CouchDB.request("PUT", "/_config/test/foo",{
-      body : "bar"
+      body : JSON.stringify("bar")
     });
     T(xhr.status == 200);
     xhr = CouchDB.request("GET", "/_config/test");
@@ -1975,78 +1977,139 @@
     T(xhr.responseText == '"bar"');
   },
   
-  security : function(debug) {
+  security_validation : function(debug) {
+    // This tests couchdb's security and validation features. This does
+    // not test authentication, except to use test authentication code made
+    // specifically for this testing. It is a WWWW-Authenticate scheme named
+    // X-Couch-Test-Auth, and the user names and passwords are hard coded
+    // on the server-side.
+    // 
+    // We could have used Basic authentication, however the XMLHttpRequest
+    // implementation for Firefox and Safari, and probably other browsers are
+    // broken (Firefox always prompts the user on 401 failures, Safari gives
+    // odd security errors when using different name/passwords, perhaps due
+    // to cross site scripting prevention).  These problems essentially make Basic
+    // authentication testing in the browser impossible. But while hard to
+    // test automated in the browser, Basic auth may still useful for real
+    // world use where these bugs/behaviors don't matter.
+    //
+    // So for testing purposes we are using this custom X-Couch-Test-Auth.
+    // It's identical to Basic auth, except it doesn't even base64 encode
+    // the "username:password" string, it's sent completely plain text.
+    // Firefox and Safari both deal with this correctly (which is to say
+    // they correctly do nothing special).
+    
+    
     var db = new CouchDB("test_suite_db");
     db.deleteDb();
     db.createDb();
     if (debug) debugger;
-
-    var designDoc = {
-      _id:"_design/test",
-      language: "javascript",
-      validate_doc_update: "(" + (function (newDoc, oldDoc, userCtx) {
-        // docs should have an author field.
-        if (!newDoc.author) {
-          throw {error:"forbidden",
-                reason:"Documents must have an author field",
-                http_status:403};
-        }
-        
-        // Note, the next line could be:
-        //
-        // if (oldDoc && oldDoc.author != userCtx.name) {
-        //  
-        // when name is the result of basic authentication, and added:
-        // 
-        // headers:
-        //    {"WWW-Authenticate": "Basic realm=\"" + userCtx.db + "\""},
-        //
-        // to the thrown exception. But when trying to authenticate in this
-        // manner, most browsers have  weird behaviors that make testing it
-        // in the browser difficult. So instead we use special header values
-        // a proof of concept.
-        if (oldDoc && oldDoc.author != userCtx["X-Couch-Username"]) {
-            throw {error:"unauthorized",
-                reason:"You are not the author of this document. You jerk.",
-                headers:
-                  {"X-Couch-Foo": "bar"},
-                http_status:401};
-        }
-      }).toString() + ")"
-    }
-    
-    db.save(designDoc);
-    
-    var userDb = new CouchDB("test_suite_db", {"X-Couch-Username":"test user"});
     
-    try {
-      userDb.save({foo:1});
-      T(false && "Can't get here. Should have thrown an error");
-    } catch (e) {
-      T(e.error == "forbidden");
-      T(userDb.last_req.status == 403);
-    }
-    
-    userDb.save({_id:"testdoc", foo:1, author:"test user"});
+    run_on_modified_server(
+      [{section: "httpd",
+        key: "authentication_handler",
+        value: "{couch_httpd, special_test_authentication_handler}"},
+      {section:"httpd",
+        key: "WWW-Authenticate",
+        value:  "X-Couch-Test-Auth"}],
+        
+      function () {
     
-    var doc = userDb.open("testdoc");
-    doc.foo=2;
-    userDb.save(doc);
+        // try saving document usin the wrong credentials
+        var wrongPasswordDb = new CouchDB("test_suite_db",
+          {"WWW-Authenticate": "X-Couch-Test-Auth Damien Katz:foo"}
+        );
     
-    var user2Db = new CouchDB("test_suite_db", {"X-Couch-Username":"test user2"});
+        try {
+          wrongPasswordDb.save({foo:1,author:"Damien Katz"});
+          T(false && "Can't get here. Should have thrown an error 1");
+        } catch (e) {
+          T(e.error == "unauthorized");
+          T(wrongPasswordDb.last_req.status == 401);
+        }
+        
+        
+        // Create the design doc that will run custom validation code
+        var designDoc = {
+          _id:"_design/test",
+          language: "javascript",
+          validate_doc_update: "(" + (function (newDoc, oldDoc, userCtx) {
+            log("newDoc: " + newDoc.toSource());
+            if (oldDoc) {
+              log("oldDoc: " + oldDoc.toSource());
+            }
+            // docs should have an author field.
+            if (!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."};
+            }
+          }).toString() + ")"
+        }
+
+        // Save a document normally
+        var userDb = new CouchDB("test_suite_db",
+          {"WWW-Authenticate": "X-Couch-Test-Auth Damien Katz:pecan pie"}
+        );
+        
+        T(userDb.save({_id:"testdoc", foo:1, author:"Damien Katz"}).ok);
+        
+        // Attempt to save the design as a non-admin
+        try {
+          userDb.save(designDoc);
+          T(false && "Can't get here. Should have thrown an error on design doc");
+        } catch (e) {
+          T(e.error == "unauthorized");
+          T(userDb.last_req.status == 401);
+        }
+        
+        // add user as admin
+        db.setAdmins(["Damien Katz"]);
+        
+        T(userDb.save(designDoc).ok);
     
-    var doc = user2Db.open("testdoc");
-    doc.foo=3;
-    try {
-      user2Db.save(doc);
-      T(false && "Can't get here. Should have thrown an error 2");
-    } catch (e) {
-      T(e.error == "unauthorized");
-      T(user2Db.last_req.status == 401);
-      T(user2Db.last_req.getResponseHeader("X-Couch-Foo") == "bar");
-    }
+        // update the document
+        var doc = userDb.open("testdoc");
+        doc.foo=2;
+        T(userDb.save(doc).ok);
+        
+        // Save a document that's missing an author field.
+        try {
+          userDb.save({foo:1});
+          T(false && "Can't get here. Should have thrown an error 2");
+        } catch (e) {
+          T(e.error == "forbidden");
+          T(userDb.last_req.status == 403);
+        }
     
+        // Now attempt to update the document as a different user, Jan 
+        var user2Db = new CouchDB("test_suite_db",
+          {"WWW-Authenticate": "X-Couch-Test-Auth Jan Lehnardt:apple"}
+        );
     
+        var doc = user2Db.open("testdoc");
+        doc.foo=3;
+        try {
+          user2Db.save(doc);
+          T(false && "Can't get here. Should have thrown an error 3");
+        } catch (e) {
+          T(e.error == "unauthorized");
+          T(user2Db.last_req.status == 401);
+        }
+        
+        // Now have Damien change the author to Jan
+        doc = userDb.open("testdoc");
+        doc.author="Jan Lehnardt";
+        T(userDb.save(doc).ok);
+        
+        // Now update the document as Jan
+        doc = user2Db.open("testdoc");
+        doc.foo = 3;
+        T(user2Db.save(doc).ok);
+      });
   }
 };
 
@@ -2067,10 +2130,32 @@
   return docs;
 }
 
+function run_on_modified_server(settings, fun) {
+  try {
+    // set the settings
+    for(var i=0; i < settings.length; i++) {
+      var s = settings[i];
+      var xhr = CouchDB.request("PUT", "/_config/" + s.section + "/" + s.key, {
+        body: JSON.stringify(s.value),
+        headers: {"X-Couch-Persist": "false"}
+      });
+      CouchDB.maybeThrowError(xhr);
+      s.oldValue = xhr.responseText;
+    }
+    // run the thing
+    fun();
+  } finally {
+    // unset the settings
+    for(var j=0; j < i; j++) {
+      var s = settings[j];
+      CouchDB.request("PUT", "/_config/" + s.section + "/" + s.key, {
+        body: s.oldValue,
+        headers: {"X-Couch-Persist": "false"}
+      });
+    }
+  }
+}
+
 function restartServer() {
-  var reply = CouchDB.request("POST", "/_restart");
-  do {
-    var xhr = CouchDB.request("GET", "/");
-  } while(xhr.status != 200);
-  return JSON.parse(reply.responseText);
+  CouchDB.request("POST", "/_restart");
 }

Modified: incubator/couchdb/trunk/src/couchdb/couch_btree.erl
URL: http://svn.apache.org/viewvc/incubator/couchdb/trunk/src/couchdb/couch_btree.erl?rev=719160&r1=719159&r2=719160&view=diff
==============================================================================
--- incubator/couchdb/trunk/src/couchdb/couch_btree.erl (original)
+++ incubator/couchdb/trunk/src/couchdb/couch_btree.erl Wed Nov 19 20:42:43 2008
@@ -277,11 +277,11 @@
         {NodeType, NodeList} = get_node(Bt, Pointer)
     end,
     NodeTuple = list_to_tuple(NodeList),
+    
+    {ok, NewNodeList, QueryOutput2, Bt2} =
     case NodeType of
-    kp_node ->
-        {ok, NewNodeList, QueryOutput2, Bt2} = modify_kpnode(Bt, NodeTuple, 1, Actions, [], QueryOutput);
-    kv_node ->
-        {ok, NewNodeList, QueryOutput2, Bt2} = modify_kvnode(Bt, NodeTuple, 1, Actions, [], QueryOutput)
+    kp_node -> modify_kpnode(Bt, NodeTuple, 1, Actions, [], QueryOutput);
+    kv_node -> modify_kvnode(Bt, NodeTuple, 1, Actions, [], QueryOutput)
     end,
     case NewNodeList of
     [] ->  % no nodes remain

Modified: incubator/couchdb/trunk/src/couchdb/couch_config.erl
URL: http://svn.apache.org/viewvc/incubator/couchdb/trunk/src/couchdb/couch_config.erl?rev=719160&r1=719159&r2=719160&view=diff
==============================================================================
--- incubator/couchdb/trunk/src/couchdb/couch_config.erl (original)
+++ incubator/couchdb/trunk/src/couchdb/couch_config.erl Wed Nov 19 20:42:43 2008
@@ -20,12 +20,14 @@
 -include("couch_db.hrl").
 
 -behaviour(gen_server).
--export([start_link/1, init/1, handle_call/3, handle_cast/2, handle_info/2,terminate/2, code_change/3]).
--export([all/0, get/1, get/2, get/3, delete/2, set/3, register/1, register/2,load_ini_file/1]).
+-export([start_link/1, init/1, handle_call/3, handle_cast/2, handle_info/2,
+        terminate/2, code_change/3]).
+-export([all/0, get/1, get/2, get/3, delete/2, set/3, set/4, register/1,
+        register/2, load_ini_file/1]).
 
 -record(config,
     {notify_funs=[],
-    writeback_filename=""
+    write_filename=""
     }).
 
 %% Public API %%
@@ -36,29 +38,37 @@
     gen_server:start_link({local, ?MODULE}, ?MODULE, IniFiles, []).
 
 all() ->
-    gen_server:call(?MODULE, all).
+    lists:sort(ets:tab2list(?MODULE)).
+
+
 get(Section) when is_binary(Section) ->
-    gen_server:call(?MODULE, {fetch, binary_to_list(Section)});
+    ?MODULE:get(?b2l(Section));
 get(Section) ->
-    gen_server:call(?MODULE, {fetch, Section}).
-get(Section, Key) when is_binary(Section) and is_binary(Key) ->
-    get(binary_to_list(Section), binary_to_list(Key), undefined);
+    Matches = ets:match(?MODULE, {{Section, '$1'}, '$2'}),
+    [{Key, Value} || [Key, Value] <- Matches].
+
+
 get(Section, Key) ->
-    get(Section, Key, undefined).
+    ?MODULE:get(Section, Key, undefined).
+    
 get(Section, Key, Default) when is_binary(Section) and is_binary(Key) ->
-    gen_server:call(?MODULE, {fetch, binary_to_list(Section), binary_to_list(Key), Default});
+    ?MODULE:get(?b2l(Section), ?b2l(Key), Default);
 get(Section, Key, Default) ->
-    gen_server:call(?MODULE, {fetch, Section, Key, Default}).
+    case ets:lookup(?MODULE, {Section, Key}) of
+    [] -> Default;
+    [{_,Result}] -> Result
+    end.
 
-set(Section, Key, Value) when is_binary(Section) and is_binary(Key)  ->
-    gen_server:call(?MODULE, {set, [{{binary_to_list(Section), binary_to_list(Key)}, Value}]});
 set(Section, Key, Value) ->
-    gen_server:call(?MODULE, {set, [{{Section, Key}, Value}]}).
+    set(Section, Key, Value, true).
+
+set(Section, Key, Value, Persist) when is_binary(Section) and is_binary(Key)  ->
+    set(?b2l(Section), ?b2l(Key), Value, Persist);
+set(Section, Key, Value, Persist) ->
+    gen_server:call(?MODULE, {set, [{{Section, Key}, Value}], Persist}).
 
-delete(Section, Key) when is_binary(Section) and is_binary(Key) ->
-    gen_server:call(?MODULE, {delete, {binary_to_list(Section), binary_to_list(Key)}});
 delete(Section, Key) ->
-    gen_server:call(?MODULE, {delete, {Section, Key}}).
+    set(Section, Key, "").
 
 register(Fun) ->
     ?MODULE:register(Fun, self()).
@@ -73,42 +83,36 @@
 init(IniFiles) ->
     ets:new(?MODULE, [named_table, set, protected]),
     [ok = load_ini_file(IniFile) || IniFile <- IniFiles],
-    {ok, #config{writeback_filename=lists:last(IniFiles)}}.
+    {ok, #config{write_filename=lists:last(IniFiles)}}.
 
-handle_call(all, _From, Config) ->
-    {reply, lists:sort(ets:tab2list(?MODULE)), Config};
-
-handle_call({fetch, Section}, _From, Config) ->
-    Matches = ets:match(?MODULE, {{Section, '$1'}, '$2'}),
-    {reply, [{Key, Value} || [Key, Value] <- Matches], Config};
-
-handle_call({fetch, Section, Key, Default}, _From, Config) ->
-    Ret = case ets:lookup(?MODULE, {Section, Key}) of
-    [] -> Default;
-    [{_,Result}] -> Result
-    end,
-    {reply, Ret, Config};
-
-handle_call({set, KVs}, _From, Config) ->
-    [ok = insert_and_commit(Config, KV) || KV <- KVs],
-    {reply, ok, Config};
-
-handle_call({delete, Key}, _From, Config) ->
-    ets:delete(?MODULE, Key),
+handle_call({set, KVs, Persist}, _From, Config) ->
+    lists:map(
+        fun({{Section, Key}, Value}=KV) ->
+            true = ets:insert(?MODULE, KV),
+            if Persist ->
+                ok = couch_config_writer:save_to_file(KV,
+                        Config#config.write_filename);
+            true -> ok
+            end,
+            [catch F(Section, Key, Value)
+                    || {_Pid, F} <- Config#config.notify_funs]
+        end, KVs),
     {reply, ok, Config};
 
 handle_call({register, Fun, Pid}, _From, #config{notify_funs=PidFuns}=Config) ->
     erlang:monitor(process, Pid),
-    {reply, ok, Config#config{notify_funs=[{Pid, Fun}|PidFuns]}}.
-
-%% @spec insert_and_commit(Tab::etstable(), Config::any()) -> ok
-%% @doc Inserts a Key/Value pair into the ets table, writes it to the storage
-%%      ini file and calls all registered callback functions for Key.
-insert_and_commit(Config, KV) ->
-    true = ets:insert(?MODULE, KV),
-    % notify funs
-    [catch Fun(KV) || {_Pid, Fun} <- Config#config.notify_funs],
-    couch_config_writer:save_to_file(KV, Config#config.writeback_filename).
+    % convert 1 and 2 arity to 3 arity
+    Fun2 = 
+    if is_function(Fun, 1) ->
+        fun(Section, _Key, _Value) -> Fun(Section) end;
+    is_function(Fun, 2) ->
+        fun(Section, Key, _Value) -> Fun(Section, Key) end;
+    is_function(Fun, 3) ->
+        Fun
+    end,
+    {reply, ok, Config#config{notify_funs=[{Pid, Fun2}|PidFuns]}}.
+    
+    
 
 %% @spec load_ini_file(IniFile::filename()) -> ok
 %% @doc Parses an ini file and stores Key/Value Pairs into the ets table.
@@ -120,7 +124,7 @@
            IniBin0;
         {error, enoent} ->
            Msg = io_lib:format("Couldn't find server configuration file ~s.", [IniFilename]),
-           io:format("~s~n", [Msg]),
+           ?LOG_ERROR("~s~n", [Msg]),
            throw({startup_error, Msg})
     end,
 

Modified: incubator/couchdb/trunk/src/couchdb/couch_db.erl
URL: http://svn.apache.org/viewvc/incubator/couchdb/trunk/src/couchdb/couch_db.erl?rev=719160&r1=719159&r2=719160&view=diff
==============================================================================
--- incubator/couchdb/trunk/src/couchdb/couch_db.erl (original)
+++ incubator/couchdb/trunk/src/couchdb/couch_db.erl Wed Nov 19 20:42:43 2008
@@ -21,7 +21,7 @@
 -export([enum_docs/4,enum_docs/5,enum_docs_since/4,enum_docs_since/5]).
 -export([enum_docs_since_reduce_to_count/1,enum_docs_reduce_to_count/1]).
 -export([increment_update_seq/1,get_purge_seq/1,purge_docs/2,get_last_purged/1]).
--export([start_link/3,make_doc/2]).
+-export([start_link/3,make_doc/2,set_admins/2,get_admins/1]).
 -export([init/1,terminate/2,handle_call/3,handle_cast/2,code_change/3,handle_info/2]).
 
 
@@ -67,9 +67,9 @@
 close(#db{fd=Fd}) ->
     couch_file:drop_ref(Fd).
 
-open_ref_counted(MainPid, OpeningPid, UserCred) ->
+open_ref_counted(MainPid, OpeningPid, UserCtx) ->
     {ok, Db} = gen_server:call(MainPid, {open_ref_counted_instance, OpeningPid}),
-    {ok, Db#db{user_ctx=UserCred}}.
+    {ok, Db#db{user_ctx=UserCtx}}.
 
 num_refs(MainPid) ->
     gen_server:call(MainPid, num_refs).
@@ -172,6 +172,16 @@
         ],
     {ok, InfoList}.
 
+get_admins(#db{admins=Admins}) ->
+    Admins.
+
+set_admins(#db{update_pid=UpdatePid,user_ctx=Ctx}, 
+        Admins) when is_list(Admins) ->
+    case gen_server:call(UpdatePid, {set_admins, Admins, Ctx}, infinity) of
+    ok -> ok;
+    Error -> throw(Error)
+    end.
+
 name(#db{name=Name}) ->
     Name.
     
@@ -203,15 +213,28 @@
        group_alike_docs(Rest, [[Doc]|[Bucket|RestBuckets]])
     end.
 
+
+validate_doc_update(#db{user_ctx=UserCtx, admins=Admins},
+        #doc{id= <<"_design/",_/binary>>}=Doc, _GetDiskDocFun) ->
+    UserNames = [UserCtx#user_ctx.name | UserCtx#user_ctx.roles],
+    % if the user is a server admin or db admin, allow the save
+    case length(UserNames -- [<<"_admin">> | Admins]) == length(UserNames) of
+    true ->
+        % not an admin
+        throw({unauthorized, <<"You are not a server or database admin.">>});
+    false ->
+        Doc
+    end;
 validate_doc_update(#db{validate_doc_funs=[]}, Doc, _GetDiskDocFun) ->
     Doc;
-validate_doc_update(_Db, #doc{id= <<"_design/",_/binary>>}=Doc, _GetDiskDocFun) ->
-    Doc;
 validate_doc_update(_Db, #doc{id= <<"_local/",_/binary>>}=Doc, _GetDiskDocFun) ->
     Doc;
-validate_doc_update(#db{name=DbName,user_ctx={CtxProps}}=Db, Doc, GetDiskDocFun) ->
+validate_doc_update(#db{name=DbName,user_ctx=Ctx}=Db, Doc, GetDiskDocFun) ->
     DiskDoc = GetDiskDocFun(),
-    [case Fun(Doc, DiskDoc, {[{<<"db">>, DbName} | CtxProps]}) of
+    JsonCtx =  {[{<<"db">>, DbName},
+            {<<"name">>,Ctx#user_ctx.name},
+            {<<"roles">>,Ctx#user_ctx.roles}]},
+    [case Fun(Doc, DiskDoc, JsonCtx) of
         ok -> ok;
         Error -> throw(Error)
     end || Fun <- Db#db.validate_doc_funs],

Modified: incubator/couchdb/trunk/src/couchdb/couch_db.hrl
URL: http://svn.apache.org/viewvc/incubator/couchdb/trunk/src/couchdb/couch_db.hrl?rev=719160&r1=719159&r2=719160&view=diff
==============================================================================
--- incubator/couchdb/trunk/src/couchdb/couch_db.hrl (original)
+++ incubator/couchdb/trunk/src/couchdb/couch_db.hrl Wed Nov 19 20:42:43 2008
@@ -85,19 +85,35 @@
     meta = []
     }).
     
-    
 
 
+-record(user_ctx,
+    {name=nil,
+    roles=[]
+    }).
+
+% This should be updated anytime a header change happens that requires more
+% than filling in new defaults.
+%
+% As long the changes are limited to new header fields (with inline
+% defaults) added to the end of the file, then there is no need to increment
+% the disk revision number.
+%
+% if the disk revision is incremented, then new upgrade logic will need to be
+% added to couch_db_updater:init_db.
+
+-define(LATEST_DISK_VERSION, 0).
 
 -record(db_header,
-    {write_version = 0,
+    {disk_version = ?LATEST_DISK_VERSION,  
      update_seq = 0,
      summary_stream_state = nil,
      fulldocinfo_by_id_btree_state = nil,
      docinfo_by_seq_btree_state = nil,
      local_docs_btree_state = nil,
      purge_seq = 0,
-     purged_docs = nil
+     purged_docs = nil,
+     admins_ptr = nil 
     }).
 
 -record(db,
@@ -114,9 +130,11 @@
     name,
     filepath,
     validate_doc_funs=[],
-    user_ctx={[]}
+    admins=[],
+    admins_ptr=nil,
+    user_ctx=#user_ctx{}
     }).
-    
+
 
 -record(view_query_args, {
     start_key = nil,

Modified: incubator/couchdb/trunk/src/couchdb/couch_db_updater.erl
URL: http://svn.apache.org/viewvc/incubator/couchdb/trunk/src/couchdb/couch_db_updater.erl?rev=719160&r1=719159&r2=719160&view=diff
==============================================================================
--- incubator/couchdb/trunk/src/couchdb/couch_db_updater.erl (original)
+++ incubator/couchdb/trunk/src/couchdb/couch_db_updater.erl Wed Nov 19 20:42:43 2008
@@ -60,6 +60,19 @@
     couch_db_update_notifier:notify({updated, Db#db.name}),
     {reply, {ok, Db2#db.update_seq}, Db2};
 
+handle_call({set_admins, NewAdmins, #user_ctx{roles=Roles}}, _From, Db) ->
+    DbAdmins = [<<"_admin">> | Db#db.admins],
+    case length(DbAdmins -- Roles) == length(DbAdmins) of
+    true ->
+        {reply, {unauthorized, <<"You are not a db or server admin.">>}, Db};
+    false ->
+        {ok, Ptr} = couch_file:append_term(Db#db.fd, NewAdmins),
+        Db2 = commit_data(Db#db{admins=NewAdmins, admins_ptr=Ptr,
+                update_seq=Db#db.update_seq+1}),
+        ok = gen_server:call(Db2#db.main_pid, {db_updated, Db2}),
+        {reply, ok, Db2}
+    end;
+
 handle_call({purge_docs, _IdRevs}, _From,
         #db{compactor_pid=Pid}=Db) when Pid /= nil ->
     {reply, {error, purge_during_compaction}, Db};
@@ -128,7 +141,7 @@
     case Db#db.compactor_pid of
     nil ->
         ?LOG_INFO("Starting compaction for db \"~s\"", [Db#db.name]),
-        Pid = spawn_link(fun() -> start_copy_compact_int(Db) end),
+        Pid = spawn_link(fun() -> start_copy_compact(Db) end),
         Db2 = Db#db{compactor_pid=Pid},
         ok = gen_server:call(Db#db.main_pid, {db_updated, Db2}),
         {noreply, Db2};
@@ -166,7 +179,7 @@
         ?LOG_INFO("Compaction file still behind main file "
             "(update seq=~p. compact update seq=~p). Retrying.",
             [Db#db.update_seq, NewSeq]),
-        Pid = spawn_link(fun() -> start_copy_compact_int(Db) end),
+        Pid = spawn_link(fun() -> start_copy_compact(Db) end),
         Db2 = Db#db{compactor_pid=Pid},
         couch_file:close(NewFd),
         {noreply, Db2}
@@ -222,7 +235,19 @@
 btree_by_seq_reduce(rereduce, Reds) ->
     lists:sum(Reds).
 
-init_db(DbName, Filepath, Fd, Header) ->
+simple_upgrade_record(Old, New) when size(Old) == size(New)->
+    Old;
+simple_upgrade_record(Old, New) ->
+    NewValuesTail =
+        lists:sublist(tuple_to_list(New), size(Old) + 1, size(New)-size(Old)),
+    list_to_tuple(tuple_to_list(Old) ++ NewValuesTail).
+
+init_db(DbName, Filepath, Fd, Header0) ->
+    case element(2, Header0) of
+    ?LATEST_DISK_VERSION -> ok;
+    _ -> throw({database_disk_version_error, "Incorrect disk header version"})
+    end,
+    Header = simple_upgrade_record(Header0, #db_header{}),
     {ok, SummaryStream} = couch_stream:open(Header#db_header.summary_stream_state, Fd),
     ok = couch_stream:set_min_buffer(SummaryStream, 10000),
     Less =
@@ -242,7 +267,13 @@
             {join, fun(X,Y) -> btree_by_seq_join(X,Y) end},
             {reduce, fun(X,Y) -> btree_by_seq_reduce(X,Y) end}]),
     {ok, LocalDocsBtree} = couch_btree:open(Header#db_header.local_docs_btree_state, Fd),
-
+    case Header#db_header.admins_ptr of
+    nil ->
+        Admins = [],
+        AdminsPtr = nil;
+    AdminsPtr ->
+        {ok, Admins} = couch_file:pread_term(Fd, AdminsPtr)
+    end,
     #db{
         update_pid=self(),
         fd=Fd,
@@ -253,7 +284,9 @@
         local_docs_btree = LocalDocsBtree,
         update_seq = Header#db_header.update_seq,
         name = DbName,
-        filepath=Filepath}.
+        filepath = Filepath,
+        admins = Admins,
+        admins_ptr = AdminsPtr}.
 
 
 close_db(#db{fd=Fd,summary_stream=Ss}) ->
@@ -474,7 +507,8 @@
         summary_stream_state = couch_stream:get_state(Db#db.summary_stream),
         docinfo_by_seq_btree_state = couch_btree:get_state(Db#db.docinfo_by_seq_btree),
         fulldocinfo_by_id_btree_state = couch_btree:get_state(Db#db.fulldocinfo_by_id_btree),
-        local_docs_btree_state = couch_btree:get_state(Db#db.local_docs_btree)
+        local_docs_btree_state = couch_btree:get_state(Db#db.local_docs_btree),
+        admins_ptr = Db#db.admins_ptr
         },
     if Header == Header2 ->
         Db; % unchanged. nothing to do
@@ -532,7 +566,7 @@
 
 
           
-copy_compact_docs(Db, NewDb, Retry) ->
+copy_compact(Db, NewDb, Retry) ->
     EnumBySeqFun =
     fun(#doc_info{update_seq=Seq}=DocInfo, _Offset, {AccNewDb, AccUncopied}) ->
         case couch_util:should_flush() of
@@ -546,15 +580,19 @@
     {ok, {NewDb2, Uncopied}} =
         couch_btree:foldl(Db#db.docinfo_by_seq_btree, NewDb#db.update_seq + 1, EnumBySeqFun, {NewDb, []}),
 
-    case Uncopied of
-    [#doc_info{update_seq=LastSeq} | _] ->
-        commit_data( copy_docs(Db, NewDb2#db{update_seq=LastSeq},
-            lists:reverse(Uncopied), Retry));
-    [] ->
-        NewDb2
-    end.
+    NewDb3 = copy_docs(Db, NewDb2, lists:reverse(Uncopied), Retry),
+    
+    % copy misc header values
+    if NewDb3#db.admins /= Db#db.admins ->
+        {ok, Ptr} = couch_file:append_term(NewDb3#db.fd, Db#db.admins),
+        NewDb4 = NewDb3#db{admins=Db#db.admins, admins_ptr=Ptr};
+    true ->
+        NewDb4 = NewDb3
+    end,
+    
+    commit_data(NewDb4#db{update_seq=Db#db.update_seq}).
 
-start_copy_compact_int(#db{name=Name,filepath=Filepath}=Db) ->
+start_copy_compact(#db{name=Name,filepath=Filepath}=Db) ->
     CompactFile = Filepath ++ ".compact",
     ?LOG_DEBUG("Compaction process spawned for db \"~s\"", [Name]),
     case couch_file:open(CompactFile) of
@@ -568,7 +606,7 @@
         ok = couch_file:write_header(Fd, ?HEADER_SIG, Header=#db_header{})
     end,
     NewDb = init_db(Name, CompactFile, Fd, Header),
-    NewDb2 = copy_compact_docs(Db, NewDb, Retry),
+    NewDb2 = copy_compact(Db, NewDb, Retry),
     close_db(NewDb2),
     
     gen_server:cast(Db#db.update_pid, {compact_done, CompactFile}).

Modified: incubator/couchdb/trunk/src/couchdb/couch_httpd.erl
URL: http://svn.apache.org/viewvc/incubator/couchdb/trunk/src/couchdb/couch_httpd.erl?rev=719160&r1=719159&r2=719160&view=diff
==============================================================================
--- incubator/couchdb/trunk/src/couchdb/couch_httpd.erl (original)
+++ incubator/couchdb/trunk/src/couchdb/couch_httpd.erl Wed Nov 19 20:42:43 2008
@@ -13,17 +13,17 @@
 -module(couch_httpd).
 -include("couch_db.hrl").
 
--export([start_link/0, stop/0, handle_request/4]).
+-export([start_link/0, stop/0, handle_request/3]).
 
 -export([header_value/2,header_value/3,qs_value/2,qs_value/3,qs/1,path/1]).
--export([check_is_admin/1,unquote/1]).
+-export([verify_is_server_admin/1,unquote/1]).
 -export([parse_form/1,json_body/1,body/1,doc_etag/1]).
 -export([primary_header_value/2,partition/1,serve_file/3]).
 -export([start_chunked_response/3,send_chunk/2]).
 -export([start_json_response/2, start_json_response/3, end_json_response/1]).
 -export([send_response/4,send_method_not_allowed/2,send_error/4]).
 -export([send_json/2,send_json/3,send_json/4]).
--export([default_authenticate/1]).
+-export([default_authentication_handler/1,special_test_authentication_handler/1]).
 
 
 % Maximum size of document PUT request body (4GB)
@@ -37,24 +37,21 @@
 
     BindAddress = couch_config:get("httpd", "bind_address", any),
     Port = couch_config:get("httpd", "port", "5984"),
-    AuthenticationFun = make_arity_1_fun(
-            couch_config:get("httpd", "authentication", 
-            "{couch_httpd, default_authenticate}")),
-        
+    
     UrlHandlersList = lists:map(
         fun({UrlKey, SpecStr}) ->
-            {list_to_binary(UrlKey), make_arity_1_fun(SpecStr)}
+            {?l2b(UrlKey), make_arity_1_fun(SpecStr)}
         end, couch_config:get("httpd_global_handlers")),
         
     DbUrlHandlersList = lists:map(
         fun({UrlKey, SpecStr}) ->
-            {list_to_binary(UrlKey), make_arity_2_fun(SpecStr)}
+            {?l2b(UrlKey), make_arity_2_fun(SpecStr)}
         end, couch_config:get("httpd_db_handlers")),
     UrlHandlers = dict:from_list(UrlHandlersList),
     DbUrlHandlers = dict:from_list(DbUrlHandlersList),
     Loop = fun(Req)->
             apply(?MODULE, handle_request,
-                    [Req, UrlHandlers, DbUrlHandlers, AuthenticationFun])
+                    [Req, UrlHandlers, DbUrlHandlers])
         end,
 
     % and off we go
@@ -69,8 +66,6 @@
             ?MODULE:stop();
         ("httpd", "port") ->
             ?MODULE:stop();
-        ("httpd", "authentication") ->
-            ?MODULE:stop();
         ("httpd_global_handlers", _) ->
             ?MODULE:stop();
         ("httpd_db_handlers", _) ->
@@ -79,7 +74,8 @@
 
     {ok, Pid}.
 
-% SpecStr is a string like "{my_module, my_fun}"  or "{my_module, my_fun, foo}"
+% SpecStr is a string like "{my_module, my_fun}" 
+%  or "{my_module, my_fun, <<"my_arg">>}"
 make_arity_1_fun(SpecStr) ->
     case couch_util:parse_term(SpecStr) of
     {ok, {Mod, Fun, SpecArg}} ->
@@ -101,8 +97,9 @@
     mochiweb_http:stop(?MODULE).
     
 
-handle_request(MochiReq, UrlHandlers, DbUrlHandlers, AuthenticationFun) ->
-
+handle_request(MochiReq, UrlHandlers, DbUrlHandlers) ->
+    AuthenticationFun = make_arity_1_fun(
+            couch_config:get("httpd", "authentication_handler")),
     % for the path, use the raw path with the query string and fragment
     % removed, but URL quoting left intact
     RawUri = MochiReq:get(raw_path),
@@ -132,7 +129,7 @@
         
         % Non standard HTTP verbs aren't atoms (COPY, MOVE etc) so convert when
         % possible (if any module references the atom, then it's existing).
-        Meth -> try list_to_existing_atom(Meth) catch _ -> Meth end
+        Meth -> couch_util:to_existing_atom(Meth)
     end,
     HttpReq = #httpd{
         mochi_req = MochiReq,
@@ -143,14 +140,10 @@
         },
     DefaultFun = fun couch_httpd_db:handle_request/1,
     HandlerFun = couch_util:dict_find(HandlerKey, UrlHandlers, DefaultFun),
-    CouchHeaders = [{?l2b(K), ?l2b(V)}
-            || {"X-Couch-" ++ _= K,V}
-            <- mochiweb_headers:to_list(MochiReq:get(headers))],
     
     {ok, Resp} =
     try
-        {UserCtxProps} = AuthenticationFun(HttpReq),
-        HandlerFun(HttpReq#httpd{user_ctx={UserCtxProps ++ CouchHeaders}})
+        HandlerFun(HttpReq#httpd{user_ctx=AuthenticationFun(HttpReq)})
     catch
         Error ->
             send_error(HttpReq, Error)
@@ -165,15 +158,44 @@
     {ok, Resp}.
 
 
+special_test_authentication_handler(Req) ->
+    case header_value(Req, "WWW-Authenticate") of
+    "X-Couch-Test-Auth " ++ NamePass ->
+        % NamePass is a colon separated string: "joe schmoe:a password".
+        {ok, [Name, Pass]} = regexp:split(NamePass, ":"),
+        case {Name, Pass} of
+        {"Jan Lehnardt", "apple"} -> ok;
+        {"Christopher Lenz", "dog food"} -> ok;
+        {"Noah Slater", "biggiesmalls endian"} -> ok;
+        {"Chris Anderson", "mp3"} -> ok;
+        {"Damien Katz", "pecan pie"} -> ok;
+        {_, _} ->
+            throw({unauthorized, <<"Name or password is incorrect.">>})
+        end,
+        #user_ctx{name=?l2b(Name)};
+    _ ->
+        % No X-Couch-Test-Auth credentials sent, give admin access so the
+        % previous authentication can be restored after the test
+        #user_ctx{roles=[<<"_admin">>]}
+    end.
 
-default_authenticate(Req) ->
-    % by default, we just assume the users credentials for basic authentication
-    % are correct.
+default_authentication_handler(Req) ->
     case basic_username_pw(Req) of
-    {Username, _Pw} ->
-        {[{<<"name">>, ?l2b(Username)}]};
+    {User, Pass} ->
+        case couch_server:is_admin(User, Pass) of
+        true ->
+            #user_ctx{name=User, roles=[<<"_admin">>]};
+        false ->
+            throw({unauthorized, <<"Name or password is incorrect.">>})
+        end;
     nil ->
-        {[]}
+        case couch_server:has_admins() of
+        true ->
+            #user_ctx{};
+        false ->
+            % if no admins, then everyone is admin! Yay, admin party!
+            #user_ctx{roles=[<<"_admin">>]}
+        end
     end.
 
 
@@ -224,6 +246,14 @@
 doc_etag(#doc{revs=[DiskRev|_]}) ->
      "\"" ++ binary_to_list(DiskRev) ++ "\"".
 
+verify_is_server_admin(#httpd{user_ctx=#user_ctx{roles=Roles}}) ->
+    case lists:member(<<"_admin">>, Roles) of
+    true -> ok;
+    false -> throw({unauthorized, <<"You are not a server admin.">>})
+    end.
+
+
+
 basic_username_pw(Req) ->
     case header_value(Req, "Authorization") of
     "Basic " ++ Base64Value ->
@@ -239,27 +269,6 @@
         nil
     end.
 
-check_is_admin(Req) ->
-    IsNamedAdmin =
-    case basic_username_pw(Req) of
-    {User, Pass} ->
-        couch_server:is_admin(User, Pass);
-    nil ->
-        false
-    end,
-    
-    case IsNamedAdmin of
-    true ->
-        ok;
-    false ->
-        case couch_server:has_admins() of
-        true ->
-            throw(admin_authorization_error);
-        false ->
-            % if no admins, then everyone is admin! Yay, admin party!
-            ok
-        end
-    end.
 
 start_chunked_response(#httpd{mochi_req=MochiReq}, Code, Headers) ->
     {ok, MochiReq:respond({Code, Headers ++ server_header(), chunked})}.
@@ -315,11 +324,23 @@
     send_error(Req, 404, <<"not_found">>, Reason);
 send_error(Req, conflict) ->
     send_error(Req, 412, <<"conflict">>, <<"Document update conflict.">>);
-send_error(Req, admin_authorization_error) ->
-    send_json(Req, 401,
-        [{"WWW-Authenticate", "Basic realm=\"administrator\""}],
-        {[{<<"error">>,  <<"authorization">>},
-         {<<"reason">>, <<"Admin user name and password required">>}]});
+send_error(Req, {forbidden, Msg}) ->
+    send_json(Req, 403,
+        {[{<<"error">>,  <<"forbidden">>},
+         {<<"reason">>, Msg}]});
+send_error(Req, {unauthorized, Msg}) ->
+    case couch_config:get("httpd", "WWW-Authenticate", nil) of
+    nil ->
+        Headers = [];
+    Type ->
+        Headers = [{"WWW-Authenticate", Type}]
+    end,
+    send_json(Req, 401, Headers,
+        {[{<<"error">>,  <<"unauthorized">>},
+         {<<"reason">>, Msg}]});
+send_error(Req, {http_error, Code, Headers, Error, Reason}) ->
+    send_json(Req, Code, Headers,
+        {[{<<"error">>, Error}, {<<"reason">>, Reason}]});
 send_error(Req, {user_error, {Props}}) ->
     {Headers} = proplists:get_value(<<"headers">>, Props, {[]}),
     send_json(Req,
@@ -351,9 +372,9 @@
 send_error(Req, Code, Error, Msg) when not is_binary(Msg) ->
     send_error(Req, Code, Error, list_to_binary(io_lib:format("~p", [Msg])));
 send_error(Req, Code, Error, <<>>) ->
-    send_json(Req, Code, {[{error, Error}]});
+    send_json(Req, Code, {[{<<"error">>, Error}]});
 send_error(Req, Code, Error, Msg) ->
-    send_json(Req, Code, {[{error, Error}, {reason, Msg}]}).
+    send_json(Req, Code, {[{<<"error">>, Error}, {<<"reason">>, Msg}]}).
     
 
 

Modified: incubator/couchdb/trunk/src/couchdb/couch_httpd_db.erl
URL: http://svn.apache.org/viewvc/incubator/couchdb/trunk/src/couchdb/couch_httpd_db.erl?rev=719160&r1=719159&r2=719160&view=diff
==============================================================================
--- incubator/couchdb/trunk/src/couchdb/couch_httpd_db.erl (original)
+++ incubator/couchdb/trunk/src/couchdb/couch_httpd_db.erl Wed Nov 19 20:42:43 2008
@@ -42,7 +42,7 @@
     end.
 
 create_db_req(#httpd{user_ctx=UserCtx}=Req, DbName) ->
-    ok = couch_httpd:check_is_admin(Req),
+    ok = couch_httpd:verify_is_server_admin(Req),
     case couch_server:create(DbName, [{user_ctx, UserCtx}]) of
     {ok, Db} ->
         couch_db:close(Db),
@@ -52,7 +52,7 @@
     end.
 
 delete_db_req(#httpd{user_ctx=UserCtx}=Req, DbName) ->
-    ok = couch_httpd:check_is_admin(Req),
+    ok = couch_httpd:verify_is_server_admin(Req),
     case couch_server:delete(DbName, [{user_ctx, UserCtx}]) of
     ok ->
         send_json(Req, 200, {[{ok, true}]});
@@ -230,6 +230,18 @@
 db_req(#httpd{path_parts=[_,<<"_missing_revs">>]}=Req, _Db) ->
     send_method_not_allowed(Req, "POST");
 
+db_req(#httpd{method='PUT',path_parts=[_,<<"_admins">>]}=Req,
+        Db) ->
+    Admins = couch_httpd:json_body(Req),
+    ok = couch_db:set_admins(Db, Admins),
+    send_json(Req, {[{<<"ok">>, true}]});
+
+db_req(#httpd{method='GET',path_parts=[_,<<"_admins">>]}=Req, Db) ->
+    send_json(Req, couch_db:get_admins(Db));
+
+db_req(#httpd{path_parts=[_,<<"_admins">>]}=Req, _Db) ->
+    send_method_not_allowed(Req, "PUT,GET");
+
 db_req(#httpd{method='POST',path_parts=[DbName,<<"_design">>,Name|Rest]}=Req,
         Db) ->
     % Special case to enable using an unencoded in the URL of design docs, as

Modified: incubator/couchdb/trunk/src/couchdb/couch_httpd_misc_handlers.erl
URL: http://svn.apache.org/viewvc/incubator/couchdb/trunk/src/couchdb/couch_httpd_misc_handlers.erl?rev=719160&r1=719159&r2=719160&view=diff
==============================================================================
--- incubator/couchdb/trunk/src/couchdb/couch_httpd_misc_handlers.erl (original)
+++ incubator/couchdb/trunk/src/couchdb/couch_httpd_misc_handlers.erl Wed Nov 19 20:42:43 2008
@@ -31,7 +31,8 @@
 handle_welcome_req(#httpd{method='GET'}=Req, WelcomeMessage) ->
     send_json(Req, {[
         {couchdb, WelcomeMessage},
-        {version, list_to_binary(couch_server:get_version())}
+        {version, list_to_binary(couch_server:get_version())},
+        {start_time, list_to_binary(couch_server:get_start_time())}
     ]});
 handle_welcome_req(Req, _) ->
     send_method_not_allowed(Req, "GET,HEAD").
@@ -90,10 +91,9 @@
 
 
 handle_restart_req(#httpd{method='POST'}=Req) ->
-    ok = couch_httpd:check_is_admin(Req),
-    Response = send_json(Req, {[{ok, true}]}),
-    spawn(fun() -> couch_server:remote_restart() end),
-    Response;
+    ok = couch_httpd:verify_is_server_admin(Req),
+    couch_server_sup:restart_core_server(),
+    send_json(Req, 200, {[{ok, true}]});
 handle_restart_req(Req) ->
     send_method_not_allowed(Req, "POST").
 
@@ -114,7 +114,7 @@
 % GET /_config/
 % GET /_config
 handle_config_req(#httpd{method='GET', path_parts=[_]}=Req) ->
-    ok = couch_httpd:check_is_admin(Req),
+    ok = couch_httpd:verify_is_server_admin(Req),
     Grouped = lists:foldl(fun({{Section, Key}, Value}, Acc) ->
         case dict:is_key(Section, Acc) of
         true ->
@@ -129,22 +129,22 @@
     send_json(Req, 200, {KVs});
 % GET /_config/Section
 handle_config_req(#httpd{method='GET', path_parts=[_,Section]}=Req) ->
-    ok = couch_httpd:check_is_admin(Req),
+    ok = couch_httpd:verify_is_server_admin(Req),
     KVs = [{list_to_binary(Key), list_to_binary(Value)}
             || {Key, Value} <- couch_config:get(Section)],
     send_json(Req, 200, {KVs});
 % PUT /_config/Section/Key
 % "value"
 handle_config_req(#httpd{method='PUT', path_parts=[_, Section, Key]}=Req) ->
-    ok = couch_httpd:check_is_admin(Req),
-    Value = binary_to_list(couch_httpd:body(Req)),
-    ok = couch_config:set(Section, Key, Value),
-    send_json(Req, 200, {[
-        {ok, true}
-    ]});
+    ok = couch_httpd:verify_is_server_admin(Req),
+    Value = couch_httpd:json_body(Req),
+    Persist = couch_httpd:header_value(Req, "X-Couch-Persist") /= "false",
+    OldValue = couch_config:get(Section, Key, null),
+    ok = couch_config:set(Section, Key, ?b2l(Value), Persist),
+    send_json(Req, 200, list_to_binary(OldValue));
 % GET /_config/Section/Key
 handle_config_req(#httpd{method='GET', path_parts=[_, Section, Key]}=Req) ->
-    ok = couch_httpd:check_is_admin(Req),
+    ok = couch_httpd:verify_is_server_admin(Req),
     case couch_config:get(Section, Key, null) of
     null ->
         throw({not_found, unknown_config_value});
@@ -153,7 +153,7 @@
     end;
 % DELETE /_config/Section/Key
 handle_config_req(#httpd{method='DELETE',path_parts=[_,Section,Key]}=Req) ->
-    ok = couch_httpd:check_is_admin(Req),
+    ok = couch_httpd:verify_is_server_admin(Req),
     case couch_config:get(Section, Key, null) of
     null ->
         throw({not_found, unknown_config_value});

Modified: incubator/couchdb/trunk/src/couchdb/couch_query_servers.erl
URL: http://svn.apache.org/viewvc/incubator/couchdb/trunk/src/couchdb/couch_query_servers.erl?rev=719160&r1=719159&r2=719160&view=diff
==============================================================================
--- incubator/couchdb/trunk/src/couchdb/couch_query_servers.erl (original)
+++ incubator/couchdb/trunk/src/couchdb/couch_query_servers.erl Wed Nov 19 20:42:43 2008
@@ -69,9 +69,9 @@
     true = port_command(Port, Bin),
     case read_json(Port) of
     {[{<<"error">>, Id}, {<<"reason">>, Reason}]} ->
-        throw({list_to_atom(binary_to_list(Id)),Reason});
+        throw({Id,Reason});
     {[{<<"reason">>, Reason}, {<<"error">>, Id}]} ->
-        throw({list_to_atom(binary_to_list(Id)),Reason});
+        throw({Id,Reason});
     Result ->
         Result
     end.
@@ -181,15 +181,16 @@
     if DiskDoc == nil ->
         null;
     true -> 
-        couch_doc:to_json_obj(EditDoc, [revs])
+        couch_doc:to_json_obj(DiskDoc, [revs])
     end,
     try prompt(Port, 
             [<<"validate">>, FunSrc, JsonEditDoc, JsonDiskDoc, Ctx]) of
     1 ->
         ok;
-    {ErrorObject} ->
-        {user_error,
-           {ErrorObject}}
+    {[{<<"forbidden">>, Message}]} ->
+        throw({forbidden, Message});
+    {[{<<"unauthorized">>, Message}]} ->
+        throw({unauthorized, Message})
     after
         return_linked_port(Lang, Port)
     end.

Modified: incubator/couchdb/trunk/src/couchdb/couch_rep.erl
URL: http://svn.apache.org/viewvc/incubator/couchdb/trunk/src/couchdb/couch_rep.erl?rev=719160&r1=719159&r2=719160&view=diff
==============================================================================
--- incubator/couchdb/trunk/src/couchdb/couch_rep.erl (original)
+++ incubator/couchdb/trunk/src/couchdb/couch_rep.erl Wed Nov 19 20:42:43 2008
@@ -340,8 +340,8 @@
 open_doc(#http_db{uri=DbUrl, headers=Headers}, DocId, Options) ->
     [] = Options,
     case do_http_request(DbUrl ++ url_encode(DocId), get, Headers) of
-    {[{<<"error">>, ErrId}, {<<"reason">>, Reason}]} -> % binaries?
-        {list_to_atom(binary_to_list(ErrId)), Reason};
+    {[{<<"error">>, ErrId}, {<<"reason">>, Reason}]} ->
+        {couch_util:to_existing_atom(ErrId), Reason};
     Doc  ->
         {ok, couch_doc:from_json_obj(Doc)}
     end;

Modified: incubator/couchdb/trunk/src/couchdb/couch_server.erl
URL: http://svn.apache.org/viewvc/incubator/couchdb/trunk/src/couchdb/couch_server.erl?rev=719160&r1=719159&r2=719160&view=diff
==============================================================================
--- incubator/couchdb/trunk/src/couchdb/couch_server.erl (original)
+++ incubator/couchdb/trunk/src/couchdb/couch_server.erl Wed Nov 19 20:42:43 2008
@@ -18,7 +18,7 @@
 -export([open/2,create/2,delete/2,all_databases/0,get_version/0]).
 -export([init/1, handle_call/3,sup_start_link/0]).
 -export([handle_cast/2,code_change/3,handle_info/2,terminate/2]).
--export([dev_start/0,remote_restart/0,is_admin/2,has_admins/0]).
+-export([dev_start/0,is_admin/2,has_admins/0,get_start_time/0]).
 
 -include("couch_db.hrl").
 
@@ -26,7 +26,8 @@
     root_dir = [],
     dbname_regexp,
     max_dbs_open=100,
-    current_dbs_open=0
+    current_dbs_open=0,
+    start_time=""
     }).
 
 start() ->
@@ -62,6 +63,10 @@
         "0.0.0"
     end.
 
+get_start_time() ->
+    {ok, #server{start_time=Time}} = gen_server:call(couch_server, get_server),
+    Time.
+
 sup_start_link() ->
     gen_server:start_link({local, couch_server}, couch_server, [], []).
 
@@ -74,9 +79,6 @@
 delete(DbName, Options) ->
     gen_server:call(couch_server, {delete, DbName, Options}).
 
-remote_restart() ->
-    gen_server:call(couch_server, remote_restart).
-
 check_dbname(#server{dbname_regexp=RegExp}, DbName) ->
     case regexp:match(DbName, RegExp) of
     nomatch ->
@@ -137,13 +139,14 @@
     process_flag(trap_exit, true),
     {ok, #server{root_dir=RootDir,
                 dbname_regexp=RegExp,
-                max_dbs_open=MaxDbsOpen}}.
+                max_dbs_open=MaxDbsOpen,
+                start_time=httpd_util:rfc1123_date()}}.
 
 terminate(_Reason, _Server) ->
     ok.
 
 all_databases() ->
-    {ok, Root} = gen_server:call(couch_server, get_root),
+    {ok, #server{root_dir=Root}} = gen_server:call(couch_server, get_server),
     Filenames =
     filelib:fold_files(Root, "^[a-z0-9\\_\\$()\\+\\-]*[\\.]couch$", true,
         fun(Filename, AccIn) ->
@@ -196,10 +199,7 @@
     end.
 
 handle_call(get_server, _From, Server) ->
-    {reply, Server, Server};
-
-handle_call(get_root, _From, #server{root_dir=Root}=Server) ->
-    {reply, {ok, Root}, Server};
+    {reply, {ok, Server}, Server};
 handle_call({open, DbName, Options}, {FromPid,_}, Server) ->
     DbNameList = binary_to_list(DbName),
     UserCtx = proplists:get_value(user_ctx, Options, nil),
@@ -295,15 +295,7 @@
         end;
     Error ->
         {reply, Error, Server}
-    end;
-handle_call(remote_restart, _From, Server) ->
-    case couch_config:get("couchdb", "allow_remote_restart", "false") of
-    "true" ->
-        exit(couch_server_sup, restart);
-    _ ->
-        ok
-    end,
-    {reply, ok, Server}.
+    end.
 
 handle_cast(Msg, _Server) ->
     exit({unknown_cast_message, Msg}).

Modified: incubator/couchdb/trunk/src/couchdb/couch_server_sup.erl
URL: http://svn.apache.org/viewvc/incubator/couchdb/trunk/src/couchdb/couch_server_sup.erl?rev=719160&r1=719159&r2=719160&view=diff
==============================================================================
--- incubator/couchdb/trunk/src/couchdb/couch_server_sup.erl (original)
+++ incubator/couchdb/trunk/src/couchdb/couch_server_sup.erl Wed Nov 19 20:42:43 2008
@@ -14,7 +14,9 @@
 -behaviour(supervisor).
 
 
--export([start_link/1,stop/0,couch_config_start_link_wrapper/2,start_primary_services/0,start_secondary_services/0]).
+-export([start_link/1,stop/0, couch_config_start_link_wrapper/2,
+        start_primary_services/0,start_secondary_services/0,
+        restart_core_server/0]).
 
 -include("couch_db.hrl").
 
@@ -29,6 +31,10 @@
         {error, already_started}
     end.
 
+restart_core_server() ->
+    supervisor:terminate_child(couch_primary_services, couch_server),
+    supervisor:restart_child(couch_primary_services, couch_server).
+
 couch_config_start_link_wrapper(IniFiles, FirstConfigPid) ->
     case is_process_alive(FirstConfigPid) of
         true ->
@@ -119,7 +125,7 @@
     {ok, Pid}.
 
 start_primary_services() ->
-    supervisor:start_link(couch_server_sup,
+    supervisor:start_link({local, couch_primary_services}, couch_server_sup,
         {{one_for_one, 10, 3600}, 
             [{couch_log,
                 {couch_log, start_link, []},
@@ -156,7 +162,7 @@
         || {Name, SpecStr}
         <- couch_config:get("daemons"), SpecStr /= ""],
         
-    supervisor:start_link(couch_server_sup,
+    supervisor:start_link({local, couch_secondary_services}, couch_server_sup,
         {{one_for_one, 10, 3600}, DaemonChildSpecs}).
 
 stop() ->

Modified: incubator/couchdb/trunk/src/couchdb/couch_util.erl
URL: http://svn.apache.org/viewvc/incubator/couchdb/trunk/src/couchdb/couch_util.erl?rev=719160&r1=719159&r2=719160&view=diff
==============================================================================
--- incubator/couchdb/trunk/src/couchdb/couch_util.erl (original)
+++ incubator/couchdb/trunk/src/couchdb/couch_util.erl Wed Nov 19 20:42:43 2008
@@ -13,7 +13,7 @@
 -module(couch_util).
 
 -export([start_driver/1]).
--export([should_flush/0, should_flush/1]).
+-export([should_flush/0, should_flush/1, to_existing_atom/1]).
 -export([new_uuid/0, rand32/0, implode/2, collate/2, collate/3]).
 -export([abs_pathname/1,abs_pathname/2, trim/1, ascii_lower/1]).
 -export([encodeBase64/1, decodeBase64/1, to_hex/1,parse_term/1,dict_find/3]).
@@ -33,6 +33,16 @@
         exit(erl_ddll:format_error(Error))
     end.
 
+% works like list_to_existing_atom, except can be list or binary and it
+% gives you the original value instead of an error if no existing atom.
+to_existing_atom(V) when is_list(V)->
+    try list_to_existing_atom(V) catch _ -> V end;
+to_existing_atom(V) when is_binary(V)->
+    try list_to_existing_atom(?b2l(V)) catch _ -> V end;
+to_existing_atom(V) when is_atom(V)->
+    V.
+
+
 new_uuid() ->
     list_to_binary(to_hex(crypto:rand_bytes(16))).
     
@@ -168,11 +178,7 @@
     [Result] = erlang:port_control(drv_port(), Operation, Bin),
     % Result is 0 for lt, 1 for eq and 2 for gt. Subtract 1 to return the
     % expected typical -1, 0, 1
-    Result - 1;
-
-collate(A, B, _Options) ->
-    io:format("-----A,B:~p,~p~n", [A,B]),
-    throw({error, badtypes}).
+    Result - 1.
 
 should_flush() ->
     should_flush(?FLUSH_MAX_MEM).



Mime
View raw message