| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| couchTests.elixir = true; |
| couchTests.security_validation = function(debug) { |
| |
| var db_name = get_random_db_name(); |
| var db = new CouchDB(db_name, {"X-Couch-Full-Commit":"false"}); |
| db.createDb(); |
| var authDb_name = get_random_db_name() + "_authdb"; |
| var authDb = new CouchDB(authDb_name, {"X-Couch-Full-Commit":"false"}); |
| authDb.createDb(); |
| var adminDbA, adminDbB; // used later |
| if (debug) debugger; |
| |
| run_on_modified_server( |
| [{section: "httpd", |
| key: "authentication_handlers", |
| value: "{couch_httpd_auth, cookie_authentication_handler}, {couch_httpd_auth, default_authentication_handler}"}, |
| {section: "couch_httpd_auth", |
| key: "authentication_db", value: authDb_name}, |
| {section: "chttpd_auth", |
| key: "authentication_db", value: authDb_name}], |
| |
| function () { |
| // the special case handler does not exist (any longer) in clusters, so we have |
| // to replicate the behavior using a "normal" DB even though tests might no more |
| // run universally (why the "X-Couch-Test-Auth" header was introduced). |
| // btw: this needs to be INSIDE configured server to propagate correctly ;-) |
| // At least they'd run in the build, though |
| T(authDb.save(CouchDB.prepareUserDoc({name: "tom"}, "cat")).ok); // Basic dG9tOmNhdA== |
| T(authDb.save(CouchDB.prepareUserDoc({name: "jerry"}, "mouse")).ok); // Basic amVycnk6bW91c2U= |
| T(authDb.save(CouchDB.prepareUserDoc({name: "spike"}, "dog")).ok); // Basic c3Bpa2U6ZG9n |
| authDb.ensureFullCommit(); |
| |
| // try saving document using the wrong credentials |
| var wrongPasswordDb = new CouchDB(db_name + "", |
| {"Authorization": "Basic c3Bpa2U6Y2F0"} // spike:cat - which is wrong |
| ); |
| |
| 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); |
| } |
| |
| // test force basic login |
| var resp = wrongPasswordDb.request("GET", "/_session?basic=true"); |
| var err = JSON.parse(resp.responseText); |
| T(err.error == "unauthorized"); |
| T(resp.status == 401); |
| |
| // Create the design doc that will run custom validation code |
| var designDoc = { |
| _id:"_design/test", |
| language: "javascript", |
| validate_doc_update: stringFun(function (newDoc, oldDoc, userCtx, secObj) { |
| if (secObj.admin_override) { |
| if (userCtx.roles.indexOf('_admin') != -1) { |
| // user is admin, they can do anything |
| return true; |
| } |
| } |
| // docs should have an author field. |
| if (!newDoc._deleted && !newDoc.author) { |
| throw {forbidden: |
| "Documents must have an author field"}; |
| } |
| if (oldDoc && oldDoc.author != userCtx.name) { |
| throw {unauthorized: |
| "You are '" + userCtx.name + "', not the author '" + oldDoc.author + "' of this document. You jerk."}; |
| } |
| }) |
| } |
| |
| // Save a document normally |
| var userDb = new CouchDB("" + db_name + "", |
| {"Authorization": "Basic amVycnk6bW91c2U="} // jerry |
| ); |
| // test session |
| TEquals("jerry", JSON.parse(userDb.request("GET", "/_session").responseText).userCtx.name); |
| |
| T(userDb.save({_id:"testdoc", foo:1, author:"jerry"}).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) { |
| // cluster changes from 401 unauthorized to 403 forbidden |
| TEquals("forbidden", e.error); |
| TEquals(403, userDb.last_req.status); |
| } |
| |
| // set user as the admin |
| T(db.setSecObj({ |
| admins : {names : ["jerry"]} |
| }).ok); |
| |
| // TODO: when _security is correctly honored (COUCHDB-2990), switch back |
| //T(userDb.save(designDoc).ok); |
| T(db.save(designDoc).ok); |
| |
| var user2Db = new CouchDB("" + db_name + "", |
| {"Authorization": "Basic dG9tOmNhdA=="} // tom |
| ); |
| // Attempt to save the design as a non-admin (in replication scenario) |
| designDoc.foo = "bar"; |
| designDoc._rev = "2-642e20f96624a0aae6025b4dba0c6fb2"; |
| try { |
| user2Db.save(designDoc, {new_edits : false}); |
| T(false && "Can't get here. Should have thrown an error on design doc"); |
| } catch (e) { |
| // cluster changes from 401 unauthorized to 403 forbidden |
| TEquals("forbidden", e.error); |
| TEquals(403, userDb.last_req.status); |
| } |
| |
| // test the _session API |
| var resp = userDb.request("GET", "/_session"); |
| var user = JSON.parse(resp.responseText).userCtx; |
| T(user.name == "jerry"); |
| // test that the roles are listed properly |
| TEquals(user.roles, []); |
| |
| |
| // 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 (before and after compaction) |
| for (var i=0; i<2; i++) { |
| 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); |
| } |
| // compact. - no more available on clusters (but: test is still valid w/out compaction) |
| /*T(db.compact().ok); |
| T(db.last_req.status == 202); |
| // compaction isn't instantaneous, loop until done |
| while (db.info().compact_running) {};*/ |
| } |
| |
| // Now attempt to update the document as a different user, Jan |
| 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 jerry change the author to tom |
| doc = userDb.open("testdoc"); |
| doc.author="tom"; |
| T(userDb.save(doc).ok); |
| |
| // Now update the document as tom |
| doc = user2Db.open("testdoc"); |
| doc.foo = 3; |
| T(user2Db.save(doc).ok); |
| |
| // jerry can't delete it |
| try { |
| userDb.deleteDoc(doc); |
| T(false && "Can't get here. Should have thrown an error 4"); |
| } catch (e) { |
| T(e.error == "unauthorized"); |
| T(userDb.last_req.status == 401); |
| } |
| |
| // admin must save with author field unless admin override |
| var resp = db.request("GET", "/_session"); |
| var user = JSON.parse(resp.responseText).userCtx; |
| T(user.name == null); |
| // test that we are admin |
| TEquals(user.roles, ["_admin"]); |
| |
| // can't save the doc even though we are admin |
| var doc = db.open("testdoc"); |
| doc.foo=3; |
| try { |
| db.save(doc); |
| T(false && "Can't get here. Should have thrown an error 3"); |
| } catch (e) { |
| T(e.error == "unauthorized"); |
| T(db.last_req.status == 401); |
| } |
| |
| // now turn on admin override |
| T(db.setDbProperty("_security", {admin_override : true}).ok); |
| // TODO: re-include after COUCHDB-2990 |
| //T(db.save(doc).ok); |
| |
| // try to do something lame |
| try { |
| db.setDbProperty("_security", ["foo"]); |
| T(false && "can't do this"); |
| } catch(e) {} |
| |
| // go back to normal |
| T(db.setDbProperty("_security", {admin_override : false}).ok); |
| |
| // Now delete document |
| T(user2Db.deleteDoc(doc).ok); |
| |
| // now test bulk docs |
| var docs = [{_id:"bahbah",author:"jerry",foo:"bar"},{_id:"fahfah",foo:"baz"}]; |
| |
| // Create the docs |
| var results = db.bulkSave(docs); |
| |
| T(results[0].rev) |
| T(results[0].error == undefined) |
| T(results[1].rev === undefined) |
| T(results[1].error == "forbidden") |
| |
| T(db.open("bahbah")); |
| T(db.open("fahfah") == null); |
| |
| |
| // now all or nothing with a failure - no more available on cluster |
| /* var docs = [{_id:"booboo",author:"Damien Katz",foo:"bar"},{_id:"foofoo",foo:"baz"}]; |
| |
| // Create the docs |
| var results = db.bulkSave(docs, {all_or_nothing:true}); |
| |
| T(results.errors.length == 1); |
| T(results.errors[0].error == "forbidden"); |
| T(db.open("booboo") == null); |
| T(db.open("foofoo") == null); |
| */ |
| |
| // Now test replication |
| var AuthHeaders = {"Authorization": "Basic c3Bpa2U6ZG9n"}; // spike |
| adminDbA = new CouchDB("" + db_name + "_a", {"X-Couch-Full-Commit":"false"}); |
| adminDbB = new CouchDB("" + db_name + "_b", {"X-Couch-Full-Commit":"false"}); |
| var dbA = new CouchDB("" + db_name + "_a", AuthHeaders); |
| var dbB = new CouchDB("" + db_name + "_b", AuthHeaders); |
| // looping does not really add value as the scenario is the same anyway (there's nothing 2 be gained from it) |
| var A = CouchDB.protocol + CouchDB.host + "/" + db_name + "_a"; |
| var B = CouchDB.protocol + CouchDB.host + "/" + db_name + "_b"; |
| |
| // (the databases never exist b4 - and we made sure they're deleted below) |
| //adminDbA.deleteDb(); |
| adminDbA.createDb(); |
| //adminDbB.deleteDb(); |
| adminDbB.createDb(); |
| |
| // save and replicate a documents that will and will not pass our design |
| // doc validation function. |
| T(dbA.save({_id:"foo1",value:"a",author:"tom"}).ok); |
| T(dbA.save({_id:"foo2",value:"a",author:"spike"}).ok); |
| T(dbA.save({_id:"bad1",value:"a"}).ok); |
| |
| T(CouchDB.replicate(A, B, {headers:AuthHeaders}).ok); |
| T(CouchDB.replicate(B, A, {headers:AuthHeaders}).ok); |
| |
| T(dbA.open("foo1")); |
| T(dbB.open("foo1")); |
| T(dbA.open("foo2")); |
| T(dbB.open("foo2")); |
| |
| // save the design doc to dbA |
| delete designDoc._rev; // clear rev from previous saves |
| T(adminDbA.save(designDoc).ok); |
| |
| // no affect on already saved docs |
| T(dbA.open("bad1")); |
| |
| // Update some docs on dbB. Since the design hasn't replicated, anything |
| // is allowed. |
| |
| // this edit will fail validation on replication to dbA (no author) |
| T(dbB.save({_id:"bad2",value:"a"}).ok); |
| |
| // this edit will fail security on replication to dbA (wrong author |
| // replicating the change) |
| var foo1 = dbB.open("foo1"); |
| foo1.value = "b"; |
| T(dbB.save(foo1).ok); |
| |
| // this is a legal edit |
| var foo2 = dbB.open("foo2"); |
| foo2.value = "b"; |
| T(dbB.save(foo2).ok); |
| |
| var results = CouchDB.replicate({"url": B, "headers": AuthHeaders}, {"url": A, "headers": AuthHeaders}, {headers:AuthHeaders}); |
| T(results.ok); |
| TEquals(1, results.history[0].docs_written); |
| TEquals(2, results.history[0].doc_write_failures); |
| |
| // bad2 should not be on dbA |
| T(dbA.open("bad2") == null); |
| |
| // The edit to foo1 should not have replicated. |
| T(dbA.open("foo1").value == "a"); |
| |
| // The edit to foo2 should have replicated. |
| T(dbA.open("foo2").value == "b"); |
| }); |
| |
| // cleanup |
| db.deleteDb(); |
| if(adminDbA){ |
| adminDbA.deleteDb(); |
| } |
| if(adminDbB){ |
| adminDbB.deleteDb(); |
| } |
| authDb.deleteDb(); |
| // don't have to clean the backend authDb since this test only calls |
| // couch_auth_cache:get_admin/1 which doesn't auto-create the users db |
| }; |