/**
 * This tests that user defined roles actually grant users the ability to perform the actions they
 * should, and that changing the privileges assigned to roles changes the access granted to the user
 */

function runTest(conn) {
    var authzErrorCode = 13;
    var hasAuthzError = function(result) {
        assert(result.hasWriteError());
        assert.eq(authzErrorCode, result.getWriteError().code);
    };

    conn.getDB('admin').createUser({user: 'admin', pwd: 'pwd', roles: ['root']});
    conn.getDB('admin').auth('admin', 'pwd');
    conn.getDB('admin')
        .createUser({user: 'userAdmin', pwd: 'pwd', roles: ['userAdminAnyDatabase']});
    conn.getDB('admin').logout();

    var userAdminConn = new Mongo(conn.host);
    var adminUserAdmin = userAdminConn.getDB('admin');
    adminUserAdmin.auth('userAdmin', 'pwd');
    adminUserAdmin.createRole({role: 'adminRole', privileges: [], roles: []});
    var testUserAdmin = userAdminConn.getDB('test');
    testUserAdmin.createRole({role: 'testRole1', privileges: [], roles: []});
    testUserAdmin.createRole({role: 'testRole2', privileges: [], roles: ['testRole1']});
    testUserAdmin.createUser(
        {user: 'testUser', pwd: 'pwd', roles: ['testRole2', {role: 'adminRole', db: 'admin'}]});

    var testDB = conn.getDB('test');
    assert(testDB.auth('testUser', 'pwd'));

    // At this point there are 3 db handles in use.  testUserAdmin and adminUserAdmin are handles to
    // the "test" and "admin" dbs respectively.  Both testUserAdmin and adminUserAdmin are on the
    // same connection (userAdminConn) which has been auth'd as a user with the
    // 'userAdminAnyDatabase' role.  Those will be used for manipulating the user defined roles
    // used in the test.  "testDB" is a handle to the test database on a connection that has been
    // auth'd as 'testUser@test' - this is the connection that will be used to test how privilege
    // enforcement works.

    // test CRUD
    hasAuthzError(testDB.foo.insert({a: 1}));
    assert.throws(function() {
        testDB.foo.findOne();
    });

    testUserAdmin.grantPrivilegesToRole(
        'testRole1', [{resource: {db: 'test', collection: ''}, actions: ['find']}]);

    hasAuthzError(testDB.foo.insert({a: 1}));
    assert.doesNotThrow(function() {
        testDB.foo.findOne();
    });
    assert.eq(0, testDB.foo.count());
    assert.eq(0, testDB.foo.find().itcount());

    testUserAdmin.grantPrivilegesToRole(
        'testRole1', [{resource: {db: 'test', collection: 'foo'}, actions: ['insert']}]);

    assert.writeOK(testDB.foo.insert({a: 1}));
    assert.eq(1, testDB.foo.findOne().a);
    assert.eq(1, testDB.foo.count());
    assert.eq(1, testDB.foo.find().itcount());
    hasAuthzError(testDB.foo.update({a: 1}, {$inc: {a: 1}}));
    assert.eq(1, testDB.foo.findOne().a);

    hasAuthzError(testDB.bar.insert({a: 1}));
    assert.eq(0, testDB.bar.count());

    adminUserAdmin.grantPrivilegesToRole(
        'adminRole', [{resource: {db: '', collection: 'foo'}, actions: ['update']}]);
    assert.writeOK(testDB.foo.update({a: 1}, {$inc: {a: 1}}));
    assert.eq(2, testDB.foo.findOne().a);
    assert.writeOK(testDB.foo.update({b: 1}, {$inc: {b: 1}}, true));  // upsert
    assert.eq(2, testDB.foo.count());
    assert.eq(2, testDB.foo.findOne({b: {$exists: true}}).b);
    hasAuthzError(testDB.foo.remove({b: 2}));
    assert.eq(2, testDB.foo.count());

    adminUserAdmin.grantPrivilegesToRole(
        'adminRole', [{resource: {db: '', collection: ''}, actions: ['remove']}]);
    assert.writeOK(testDB.foo.remove({b: 2}));
    assert.eq(1, testDB.foo.count());

    // Test revoking privileges
    testUserAdmin.revokePrivilegesFromRole(
        'testRole1', [{resource: {db: 'test', collection: 'foo'}, actions: ['insert']}]);
    hasAuthzError(testDB.foo.insert({a: 1}));
    assert.eq(1, testDB.foo.count());
    assert.writeOK(testDB.foo.update({a: 2}, {$inc: {a: 1}}));
    assert.eq(3, testDB.foo.findOne({a: {$exists: true}}).a);
    hasAuthzError(testDB.foo.update({c: 1}, {$inc: {c: 1}}, true));  // upsert should fail
    assert.eq(1, testDB.foo.count());

    // Test changeOwnPassword/changeOwnCustomData
    assert.throws(function() {
        testDB.changeUserPassword('testUser', 'password');
    });
    assert.throws(function() {
        testDB.updateUser('testUser', {customData: {zipCode: 10036}});
    });
    assert.eq(null, testDB.getUser('testUser').customData);
    testUserAdmin.grantPrivilegesToRole(
        'testRole1',
            [{
               resource: {db: 'test', collection: ''},
               actions: ['changeOwnPassword', 'changeOwnCustomData']
            }]);
    testDB.changeUserPassword('testUser', 'password');
    assert(!testDB.auth('testUser', 'pwd'));
    assert(testDB.auth('testUser', 'password'));
    testDB.updateUser('testUser', {customData: {zipCode: 10036}});
    assert.eq(10036, testDB.getUser('testUser').customData.zipCode);

    testUserAdmin.revokeRolesFromRole('testRole2', ['testRole1']);
    assert.throws(function() {
        testDB.changeUserPassword('testUser', 'pwd');
    });
    assert.throws(function() {
        testDB.foo.findOne();
    });
    assert.throws(function() {
        testDB.updateUser('testUser', {customData: {zipCode: 10028}});
    });
    assert.eq(10036, testDB.getUser('testUser').customData.zipCode);

    // Test changeAnyPassword/changeAnyCustomData
    testUserAdmin.grantPrivilegesToRole('testRole2',
                                            [{
                                               resource: {db: 'test', collection: ''},
                                               actions: ['changePassword', 'changeCustomData']
                                            }]);
    testDB.changeUserPassword('testUser', 'pwd');
    assert(!testDB.auth('testUser', 'password'));
    assert(testDB.auth('testUser', 'pwd'));
    testDB.updateUser('testUser', {customData: {zipCode: 10028}});
    assert.eq(10028, testDB.getUser('testUser').customData.zipCode);

    // Test privileges on the cluster resource
    assert.commandFailed(testDB.runCommand({serverStatus: 1}));
    adminUserAdmin.grantPrivilegesToRole(
        'adminRole', [{resource: {cluster: true}, actions: ['serverStatus']}]);
    assert.commandWorked(testDB.serverStatus());
}

jsTest.log('Test standalone');
var conn = MongoRunner.runMongod({auth: ''});
runTest(conn);
MongoRunner.stopMongod(conn.port);

jsTest.log('Test sharding');
var st = new ShardingTest({shards: 2, config: 3, keyFile: 'jstests/libs/key1'});
runTest(st.s);
st.stop();
