// Check if this build supports the authenticationMechanisms startup parameter.
load("jstests/libs/logv2_helpers.js");

const SERVER_CERT = "jstests/libs/server.pem";
const SERVER_SAN_CERT = "jstests/libs/server_SAN.pem";
const CLIENT_CERT = "jstests/libs/client.pem";
const CA_CERT = "jstests/libs/ca.pem";

const SERVER_USER = "CN=server,OU=Kernel,O=MongoDB,L=New York City,ST=New York,C=US";
const SERVER_SAN_USER =
    "CN=Kernel Client Peer Role,OU=Kernel,O=MongoDB,L=New York City,ST=New York,C=US";
const CLIENT_USER = "CN=client,OU=KernelUser,O=MongoDB,L=New York City,ST=New York,C=US";

function authAndTest(cert, user) {
    const INVALID_USER = "C=US,ST=New York,L=New York City,O=MongoDB,OU=KernelUser,CN=invalid";

    let external = db.getSiblingDB("$external");
    let test = db.getSiblingDB("test");

    assert(!external.auth({user: INVALID_USER, mechanism: 'MONGODB-X509'}),
           "authentication with invalid user should fail");
    assert(external.auth({user: user, mechanism: 'MONGODB-X509'}),
           "authentication with valid user failed");
    assert(external.auth({mechanism: 'MONGODB-X509'}),
           "authentication with valid cert and no user field failed");
    assert(external.runCommand({authenticate: 1, mechanism: 'MONGODB-X509', user: user}).ok,
           "runCommand authentication with valid cert and user field failed");
    assert(external.runCommand({authenticate: 1, mechanism: 'MONGODB-X509'}).ok,
           "runCommand authentication with valid cert and no user field failed");
    // Smoke our current user with a find.
    test.foo.findOne();

    // Check that we can add a user and read data.
    test.createUser(
        {user: "test", pwd: "test", roles: [{'role': 'readWriteAnyDatabase', 'db': 'admin'}]});
    test.foo.findOne();

    // Reads are not allowed after logout.
    external.logout();
    assert.throws(function() {
        test.foo.findOne();
    }, [], "read after logout");
}

function runSubShell(conn, cert, user, func) {
    const args = [
        'mongo',
        '--tls',
        `--tlsCAFile=${CA_CERT}`,
        `--tlsCertificateKeyFile=${cert}`,
        '--tlsAllowInvalidHostnames',
        '--authenticationDatabase=$external',
        '--authenticationMechanism=MONGODB-X509',
        `mongodb://${conn.host}`,
        '--eval',
        `(${func.toString()})('${cert}', '${user}');`
    ];
    const ret = _runMongoProgram(...args);
    assert(ret == ErrorCodes.OK, 'subshell did not succeed');
}

function initUser(conn, user) {
    const external = conn.getDB("$external");
    external.createUser({
        user: user,
        roles: [
            {'role': 'userAdminAnyDatabase', 'db': 'admin'},
            {'role': 'readWriteAnyDatabase', 'db': 'admin'},
            {'role': 'clusterMonitor', 'db': 'admin'},
        ]
    });

    // Localhost exception should not be in place anymore
    const test = conn.getDB("test");
    assert.throws(function() {
        test.foo.findOne();
    }, [], "read without login");
}

const x509_options = {
    sslMode: "requireSSL",
    sslPEMKeyFile: SERVER_CERT,
    sslCAFile: CA_CERT
};

const mongodOptions =
    Object.merge(x509_options, {auth: "", setParameter: {enforceUserClusterSeparation: false}});

const mongosOptions =
    Object.merge(x509_options, {setParameter: {enforceUserClusterSeparation: false}});

function runMongodTest(desc, func) {
    print(desc);
    const mongo = MongoRunner.runMongod(mongodOptions);
    func(mongo);

    MongoRunner.stopMongod(mongo);
}

