diff options
author | Damien F. Katz <damien@apache.org> | 2008-11-20 04:42:43 +0000 |
---|---|---|
committer | Damien F. Katz <damien@apache.org> | 2008-11-20 04:42:43 +0000 |
commit | 2c260766864a56e10aa45c3b1782f640b21a0bac (patch) | |
tree | ba41373450b909079755103172fb14a7ed7944c6 | |
parent | 8ec0f5d5407ccd9a7cee0fc579ad08d8f4be5bd7 (diff) |
Nearly completed security/validation work. Still needs replication testing.
git-svn-id: https://svn.apache.org/repos/asf/incubator/couchdb/trunk@719160 13f79535-47bb-0310-9956-ffa450edef68
-rw-r--r-- | etc/couchdb/default.ini.tpl.in | 1 | ||||
-rw-r--r-- | share/www/script/couch.js | 50 | ||||
-rw-r--r-- | share/www/script/couch_tests.js | 223 | ||||
-rw-r--r-- | src/couchdb/couch_btree.erl | 8 | ||||
-rw-r--r-- | src/couchdb/couch_config.erl | 104 | ||||
-rw-r--r-- | src/couchdb/couch_db.erl | 37 | ||||
-rw-r--r-- | src/couchdb/couch_db.hrl | 28 | ||||
-rw-r--r-- | src/couchdb/couch_db_updater.erl | 70 | ||||
-rw-r--r-- | src/couchdb/couch_httpd.erl | 131 | ||||
-rw-r--r-- | src/couchdb/couch_httpd_db.erl | 16 | ||||
-rw-r--r-- | src/couchdb/couch_httpd_misc_handlers.erl | 30 | ||||
-rw-r--r-- | src/couchdb/couch_query_servers.erl | 13 | ||||
-rw-r--r-- | src/couchdb/couch_rep.erl | 4 | ||||
-rw-r--r-- | src/couchdb/couch_server.erl | 32 | ||||
-rw-r--r-- | src/couchdb/couch_server_sup.erl | 12 | ||||
-rw-r--r-- | src/couchdb/couch_util.erl | 18 |
16 files changed, 499 insertions, 278 deletions
diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/default.ini.tpl.in index 4b44c0fc..6725aa6c 100644 --- a/etc/couchdb/default.ini.tpl.in +++ b/etc/couchdb/default.ini.tpl.in @@ -11,6 +11,7 @@ view_timeout = 5000 ; 5 seconds [httpd] port = 5984 bind_address = 127.0.0.1 +authentication_handler = {couch_httpd, default_authentication_handler} [log] file = %localstatelogdir%/couch.log diff --git a/share/www/script/couch.js b/share/www/script/couch.js index 934dacf8..d872c1f8 100644 --- a/share/www/script/couch.js +++ b/share/www/script/couch.js @@ -21,7 +21,7 @@ function CouchDB(name, httpHeaders) { // 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 @@ function CouchDB(name, httpHeaders) { // 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 @@ function CouchDB(name, httpHeaders) { 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 @@ function CouchDB(name, httpHeaders) { // 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 @@ function CouchDB(name, httpHeaders) { // 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 @@ function CouchDB(name, httpHeaders) { // 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 @@ function CouchDB(name, httpHeaders) { 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 @@ function CouchDB(name, httpHeaders) { 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 @@ function CouchDB(name, httpHeaders) { 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 @@ function CouchDB(name, httpHeaders) { // 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 @@ function CouchDB(name, httpHeaders) { 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 @@ function CouchDB(name, httpHeaders) { } 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) { diff --git a/share/www/script/couch_tests.js b/share/www/script/couch_tests.js index 722d3113..36cf2c25 100644 --- a/share/www/script/couch_tests.js +++ b/share/www/script/couch_tests.js @@ -130,7 +130,7 @@ var tests = { 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 @@ var tests = { 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 @@ var tests = { 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 @@ var tests = { // 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 @@ var tests = { 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 @@ function makeDocs(start, end, templateDoc) { 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"); } diff --git a/src/couchdb/couch_btree.erl b/src/couchdb/couch_btree.erl index 29e82911..14bc4a1f 100644 --- a/src/couchdb/couch_btree.erl +++ b/src/couchdb/couch_btree.erl @@ -277,11 +277,11 @@ modify_node(Bt, RootPointerInfo, Actions, QueryOutput) -> {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 diff --git a/src/couchdb/couch_config.erl b/src/couchdb/couch_config.erl index f3b4f328..d989b31d 100644 --- a/src/couchdb/couch_config.erl +++ b/src/couchdb/couch_config.erl @@ -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 @@ start_link(IniFiles) -> 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 @@ register(Fun, Pid) -> init(IniFiles) -> ets:new(?MODULE, [named_table, set, protected]), [ok = load_ini_file(IniFile) || IniFile <- IniFiles], - {ok, #config{writeback_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), + {ok, #config{write_filename=lists:last(IniFiles)}}. + +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 @@ load_ini_file(IniFile) -> 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, diff --git a/src/couchdb/couch_db.erl b/src/couchdb/couch_db.erl index 6441e2e1..7de5b8db 100644 --- a/src/couchdb/couch_db.erl +++ b/src/couchdb/couch_db.erl @@ -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 @@ open(DbName, Options) -> 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 @@ get_db_info(Db) -> ], {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([Doc|Rest], [Bucket|RestBuckets]) -> 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], diff --git a/src/couchdb/couch_db.hrl b/src/couchdb/couch_db.hrl index 6769c840..7bf3cb9d 100644 --- a/src/couchdb/couch_db.hrl +++ b/src/couchdb/couch_db.hrl @@ -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, diff --git a/src/couchdb/couch_db_updater.erl b/src/couchdb/couch_db_updater.erl index fecc5a14..67a6f624 100644 --- a/src/couchdb/couch_db_updater.erl +++ b/src/couchdb/couch_db_updater.erl @@ -60,6 +60,19 @@ handle_call(increment_update_seq, _From, Db) -> 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 @@ handle_cast(start_compact, Db) -> 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 @@ handle_cast({compact_done, CompactFilepath}, #db{filepath=Filepath}=Db) -> ?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(reduce, DocInfos) -> 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 @@ init_db(DbName, Filepath, Fd, Header) -> {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 @@ init_db(DbName, Filepath, Fd, Header) -> 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 @@ commit_data(#db{fd=Fd, header=Header} = Db) -> 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_docs(#db{fd=SrcFd}=Db, #db{fd=DestFd,summary_stream=DestStream}=NewDb, Info -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 @@ copy_compact_docs(Db, NewDb, Retry) -> {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 @@ start_copy_compact_int(#db{name=Name,filepath=Filepath}=Db) -> 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}). diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl index 6c8873dd..e1c99651 100644 --- a/src/couchdb/couch_httpd.erl +++ b/src/couchdb/couch_httpd.erl @@ -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 @@ start_link() -> 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 @@ start_link() -> ?MODULE:stop(); ("httpd", "port") -> ?MODULE:stop(); - ("httpd", "authentication") -> - ?MODULE:stop(); ("httpd_global_handlers", _) -> ?MODULE:stop(); ("httpd_db_handlers", _) -> @@ -79,7 +74,8 @@ start_link() -> {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 @@ stop() -> 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 @@ handle_request(MochiReq, UrlHandlers, DbUrlHandlers, AuthenticationFun) -> % 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 @@ handle_request(MochiReq, UrlHandlers, DbUrlHandlers, AuthenticationFun) -> }, 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 @@ handle_request(MochiReq, UrlHandlers, DbUrlHandlers, AuthenticationFun) -> {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 @@ json_body(#httpd{mochi_req=MochiReq}) -> 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 @@ basic_username_pw(Req) -> 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, {not_found, Reason}) -> 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(Error) -> 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}]}). diff --git a/src/couchdb/couch_httpd_db.erl b/src/couchdb/couch_httpd_db.erl index a239ceb5..0bf9d62e 100644 --- a/src/couchdb/couch_httpd_db.erl +++ b/src/couchdb/couch_httpd_db.erl @@ -42,7 +42,7 @@ handle_request(#httpd{path_parts=[DbName|RestParts],method=Method, 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 @@ create_db_req(#httpd{user_ctx=UserCtx}=Req, DbName) -> 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{method='POST',path_parts=[_,<<"_missing_revs">>]}=Req, Db) -> 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 diff --git a/src/couchdb/couch_httpd_misc_handlers.erl b/src/couchdb/couch_httpd_misc_handlers.erl index b62a4b85..766263ee 100644 --- a/src/couchdb/couch_httpd_misc_handlers.erl +++ b/src/couchdb/couch_httpd_misc_handlers.erl @@ -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_replicate_req(Req) -> 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 @@ handle_uuids_req(Req) -> % 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 @@ handle_config_req(#httpd{method='GET', path_parts=[_]}=Req) -> 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 @@ handle_config_req(#httpd{method='GET', path_parts=[_, Section, Key]}=Req) -> 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}); diff --git a/src/couchdb/couch_query_servers.erl b/src/couchdb/couch_query_servers.erl index 141c9406..8465a632 100644 --- a/src/couchdb/couch_query_servers.erl +++ b/src/couchdb/couch_query_servers.erl @@ -69,9 +69,9 @@ prompt(Port, Json) -> 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 @@ validate_doc_update(Lang, FunSrc, EditDoc, DiskDoc, Ctx) -> 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. diff --git a/src/couchdb/couch_rep.erl b/src/couchdb/couch_rep.erl index eda62c86..08d4df8f 100644 --- a/src/couchdb/couch_rep.erl +++ b/src/couchdb/couch_rep.erl @@ -340,8 +340,8 @@ update_docs(Db, Docs, Options, NewEdits) -> 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; diff --git a/src/couchdb/couch_server.erl b/src/couchdb/couch_server.erl index a81ca5e1..34aa16b7 100644 --- a/src/couchdb/couch_server.erl +++ b/src/couchdb/couch_server.erl @@ -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 @@ get_version() -> "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 @@ create(DbName, Options) -> 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 @@ init([]) -> 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 @@ try_close_lru(StartTime) -> 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 @@ handle_call({delete, DbName, Options}, _From, Server) -> 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}). diff --git a/src/couchdb/couch_server_sup.erl b/src/couchdb/couch_server_sup.erl index b75747d7..9c74a1a3 100644 --- a/src/couchdb/couch_server_sup.erl +++ b/src/couchdb/couch_server_sup.erl @@ -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 @@ start_link(IniFiles) -> {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 @@ start_server(IniFiles) -> {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 @@ start_secondary_services() -> || {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() -> diff --git a/src/couchdb/couch_util.erl b/src/couchdb/couch_util.erl index e6d6226b..d0f0c0b6 100644 --- a/src/couchdb/couch_util.erl +++ b/src/couchdb/couch_util.erl @@ -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 @@ start_driver(LibDir) -> 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 @@ collate(A, B, Options) when is_binary(A), is_binary(B) -> [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). |