summaryrefslogtreecommitdiff
path: root/share
diff options
context:
space:
mode:
authorDamien F. Katz <damien@apache.org>2008-11-20 04:42:43 +0000
committerDamien F. Katz <damien@apache.org>2008-11-20 04:42:43 +0000
commit2c260766864a56e10aa45c3b1782f640b21a0bac (patch)
treeba41373450b909079755103172fb14a7ed7944c6 /share
parent8ec0f5d5407ccd9a7cee0fc579ad08d8f4be5bd7 (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
Diffstat (limited to 'share')
-rw-r--r--share/www/script/couch.js50
-rw-r--r--share/www/script/couch_tests.js223
2 files changed, 186 insertions, 87 deletions
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");
}