// Test the killCursors command.
// @tags: [requires_sharding]
(function() {
'use strict';

// Multiple users cannot be authenticated on one connection within a session.
TestData.disableImplicitSessions = true;

function runTest(mongod) {
    /**
     * Open a cursor on `db` while authenticated as `authUsers`.
     * Then logout, and log back in as `killUsers` and try to kill that cursor.
     *
     * @param db - The db to create a cursor on and ultimately kill agains.
     * @param authUsers - Array of ['username', db] pairs to create the cursor under.
     * @param killUsers - Array of ['username', dn] pairs to use when killing.
     * @param shouldWork - Whether we expect success
     */
    function tryKill(db, authUsers, killUsers, shouldWork) {
        function loginAll(users) {
            users.forEach(function(u) {
                assert(u[1].auth(u[0], 'pass'));
            });
        }

        function logoutAll() {
            [testA, testB].forEach(function(d) {
                const users = assert.commandWorked(d.runCommand({connectionStatus: 1}))
                                  .authInfo.authenticatedUsers;
                users.forEach(function(u) {
                    mongod.getDB(u.db).logout();
                });
            });
        }

        function doKill(extra) {
            // Create a cursor to be killed later.
            loginAll(authUsers);
            let cmd = {find: db.coll.getName(), batchSize: 2};
            Object.assign(cmd, extra);
            const id = assert.commandWorked(db.runCommand(cmd)).cursor.id;
            assert.neq(id, 0, "Invalid cursor ID");
            logoutAll();

            loginAll(killUsers);
            const killCmd = db.runCommand({killCursors: db.coll.getName(), cursors: [id]});
            logoutAll();
            if (shouldWork) {
                assert.commandWorked(killCmd, "Unable to kill cursor");
            } else {
                assert.commandFailed(killCmd, "Should not have been able to kill cursor");
            }
        }

        doKill({});

        if ((authUsers.length === 1) && (killUsers.length === 1)) {
            // Session variant only makes sense with single auth'd users.
            doKill({lsid: {id: BinData(4, "QlLfPHTySm6tqfuV+EOsVA==")}});
        }
    }

    function trySelfKill(user) {
        const db = user[1];
        assert(db.auth(user[0], 'pass'));

        assert.commandWorked(db.runCommand({startSession: 1}));

        const cmd = {aggregate: 1, pipeline: [{$listLocalSessions: {}}], cursor: {batchSize: 0}};
        const res = assert.commandWorked(db.runCommand(cmd));
        print(tojson(res));
        const id = res.cursor.id;
        assert.neq(id, 0, "Invalid cursor ID");

        const killCmdRes = db.runCommand({killCursors: db.getName() + ".$cmd", cursors: [id]});
        db.logout();

        assert.commandWorked(killCmdRes, "Unable to kill cursor");
    }

    /**
     * Create user1/user2 in testA, and user3/user4 in testB.
     * Create two 101 element collections in testA and testB.
     * Use various combinations of those users to open cursors,
     * then (potentially) different combinations of users to kill them.
     *
     * A cursor should only be killable if at least one of the users
     * who created it is trying to kill it.
     */

    const testA = mongod.getDB('testA');
    const testB = mongod.getDB('testB');
    const admin = mongod.getDB('admin');

    // Setup users
    admin.createUser({user: 'admin', pwd: 'pass', roles: jsTest.adminUserRoles});
    assert(admin.auth('admin', 'pass'));

    testA.createUser({user: 'user1', pwd: 'pass', roles: jsTest.basicUserRoles});
    testA.createUser({user: 'user2', pwd: 'pass', roles: jsTest.basicUserRoles});
    testB.createUser({user: 'user3', pwd: 'pass', roles: jsTest.basicUserRoles});
    testB.createUser({user: 'user4', pwd: 'pass', roles: jsTest.basicUserRoles});
    testB.createUser({user: 'user5', pwd: 'pass', roles: []});
    admin.logout();

    // Create a collection with batchable data
    assert(testA.auth('user1', 'pass'));
    assert(testB.auth('user3', 'pass'));
    for (var i = 0; i < 101; ++i) {
        assert.commandWorked(testA.coll.insert({_id: i}));
        assert.commandWorked(testB.coll.insert({_id: i}));
    }
    testA.logout();
    testB.logout();

    // A user can kill their own cursor.
    tryKill(testA, [['user1', testA]], [['user1', testA]], true);
    tryKill(testA, [['user2', testA]], [['user2', testA]], true);
    tryKill(testB, [['user3', testB]], [['user3', testB]], true);
    tryKill(testB, [['user4', testB]], [['user4', testB]], true);
    trySelfKill(['user1', testA]);
    trySelfKill(['user5', testB]);
    trySelfKill(['admin', admin]);

    // A user cannot kill someone else's cursor.
    tryKill(testA, [['user1', testA]], [['user2', testA]], false);
    tryKill(testA, [['user1', testA]], [['user2', testA], ['user3', testB]], false);
    tryKill(testA, [['user2', testA]], [['user1', testA]], false);
    tryKill(testA, [['user2', testA]], [['user1', testA], ['user3', testB]], false);
    tryKill(testB, [['user3', testB]], [['user1', testA], ['user4', testB]], false);
    tryKill(testB, [['user3', testB]], [['user2', testA], ['user4', testB]], false);

    // A multi-owned cursor can be killed by any/all owner.
    tryKill(testA, [['user1', testA], ['user3', testB]], [['user1', testA]], true);
    tryKill(testB, [['user1', testA], ['user3', testB]], [['user3', testB]], true);
    tryKill(
        testA, [['user1', testA], ['user3', testB]], [['user1', testA], ['user3', testB]], true);
    tryKill(
        testA, [['user1', testA], ['user3', testB]], [['user2', testA], ['user3', testB]], true);
    tryKill(
        testB, [['user1', testA], ['user3', testB]], [['user1', testA], ['user3', testB]], true);
    tryKill(
        testB, [['user1', testA], ['user3', testB]], [['user1', testA], ['user4', testB]], true);

    // An owned cursor can not be killed by other user(s).
    tryKill(
        testA, [['user1', testA], ['user3', testB]], [['user2', testA], ['user4', testB]], false);
    tryKill(testA, [['user1', testA]], [['user2', testA], ['user3', testB]], false);
    tryKill(
        testA, [['user1', testA], ['user3', testB]], [['user2', testA], ['user4', testB]], false);

    // Admin can kill anything.
    tryKill(testA, [['user1', testA]], [['admin', admin]], true);
    tryKill(testA, [['user2', testA]], [['admin', admin]], true);
    tryKill(testB, [['user3', testB]], [['admin', admin]], true);
    tryKill(testB, [['user4', testB]], [['admin', admin]], true);
    tryKill(testA, [['user1', testA], ['user3', testB]], [['admin', admin]], true);
    tryKill(testB, [['user2', testA], ['user4', testB]], [['admin', admin]], true);
}

const mongod = MongoRunner.runMongod({auth: ""});
runTest(mongod);
MongoRunner.stopMongod(mongod);

const st =
    new ShardingTest({shards: 1, mongos: 1, config: 1, other: {keyFile: 'jstests/libs/key1'}});
runTest(st.s0);
st.stop();
})();
