summaryrefslogtreecommitdiff
path: root/jstests/replsets/rollback_auth.js
blob: 5c08e4b3faecf47d501efaecf23fd9e1f6fdb005 (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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
// Tests rollback of auth data in replica sets.
// This test creates a user and then does two different sets of updates to that user's privileges
// using the replSetTest command to trigger a rollback and verify that at the end the access control
// data is rolled back correctly and the user only has access to the expected collections.
//
// If all data-bearing nodes in a replica set are using an ephemeral storage engine, the set will
// not be able to survive a scenario where all data-bearing nodes are down simultaneously. In such a
// scenario, none of the members will have any data, and upon restart will each look for a member to
// inital sync from, so no primary will be elected. This test induces such a scenario, so cannot be
// run on ephemeral storage engines.
// @tags: [requires_persistence]

(function() {
"use strict";

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

// helper function for verifying contents at the end of the test
function checkFinalResults(db) {
    assert.commandWorked(db.runCommand({dbStats: 1}));
    assert.commandFailedWithCode(db.runCommand({collStats: 'foo'}), authzErrorCode);
    assert.commandFailedWithCode(db.runCommand({collStats: 'bar'}), authzErrorCode);
    assert.commandWorked(db.runCommand({collStats: 'baz'}));
    assert.commandWorked(db.runCommand({collStats: 'foobar'}));
}

const authzErrorCode = 13;

jsTestLog("Setting up replica set");

const name = "rollbackAuth";
const replTest = new ReplSetTest({name: name, nodes: 3, keyFile: 'jstests/libs/key1'});
const nodes = replTest.nodeList();
const conns = replTest.startSet();
replTest.initiate({
    "_id": "rollbackAuth",
    "members": [
        {"_id": 0, "host": nodes[0], "priority": 3},
        {"_id": 1, "host": nodes[1]},
        {"_id": 2, "host": nodes[2], arbiterOnly: true}
    ]
});

// Make sure we have a primary
replTest.waitForState(replTest.nodes[0], ReplSetTest.State.PRIMARY);
const primary = replTest.getPrimary();
const a_conn = conns[0];
const b_conn = conns[1];
a_conn.setSecondaryOk();
b_conn.setSecondaryOk();
const A_admin = a_conn.getDB("admin");
const B_admin = b_conn.getDB("admin");
const A_test = a_conn.getDB("test");
const B_test = b_conn.getDB("test");
assert.eq(primary, conns[0], "conns[0] assumed to be primary");
assert.eq(a_conn, primary);

// Make sure we have an arbiter
assert.soon(function() {
    const res = conns[2].getDB("admin").runCommand({replSetGetStatus: 1});
    return res.myState == 7;
}, "Arbiter failed to initialize.");

jsTestLog("Creating initial data");

// Create collections that will be used in test
A_admin.createUser({user: 'admin', pwd: 'pwd', roles: ['root']});
A_admin.auth('admin', 'pwd');

// Set up user admin user
A_admin.createUser({user: 'userAdmin', pwd: 'pwd', roles: ['userAdminAnyDatabase']});

A_test.foo.insert({a: 1});
A_test.bar.insert({a: 1});
A_test.baz.insert({a: 1});
A_test.foobar.insert({a: 1});
A_admin.logout();

assert(A_admin.auth('userAdmin', 'pwd'));

// Give replication time to catch up.
assert.soon(function() {
    return B_admin.auth('userAdmin', 'pwd');
});

// Create a basic user and role
A_admin.createRole({
    role: 'replStatusRole',  // To make awaitReplication() work
    roles: [],
    privileges: [
        {resource: {cluster: true}, actions: ['replSetGetStatus']},
        {resource: {db: 'local', collection: ''}, actions: ['find']},
        {resource: {db: 'local', collection: 'system.replset'}, actions: ['find']}
    ]
});
A_test.createRole({
    role: 'myRole',
    roles: [],
    privileges: [{resource: {db: 'test', collection: ''}, actions: ['dbStats']}]
});
A_test.createUser(
    {user: 'spencer', pwd: 'pwd', roles: ['myRole', {role: 'replStatusRole', db: 'admin'}]});

A_admin.logout();
B_admin.logout();

assert(A_test.auth('spencer', 'pwd'));

// wait for secondary to get this data
assert.soon(function() {
    return B_test.auth('spencer', 'pwd');
});

assert.commandWorked(A_test.runCommand({dbStats: 1}));
assert.commandFailedWithCode(A_test.runCommand({collStats: 'foo'}), authzErrorCode);
assert.commandFailedWithCode(A_test.runCommand({collStats: 'bar'}), authzErrorCode);
assert.commandFailedWithCode(A_test.runCommand({collStats: 'baz'}), authzErrorCode);
assert.commandFailedWithCode(A_test.runCommand({collStats: 'foobar'}), authzErrorCode);

assert.commandWorked(B_test.runCommand({dbStats: 1}));
assert.commandFailedWithCode(B_test.runCommand({collStats: 'foo'}), authzErrorCode);
assert.commandFailedWithCode(B_test.runCommand({collStats: 'bar'}), authzErrorCode);
assert.commandFailedWithCode(B_test.runCommand({collStats: 'baz'}), authzErrorCode);
assert.commandFailedWithCode(B_test.runCommand({collStats: 'foobar'}), authzErrorCode);

jsTestLog("Doing writes that will eventually be rolled back");

// down A and wait for B to become primary
A_test.logout();
replTest.stop(0);
assert.soon(function() {
    try {
        return B_admin.hello().isWritablePrimary;
    } catch (e) {
        return false;
    }
}, "B didn't become primary");
printjson(assert.commandWorked(B_test.adminCommand('replSetGetStatus')));
B_test.logout();

// Modify the the user and role in a way that will be rolled back.
assert(B_admin.auth('admin', 'pwd'));
B_test.grantPrivilegesToRole(
    'myRole',
    [{resource: {db: 'test', collection: 'foo'}, actions: ['collStats']}],
    {});  // Default write concern will wait for majority, which will time out.
B_test.createRole({
    role: 'temporaryRole',
    roles: [],
    privileges: [{resource: {db: 'test', collection: 'bar'}, actions: ['collStats']}]
},
                  {});  // Default write concern will wait for majority, which will time out.
B_test.grantRolesToUser('spencer',
                        ['temporaryRole'],
                        {});  // Default write concern will wait for majority, which will time out.
B_admin.logout();

assert(B_test.auth('spencer', 'pwd'));
assert.commandWorked(B_test.runCommand({dbStats: 1}));
assert.commandWorked(B_test.runCommand({collStats: 'foo'}));
assert.commandWorked(B_test.runCommand({collStats: 'bar'}));
assert.commandFailedWithCode(B_test.runCommand({collStats: 'baz'}), authzErrorCode);
assert.commandFailedWithCode(B_test.runCommand({collStats: 'foobar'}), authzErrorCode);
B_test.logout();

// down B, bring A back up, then wait for A to become primary
// insert new data into A so that B will need to rollback when it reconnects to A
replTest.stop(1);

replTest.restart(0);
assert.soon(function() {
    try {
        return A_admin.hello().isWritablePrimary;
    } catch (e) {
        return false;
    }
}, "A didn't become primary");

// A should not have the new data as it was down
assert(A_test.auth('spencer', 'pwd'));
assert.commandWorked(A_test.runCommand({dbStats: 1}));
assert.commandFailedWithCode(A_test.runCommand({collStats: 'foo'}), authzErrorCode);
assert.commandFailedWithCode(A_test.runCommand({collStats: 'bar'}), authzErrorCode);
assert.commandFailedWithCode(A_test.runCommand({collStats: 'baz'}), authzErrorCode);
assert.commandFailedWithCode(A_test.runCommand({collStats: 'foobar'}), authzErrorCode);
A_test.logout();

jsTestLog("Doing writes that should persist after the rollback");
// Modify the user and role in a way that will persist.
A_admin.auth('userAdmin', 'pwd');
// Default write concern will wait for majority, which would time out
// so we override it with an empty write concern
A_test.grantPrivilegesToRole(
    'myRole', [{resource: {db: 'test', collection: 'baz'}, actions: ['collStats']}], {});

A_test.createRole({
    role: 'persistentRole',
    roles: [],
    privileges: [{resource: {db: 'test', collection: 'foobar'}, actions: ['collStats']}]
},
                  {});
A_test.grantRolesToUser('spencer', ['persistentRole'], {});
A_admin.logout();

A_test.auth('spencer', 'pwd');

// A has the data we just wrote, but not what B wrote before
checkFinalResults(A_test);

jsTestLog("Triggering rollback");

// bring B back in contact with A
// as A is primary, B will roll back and then catch up
replTest.restart(1);
assert.soonNoExcept(function() {
    authutil.asCluster(replTest.nodes, 'jstests/libs/key1', function() {
        replTest.awaitReplication();
    });

    return B_test.auth('spencer', 'pwd');
});
// Now both A and B should agree
checkFinalResults(A_test);
checkFinalResults(B_test);

A_test.logout();

// Verify data consistency between nodes.
authutil.asCluster(replTest.nodes, 'jstests/libs/key1', function() {
    replTest.checkOplogs();
});

// DB hash check is done in stopSet.
replTest.stopSet();
}());