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
|
// 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";
// Arbiters don't replicate the admin.system.keys collection, so they can never validate or sign
// clusterTime. Gossiping a clusterTime to an arbiter as a user other than __system will fail,
// so we skip gossiping for this test.
//
// TODO SERVER-32639: remove this flag.
TestData.skipGossipingClusterTime = true;
// TODO SERVER-35447: 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
var checkFinalResults = function(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'}));
};
var authzErrorCode = 13;
jsTestLog("Setting up replica set");
var name = "rollbackAuth";
var replTest = new ReplSetTest({name: name, nodes: 3, keyFile: 'jstests/libs/key1'});
var nodes = replTest.nodeList();
var 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 master
replTest.waitForState(replTest.nodes[0], ReplSetTest.State.PRIMARY);
var master = replTest.getPrimary();
var a_conn = conns[0];
var b_conn = conns[1];
a_conn.setSlaveOk();
b_conn.setSlaveOk();
var A = a_conn.getDB("admin");
var B = b_conn.getDB("admin");
var a = a_conn.getDB("test");
var b = b_conn.getDB("test");
assert.eq(master, conns[0], "conns[0] assumed to be master");
assert.eq(a_conn, master);
// Make sure we have an arbiter
assert.soon(function() {
var 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.createUser({user: 'admin', pwd: 'pwd', roles: ['root']});
A.auth('admin', 'pwd');
a.foo.insert({a: 1});
a.bar.insert({a: 1});
a.baz.insert({a: 1});
a.foobar.insert({a: 1});
// Set up user admin user
A.createUser({user: 'userAdmin', pwd: 'pwd', roles: ['userAdminAnyDatabase']});
A.auth('userAdmin', 'pwd'); // Logs out of admin@admin user
B.auth('userAdmin', 'pwd');
// Create a basic user and role
A.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.createRole({
role: 'myRole',
roles: [],
privileges: [{resource: {db: 'test', collection: ''}, actions: ['dbStats']}]
});
a.createUser(
{user: 'spencer', pwd: 'pwd', roles: ['myRole', {role: 'replStatusRole', db: 'admin'}]});
assert(a.auth('spencer', 'pwd'));
// wait for secondary to get this data
assert.soon(function() {
return b.auth('spencer', 'pwd');
});
assert.commandWorked(a.runCommand({dbStats: 1}));
assert.commandFailedWithCode(a.runCommand({collStats: 'foo'}), authzErrorCode);
assert.commandFailedWithCode(a.runCommand({collStats: 'bar'}), authzErrorCode);
assert.commandFailedWithCode(a.runCommand({collStats: 'baz'}), authzErrorCode);
assert.commandFailedWithCode(a.runCommand({collStats: 'foobar'}), authzErrorCode);
assert.commandWorked(b.runCommand({dbStats: 1}));
assert.commandFailedWithCode(b.runCommand({collStats: 'foo'}), authzErrorCode);
assert.commandFailedWithCode(b.runCommand({collStats: 'bar'}), authzErrorCode);
assert.commandFailedWithCode(b.runCommand({collStats: 'baz'}), authzErrorCode);
assert.commandFailedWithCode(b.runCommand({collStats: 'foobar'}), authzErrorCode);
jsTestLog("Doing writes that will eventually be rolled back");
// down A and wait for B to become master
replTest.stop(0);
assert.soon(function() {
try {
return B.isMaster().ismaster;
} catch (e) {
return false;
}
}, "B didn't become master");
printjson(b.adminCommand('replSetGetStatus'));
// Modify the the user and role in a way that will be rolled back.
b.grantPrivilegesToRole(
'myRole',
[{resource: {db: 'test', collection: 'foo'}, actions: ['collStats']}],
{}); // Default write concern will wait for majority, which will time out.
b.createRole({
role: 'temporaryRole',
roles: [],
privileges: [{resource: {db: 'test', collection: 'bar'}, actions: ['collStats']}]
},
{}); // Default write concern will wait for majority, which will time out.
b.grantRolesToUser('spencer',
['temporaryRole'],
{}); // Default write concern will wait for majority, which will time out.
assert.commandWorked(b.runCommand({dbStats: 1}));
assert.commandWorked(b.runCommand({collStats: 'foo'}));
assert.commandWorked(b.runCommand({collStats: 'bar'}));
assert.commandFailedWithCode(b.runCommand({collStats: 'baz'}), authzErrorCode);
assert.commandFailedWithCode(b.runCommand({collStats: 'foobar'}), authzErrorCode);
// down B, bring A back up, then wait for A to become master
// 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.isMaster().ismaster;
} catch (e) {
return false;
}
}, "A didn't become master");
// A should not have the new data as it was down
assert.commandWorked(a.runCommand({dbStats: 1}));
assert.commandFailedWithCode(a.runCommand({collStats: 'foo'}), authzErrorCode);
assert.commandFailedWithCode(a.runCommand({collStats: 'bar'}), authzErrorCode);
assert.commandFailedWithCode(a.runCommand({collStats: 'baz'}), authzErrorCode);
assert.commandFailedWithCode(a.runCommand({collStats: 'foobar'}), authzErrorCode);
jsTestLog("Doing writes that should persist after the rollback");
// Modify the user and role in a way that will persist.
A.auth('userAdmin', 'pwd');
// Default write concern will wait for majority, which would time out
// so we override it with an empty write concern
a.grantPrivilegesToRole(
'myRole', [{resource: {db: 'test', collection: 'baz'}, actions: ['collStats']}], {});
a.createRole({
role: 'persistentRole',
roles: [],
privileges: [{resource: {db: 'test', collection: 'foobar'}, actions: ['collStats']}]
},
{});
a.grantRolesToUser('spencer', ['persistentRole'], {});
A.logout();
a.auth('spencer', 'pwd');
// A has the data we just wrote, but not what B wrote before
checkFinalResults(a);
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);
authutil.asCluster(replTest.nodes, 'jstests/libs/key1', function() {
replTest.awaitReplication();
});
assert.soon(function() {
return b.auth('spencer', 'pwd');
});
// Now both A and B should agree
checkFinalResults(a);
checkFinalResults(b);
// Verify data consistency between nodes.
authutil.asCluster(replTest.nodes, 'jstests/libs/key1', function() {
replTest.checkOplogs();
});
// DB hash check is done in stopSet.
replTest.stopSet();
}());
|