// 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.replicator_db_security = function(debug) {

  var reset_dbs = function(dbs) {
    dbs.forEach(function(db) {
      db.deleteDb();
      try { db.createDb() } catch (e) {};
    });
  };

  var dbs = ["couch_test_rep_db", "couch_test_users_db",
    "test_suite_db_a", "test_suite_db_b", "test_suite_db_c"]
    .map(function(db_name) {
      return new CouchDB(db_name, {"X-Couch-Full-Commit":"false"});
    });

  var repDb = dbs[0];
  var usersDb = dbs[1];
  var dbA = dbs[2];
  var dbB = dbs[3];
  var dbC = dbs[4];

  if (debug) debugger;

  var loginUser = function(username) {
    var pws = {
      jan: "apple",
      jchris: "mp3",
      fdmanana: "foobar",
      benoitc: "test"
    };
    T(CouchDB.login(username, pws[username]).ok);
  };

  var repChanges = function(username) {
    var pws = {
      jan: "apple",
      jchris: "mp3",
      fdmanana: "foobar",
      benoitc: "test"
    };
    T(CouchDB.login(username, pws[username]).ok);
    var changes = CouchDB.request(
      "GET",
       "/" + repDb.name + "/_changes?include_docs=true" +
         "&anti-cache=" + String(Math.round(Math.random() * 100000)));
    return changes = JSON.parse(changes.responseText);
  };

  var save_as = function(db, doc, username)
  {
    loginUser(username);
    try {
      return db.save(doc);
    } catch (ex) {
      return ex;
    } finally {
      CouchDB.logout();
    }
  };

  var open_as = function(db, docId, username) {
    loginUser(username);
    try {
      return db.open(docId);
    } finally {
      CouchDB.logout();
    }
  };

  // from test replicator_db.js
  function waitForDocPos(db, docId, pos) {
    var doc, curPos, t0, t1,
        maxWait = 3000;

    doc = db.open(docId);
    curPos = Number(doc._rev.split("-", 1));
    t0 = t1 = new Date();

    while ((curPos < pos) && ((t1 - t0) <= maxWait)) {
       doc = db.open(docId);
       curPos = Number(doc._rev.split("-", 1));
       t1 = new Date();
    }

    return doc;
  }

  var testFun = function()
  {
    reset_dbs(dbs);

    // _replicator db
    // in admin party mode, anonymous should be able to create a replication
    var repDoc = {
      _id: "null-owner-rep",
      source: dbA.name,
      target: dbB.name
    };
    var result = repDb.save(repDoc);
    TEquals(true, result.ok, "should allow anonymous replication docs in admin party");
    // new docs should get an owner field enforced. In admin party mode owner is null
    repDoc = repDb.open(repDoc._id);
    TIsnull(repDoc.owner, "owner should be null in admin party");

// Uncomment when _users database security changes are implemented.
//
//     var jchrisDoc = {
//       _id: "org.couchdb.user:jchris",
//       type: "user",
//       name: "jchris",
//       password: "mp3",
//       roles: []
//     };
    var jchrisDoc = CouchDB.prepareUserDoc({
      name: "jchris",
      roles: []
    }, "mp3");
    usersDb.save(jchrisDoc); // set up a non-admin user

// Uncomment when _users database security changes are implemented.
//
//     var jchrisDoc = {
//       _id: "org.couchdb.user:fdmanana",
//       type: "user",
//       name: "fdmanana",
//       password: "foobar",
//       roles: []
//     };
    var fdmananaDoc = CouchDB.prepareUserDoc({
      name: "fdmanana",
      roles: []
    }, "foobar");
    usersDb.save(fdmananaDoc); // set up a non-admin user

// Uncomment when _users database security changes are implemented.
//
//     var benoitcDoc = {
//       _id: "org.couchdb.user:fdmanana",
//       type: "user",
//       name: "fdmanana",
//       password: "foobar",
//       roles: []
//     };
    var benoitcDoc = CouchDB.prepareUserDoc({
      name: "benoitc",
      roles: []
    }, "test");
    usersDb.save(benoitcDoc); // set up a non-admin user

    T(repDb.setSecObj({
      "admins" : {
        roles : [],
        names : ["benoitc"]
      }
    }).ok);
    
    run_on_modified_server([
        {
          section: "admins",
          key: "jan",
          value: "apple"
        }
      ], function() {
        // replication docs from admin-party mode in non-admin party mode can not
        //   be edited by non-admins (non-server admins)
        repDoc = repDb.open(repDoc._id);
        repDoc.target = dbC.name;
        var result = save_as(repDb, repDoc, "jchris");
        TEquals("forbidden", result.error, "should forbid editing null-owner docs");

        // replication docs from admin-party mode in non-admin party mode can only
        //   be edited by admins (server admins)
        repDoc = waitForDocPos(repDb, repDoc._id, 3);
        repDoc.target = dbC.name;
        var result = save_as(repDb, repDoc, "jan");
        repDoc = open_as(repDb, repDoc._id, "jchris");
        TEquals(true, result.ok, "should allow editing null-owner docs to admins");
        TEquals("jan", repDoc.owner, "owner should be the admin now");

        // user can update their own replication docs (repDoc.owner)
        var jchrisRepDoc = {
          _id: "jchris-rep-doc",
          source: dbC.name,
          target: dbA.name,
          user_ctx: { name: "jchris", roles: [] }
        };

        var result = save_as(repDb, jchrisRepDoc, "jchris");
        TEquals(true, result.ok, "should create rep doc");
        jchrisRepDoc = repDb.open(jchrisRepDoc._id);
        TEquals("jchris", jchrisRepDoc.owner, "should assign correct owner");
        jchrisRepDoc = waitForDocPos(repDb, jchrisRepDoc._id, 3);
        jchrisRepDoc = open_as(repDb, jchrisRepDoc._id, "jchris");
        jchrisRepDoc.target = dbB.name;
        var result = save_as(repDb, jchrisRepDoc, "jchris");
        TEquals(true, result.ok, "should allow update of rep doc");

        // user should not be able to read from any view
        var ddoc = {
          _id: "_design/reps",
          views: {
            test: {
            map: "function(doc) {" +
              "if (doc._replication_state) { " +
                "emit(doc._id, doc._replication_state);" +
              "}" +
            "}"
            }
          }
        };

        save_as(repDb, ddoc, "jan");

        try {
          repDb.view("reps/test");
          T(false, "non-admin had view read access");
        } catch (ex) {
          TEquals("forbidden", ex.error,
            "non-admins should not be able to read a view");
        }

        // admin should be able to read from any view
        TEquals(true, CouchDB.login("jan", "apple").ok);
        var result = repDb.view("reps/test");
        CouchDB.logout();
        TEquals(2, result.total_rows, "should allow access and list two users");

        // test _all_docs, only available for _admins
        try {
          repDb.allDocs({include_docs: true});
          T(false, "non-admin had _all_docs access");
        } catch (ex) {
          TEquals("forbidden", ex.error,
            "non-admins should not be able to access _all_docs");
        }

        TEquals(true, CouchDB.login("jan", "apple").ok);
        try {
          repDb.allDocs({include_docs: true});
        } catch (ex) {
          T(false, "admin couldn't access _all_docs");
        }
        CouchDB.logout();

        try {
          repDb.view("reps/test");
          T(false, "non-admin had view read access");
        } catch (ex) {
          TEquals("forbidden", ex.error,
            "non-admins should not be able to read a view");
        }

        // admin should be able to read from any view
        TEquals(true, CouchDB.login("benoitc", "test").ok);
        var result = repDb.view("reps/test");
        CouchDB.logout();
        TEquals(2, result.total_rows, "should allow access and list two users");

        // test _all_docs, only available for _admins
        try {
          repDb.allDocs({include_docs: true});
          T(false, "non-admin had _all_docs access");
        } catch (ex) {
          TEquals("forbidden", ex.error,
            "non-admins should not be able to access _all_docs");
        }

        TEquals(true, CouchDB.login("benoitc", "test").ok);
        try {
          repDb.allDocs({include_docs: true});
        } catch (ex) {
          T(false, "admin couldn't access _all_docs");
        }
        CouchDB.logout();

        // Verify that users can't access credentials in the "source" and
        // "target" fields of replication documents owned by other users.
        var fdmananaRepDoc = {
          _id: "fdmanana-rep-doc",
          source: "http://fdmanana:foobar@" + CouchDB.host + "/" + dbC.name,
          target: dbA.name,
          user_ctx: { name: "fdmanana", roles: [] }
        };

        var result = save_as(repDb, fdmananaRepDoc, "fdmanana");
        TEquals(true, result.ok, "should create rep doc");
        waitForDocPos(repDb, fdmananaRepDoc._id, 3);
        fdmananaRepDoc = open_as(repDb, fdmananaRepDoc._id, "fdmanana");
        TEquals("fdmanana", fdmananaRepDoc.owner, "should assign correct owner");
        TEquals("http://fdmanana:foobar@" + CouchDB.host + "/" + dbC.name,
           fdmananaRepDoc.source, "source field has credentials");

        fdmananaRepDoc = open_as(repDb, fdmananaRepDoc._id, "jchris");
        TEquals("fdmanana", fdmananaRepDoc.owner, "should assign correct owner");
        TEquals("http://" + CouchDB.host + "/" + dbC.name,
           fdmananaRepDoc.source, "source field doesn't contain credentials");

        // _changes?include_docs=true, users shouldn't be able to see credentials
        // in documents owned by other users.
        var changes = repChanges("jchris");
        var doc = changes.results[changes.results.length - 1].doc;
        TEquals(fdmananaRepDoc._id, doc._id, "Got the right doc from _changes");
        TEquals("http://" + CouchDB.host + "/" + dbC.name,
           doc.source, "source field doesn't contain credentials (doc from _changes)");
        CouchDB.logout();

        // _changes?include_docs=true, user should be able to see credentials
        // in documents they own.
        var changes = repChanges("fdmanana");
        var doc = changes.results[changes.results.length - 1].doc;
        TEquals(fdmananaRepDoc._id, doc._id, "Got the right doc from _changes");
        TEquals("http://fdmanana:foobar@" + CouchDB.host + "/" + dbC.name,
           doc.source, "source field contains credentials (doc from _changes)");
        CouchDB.logout();

        // _changes?include_docs=true, admins should be able to see credentials
        // from all documents.
        var changes = repChanges("jan");
        var doc = changes.results[changes.results.length - 1].doc;
        TEquals(fdmananaRepDoc._id, doc._id, "Got the right doc from _changes");
        TEquals("http://fdmanana:foobar@" + CouchDB.host + "/" + dbC.name,
           doc.source, "source field contains credentials (doc from _changes)");
        CouchDB.logout();

        // _changes?include_docs=true, db admins should be able to see credentials
        // from all documents.
        var changes = repChanges("benoitc");
        var doc = changes.results[changes.results.length - 1].doc;
        TEquals(fdmananaRepDoc._id, doc._id, "Got the right doc from _changes");
        TEquals("http://fdmanana:foobar@" + CouchDB.host + "/" + dbC.name,
           doc.source, "source field contains credentials (doc from _changes)");
        CouchDB.logout();

        var fdmananaRepDocOAuth = {
          _id: "fdmanana-rep-doc-oauth",
          source: dbC.name,
          target: {
            url: "http://" + CouchDB.host + "/" + dbA.name,
            oauth: {
              token: "abc",
              token_secret: "foo",
              consumer_key: "123",
              consumer_secret: "321"
            }
          },
          user_ctx: { name: "fdmanana", roles: [] }
        };

        var result = save_as(repDb, fdmananaRepDocOAuth, "fdmanana");
        TEquals(true, result.ok, "should create rep doc");
        waitForDocPos(repDb, fdmananaRepDocOAuth._id, 3);
        fdmananaRepDocOAuth = open_as(repDb, fdmananaRepDocOAuth._id, "fdmanana");
        TEquals("fdmanana", fdmananaRepDocOAuth.owner, "should assign correct owner");
        TEquals("object", typeof fdmananaRepDocOAuth.target.oauth,
          "target field has oauth credentials");

        fdmananaRepDocOAuth = open_as(repDb, fdmananaRepDocOAuth._id, "jchris");
        TEquals("fdmanana", fdmananaRepDocOAuth.owner, "should assign correct owner");
        TEquals("undefined", typeof fdmananaRepDocOAuth.target.oauth,
          "target field doesn't have oauth credentials");

        // ensure "old" replicator docs still work
        // done in replicator_db.js?

        // Login as admin so run_on_modified_server can do its cleanup.
        TEquals(true, CouchDB.login("jan", "apple").ok);
      });
  };

  run_on_modified_server([
    {
      section: "couch_httpd_auth",
      key: "authentication_db",
      value: usersDb.name
    },
    {
      section: "replicator",
      key: "db",
      value: repDb.name
    }],
    testFun
  );

  // cleanup
  usersDb.deleteDb();
  repDb.deleteDb();
};
