/*

Exhaustive test for authorization of commands with user-defined roles.

The test logic implemented here operates on the test cases defined
in jstests/auth/commands.js.

*/

// constants
var testUser = "userDefinedRolesTestUser";
var testRole = "userDefinedRolesTestRole";

load("jstests/auth/lib/commands_lib.js");

function testProperAuthorization(conn, t, testcase) {
    var out = "";

    var runOnDb = conn.getDB(testcase.runOnDb);
    var firstDb = conn.getDB(firstDbName);
    var adminDb = conn.getDB(adminDbName);

    authCommandsLib.setup(conn, t, runOnDb);

    adminDb.auth("admin", "password");
    assert.commandWorked(
        adminDb.runCommand({updateRole: testRole, privileges: testcase.privileges}));
    adminDb.logout();

    assert(adminDb.auth(testUser, "password"));

    var res = runOnDb.runCommand(t.command);

    if (!testcase.expectFail && res.ok != 1 && res.code != commandNotSupportedCode) {
        // don't error if the test failed with code commandNotSupported since
        // some storage engines (e.g wiredTiger) don't support some commands (e.g. touch)
        out = "command failed with " + tojson(res) + " on db " + testcase.runOnDb +
            " with privileges " + tojson(testcase.privileges);
    } else if (testcase.expectFail && res.code == authErrCode) {
        out = "expected authorization success" + " but received " + tojson(res) + " on db " +
            testcase.runOnDb + " with privileges " + tojson(testcase.privileges);
    }

    firstDb.logout();
    authCommandsLib.teardown(conn, t, runOnDb);
    return out;
}

function testInsufficientPrivileges(conn, t, testcase, privileges) {
    var out = "";

    var runOnDb = conn.getDB(testcase.runOnDb);
    var firstDb = conn.getDB(firstDbName);
    var adminDb = conn.getDB(adminDbName);

    authCommandsLib.setup(conn, t, runOnDb);

    adminDb.auth("admin", "password");
    assert.commandWorked(adminDb.runCommand({updateRole: testRole, privileges: privileges}));
    adminDb.logout();

    assert(adminDb.auth(testUser, "password"));

    var res = runOnDb.runCommand(t.command);

    if (res.ok == 1 || res.code != authErrCode) {
        out = "expected authorization failure " + " but received " + tojson(res) +
            " with privileges " + tojson(privileges);
    }

    firstDb.logout();
    authCommandsLib.teardown(conn, t, runOnDb);
    return out;
}

function runOneTest(conn, t) {
    var failures = [];
    var msg;

    for (var i = 0; i < t.testcases.length; i++) {
        var testcase = t.testcases[i];
        if (!("privileges" in testcase)) {
            continue;
        }
        // Make a copy of the priviliges array since it will be modified.
        var privileges = testcase.privileges.map(function(p) {
            return Object.extend({}, p, true);
        });

        if (testcase.expectAuthzFailure) {
            msg = testInsufficientPrivileges(conn, t, testcase, privileges);
            if (msg) {
                failures.push(t.testname + ": " + msg);
            }
            continue;
        }

        if ((privileges.length == 1 && privileges[0].actions.length > 1) || privileges.length > 1) {
            for (var j = 0; j < privileges.length; j++) {
                var p = privileges[j];
                var resource = p.resource;
                var actions = p.actions;

                for (var k = 0; k < actions.length; k++) {
                    var privDoc = {
                        resource: resource,
                        actions: [actions[k]]
                    };
                    msg = testInsufficientPrivileges(conn, t, testcase, [privDoc]);
                    if (msg) {
                        failures.push(t.testname + ": " + msg);
                    }
                }
            }
        }

        msg = testProperAuthorization(conn, t, testcase);
        if (msg) {
            failures.push(t.testname + ": " + msg);
        }
        // test resource pattern where collection is ""
        privileges.forEach(function(j) {
            if (j.resource.collection && !j.resource.collection.startsWith('system.')) {
                j.resource.collection = "";
            }
        });
        msg = testProperAuthorization(conn, t, testcase);
        if (msg) {
            failures.push(t.testname + ": " + msg);
        }
        // test resource pattern where database is ""
        privileges.forEach(function(j) {
            if (j.resource.db) {
                j.resource.db = "";
            }
        });
        msg = testProperAuthorization(conn, t, testcase);
        if (msg) {
            failures.push(t.testname + ": " + msg);
        }
    }

    return failures;
}

function createUsers(conn) {
    var adminDb = conn.getDB(adminDbName);
    var firstDb = conn.getDB(firstDbName);
    adminDb.createUser({user: "admin", pwd: "password", roles: ["__system"]});

    assert(adminDb.auth("admin", "password"));

    assert.commandWorked(adminDb.runCommand({createRole: testRole, privileges: [], roles: []}));
    assert.commandWorked(adminDb.runCommand(
        {createUser: testUser, pwd: "password", roles: [{role: testRole, db: adminDbName}]}));

    adminDb.logout();
}

var opts = {
    auth: "",
    enableExperimentalStorageDetailsCmd: ""
};
var impls = {
    createUsers: createUsers,
    runOneTest: runOneTest
};

// run all tests standalone
var conn = MongoRunner.runMongod(opts);
authCommandsLib.runTests(conn, impls);
MongoRunner.stopMongod(conn);

// run all tests sharded
conn = new ShardingTest(
    {shards: 2, mongos: 1, keyFile: "jstests/libs/key1", other: {shardOptions: opts}});
authCommandsLib.runTests(conn, impls);
conn.stop();
