summaryrefslogtreecommitdiff
path: root/jstests/auth/security_token.js
blob: 8ff91f8b6c242a8d7fdaadd388ae235216dde97f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
// Test passing security token with op messages.
// @tags: [requires_replication, requires_sharding]

(function() {
'use strict';

const tenantID = ObjectId();
const kLogLevelForToken = 5;
const kAcceptedSecurityTokenID = 5838100;
const kLogMessageID = 5060500;
const kLogoutMessageID = 6161506;
const kStaleAuthenticationMessageID = 6161507;
const isMongoStoreEnabled = TestData.setParameters.featureFlagMongoStore;

if (!isMongoStoreEnabled) {
    assert.throws(() => MongoRunner.runMongod({
        setParameter: "multitenancySupport=true",
    }));
    return;
}

function assertNoTokensProcessedYet(conn) {
    assert.eq(false,
              checkLog.checkContainsOnceJson(conn, kAcceptedSecurityTokenID, {}),
              'Unexpected security token has been processed');
}

function makeTokenAndExpect(user, db) {
    const authUser = {user: user, db: db, tenant: tenantID};

    const token = _createSecurityToken(authUser);
    jsTest.log('Using security token: ' + tojson(token));

    // Clone and rewrite OID and BinData fields to be roundtrip-safe.
    const expect = Object.assign({}, token);
    expect.authenticatedUser = Object.assign({}, token.authenticatedUser);
    expect.authenticatedUser.tenant = {'$oid': tenantID.str};
    expect.sig = {'$binary': {base64: token.sig.base64(), subType: '0'}};

    return [token, {token: expect}];
}

function runTest(conn, enabled, rst = undefined) {
    const admin = conn.getDB('admin');
    const tenantAdmin = conn.getDB(tenantID.str + '_admin');

    // Must be authenticated as a user with ActionType::useTenant in order to use $tenant
    assert.commandWorked(admin.runCommand({createUser: 'admin', pwd: 'pwd', roles: ['root']}));
    assert(admin.auth('admin', 'pwd'));

    // Create a tenant-local user.
    const createUserCmd =
        {createUser: 'user1', "$tenant": tenantID, pwd: 'pwd', roles: ['readWriteAnyDatabase']};
    if (enabled) {
        assert.commandWorked(admin.runCommand(createUserCmd));

        // Confirm the user exists on the tenant authz collection only, and not the global
        // collection.
        assert.eq(admin.system.users.count({user: 'user1'}),
                  0,
                  'user1 should not exist on global users collection');
        assert.eq(tenantAdmin.system.users.count({user: 'user1'}),
                  1,
                  'user1 should exist on tenant users collection');
    } else {
        assert.commandFailed(admin.runCommand(createUserCmd));
    }

    if (rst) {
        rst.awaitReplication();
    }

    // Dial up the logging to watch for tenant ID being processed.
    const originalLogLevel =
        assert.commandWorked(admin.setLogLevel(kLogLevelForToken)).was.verbosity;

    const tokenConn = new Mongo(conn.host);
    const tokenDB = tokenConn.getDB('admin');

    // Basic OP_MSG command.
    tokenConn._setSecurityToken({});
    assert.commandWorked(tokenDB.runCommand({ping: 1}));
    assertNoTokensProcessedYet(conn);

    // Test that no token equates to unauthenticated.
    assert.commandFailed(tokenDB.runCommand({features: 1}));

    // Passing a security token with unknown fields will always fail.
    tokenConn._setSecurityToken({invalid: 1});
    assert.commandFailed(tokenDB.runCommand({ping: 1}));
    assertNoTokensProcessedYet(conn);

    const [token, expect] = makeTokenAndExpect('user1', 'admin');
    tokenConn._setSecurityToken(token);

    if (enabled) {
        // Basic use.
        assert.commandWorked(tokenDB.runCommand({features: 1}));

        // Connection status, verify that the user/role info is returned without serializing tenant.
        const authInfo = assert.commandWorked(tokenDB.runCommand({connectionStatus: 1})).authInfo;
        jsTest.log(authInfo);
        assert.eq(authInfo.authenticatedUsers.length, 1);
        assert(0 === bsonWoCompare(authInfo.authenticatedUsers[0], {user: 'user1', db: 'admin'}));
        assert.eq(authInfo.authenticatedUserRoles.length, 1);
        assert(0 ===
               bsonWoCompare(authInfo.authenticatedUserRoles[0],
                             {role: 'readWriteAnyDatabase', db: 'admin'}));

        // Look for "Accepted Security Token" message with explicit tenant logging.
        jsTest.log('Checking for: ' + tojson(expect));
        checkLog.containsJson(conn, kAcceptedSecurityTokenID, expect, 'Security Token not logged');

        // Negative test, logMessage requires logMessage privilege on cluster (not granted)
        assert.commandFailed(tokenDB.runCommand({logMessage: 'This is a test'}));

        // CRUD operations not yet supported in multitenancy using security token.
        assert.writeError(tokenConn.getDB('test').coll1.insert({x: 1}));

        const log = checkLog.getGlobalLog(conn).map((l) => JSON.parse(l));

        // We successfully dispatched 2 commands as a token auth'd user.
        // The failed commands did not dispatch because they are forbidden in multitenancy.
        // We should see two post-operation logout events.
        const logoutMessages = log.filter((l) => (l.id === kLogoutMessageID));
        assert.eq(logoutMessages.length,
                  2,
                  'Unexpected number of logout messages: ' + tojson(logoutMessages));

        // None of those authorization sessions should remain active into their next requests.
        const staleMessages = log.filter((l) => (l.id === kStaleAuthenticationMessageID));
        assert.eq(
            staleMessages.length, 0, 'Unexpected stale authentications: ' + tojson(staleMessages));
    } else {
        assert.commandWorked(
            admin.runCommand({createUser: 'user1', pwd: 'pwd', roles: ['readWriteAnyDatabase']}));
        // Attempting to pass a valid looking security token will fail if not enabled.
        assert.commandFailed(tokenDB.runCommand({features: 1}));
    }

    // Restore logging and conn token before shutting down.
    assert.commandWorked(admin.setLogLevel(originalLogLevel));
}

function runTests(enabled) {
    const opts = {
        auth: '',
        setParameter: "multitenancySupport=" + (enabled ? 'true' : 'false'),
    };
    {
        const standalone = MongoRunner.runMongod(opts);
        assert(standalone !== null, "MongoD failed to start");
        runTest(standalone, enabled);
        MongoRunner.stopMongod(standalone);
    }
    {
        const rst = new ReplSetTest({nodes: 2, nodeOptions: opts});
        rst.startSet({keyFile: 'jstests/libs/key1'});
        rst.initiate();
        runTest(rst.getPrimary(), enabled, rst);
        rst.stopSet();
    }
    // Do not test sharding since mongos must have an authenticated connection to
    // all mongod nodes, and this conflicts with proxying tokens which we'll be
    // performing in mongoq.
}

runTests(true);
runTests(false);
})();