function runMongodFailTest(desc, options) {
    print(desc);
    assert.throws(() => MongoRunner.runMongod(Object.merge(mongodOptions, options)),
                  [],
                  "MongoD started successfully with bad options");
}

function runMongosTest(desc, func) {
    print(desc);
    const st = new ShardingTest({
        shards: 1,
        mongos: 1,
        other: {
            keyFile: 'jstests/libs/key1',
            configOptions: mongodOptions,
            mongosOptions: mongosOptions,
            shardOptions: x509_options,
            useHostname: false
        }
    });

    const mongo = new Mongo(`localhost:${st.s0.port}`);
    func(mongo);
    st.stop();
}

function runMongosFailTest(desc, options) {
    print(desc);
    // We start the ShardingTest cleanly first because it throws and fails to clean up after itself.
    const st = new ShardingTest({
        config: 1,
        shards: 1,
        mongos: 1,
        other: {
            keyFile: 'jstests/libs/key1',
            configOptions: mongodOptions,
            mongosOptions: mongosOptions,
            shardOptions: x509_options,
            useHostname: false
        }
    });

    const failOptions = Object.merge(mongosOptions, options);
    print(`Fail options: ${tojson(failOptions)}`);

    assert.throws(function() {
        // Start a new mongos with bad options.
        st.restartMongos(0, failOptions);
    }, [], "MongoS restarted successfully with bad options");

    // Avoid st.stop() because it will throw when it attempts to stop the second mongos.
    st.stopAllShards();
    st.stopAllConfigServers();
}

runMongodTest("1a. Testing x.509 auth to mongod with a client user/cert", function(conn) {
    initUser(conn, CLIENT_USER);

    runSubShell(conn, CLIENT_CERT, CLIENT_USER, authAndTest);
});

runMongodTest("1b. Testing x.509 auth to mongod with the server user/cert", function(conn) {
    initUser(conn, SERVER_USER);

    runSubShell(conn, SERVER_CERT, SERVER_USER, authAndTest);
});

runMongodTest("1c. Testing x.509 auth to mongod with a cluster user/cert", function(conn) {
    initUser(conn, SERVER_SAN_USER);

    runSubShell(conn, SERVER_SAN_CERT, SERVER_SAN_USER, authAndTest);
});

runMongodFailTest('1d. Testing x.509 cluster auth on mongod with "x509" option',
                  {clusterAuthMode: "x509"});

runMongodFailTest('1e. Testing x.509 cluster auth on mongod with "sendX509" option',
                  {clusterAuthMode: "sendX509"});

runMongodFailTest('1e. Testing x.509 cluster auth on mongod with "sendKeyFile" option',
                  {clusterAuthMode: "sendKeyFile"});

runMongosTest("2a. Testing x.509 auth to mongos with a client user/cert", function(conn) {
    initUser(conn, CLIENT_USER);

    runSubShell(conn, CLIENT_CERT, CLIENT_USER, authAndTest);
});

runMongosTest("2b. Testing x.509 auth to mongos with the server user/cert", function(conn) {
    initUser(conn, SERVER_USER);

    runSubShell(conn, SERVER_CERT, SERVER_USER, authAndTest);
});

runMongosTest("2c. Testing x.509 auth to mongos with a cluster user/cert", function(conn) {
    initUser(conn, SERVER_SAN_USER);

    runSubShell(conn, SERVER_SAN_CERT, SERVER_SAN_USER, authAndTest);
});

runMongosFailTest('2d. Testing x.509 cluster auth on mongos with "x509" option',
                  {restart: true, clusterAuthMode: "x509"});

runMongosFailTest('2e. Testing x.509 cluster auth on mongos with "sendX509" option',
                  {clusterAuthMode: "sendX509"});

runMongosFailTest('2f. Testing x.509 cluster auth on mongos with "sendKeyFile" option',
                  {clusterAuthMode: "sendKeyFile"});
