From 2c260766864a56e10aa45c3b1782f640b21a0bac Mon Sep 17 00:00:00 2001 From: "Damien F. Katz" Date: Thu, 20 Nov 2008 04:42:43 +0000 Subject: 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 --- share/www/script/couch.js | 50 +++++---- share/www/script/couch_tests.js | 223 +++++++++++++++++++++++++++------------- 2 files changed, 186 insertions(+), 87 deletions(-) (limited to 'share') 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"); } -- cgit v1.2.3