Skip to content

Commit b1f4dc0

Browse files
committed
feat: make use of the "localhost exception" for creating users
This should heavily reduce the time needed to create users This also changes when "ephemeralForTest" is used to be able to use auth properly fixes #670 fixes #671
1 parent 770791b commit b1f4dc0

File tree

5 files changed

+440
-208
lines changed

5 files changed

+440
-208
lines changed

packages/mongodb-memory-server-core/src/MongoMemoryReplSet.ts

Lines changed: 48 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export interface ReplSetOpts {
4848
* enable auth ("--auth" / "--noauth")
4949
* @default false
5050
*/
51-
auth?: boolean | AutomaticAuth;
51+
auth?: boolean | AutomaticAuth; // TODO: remove "boolean" option next major version
5252
/**
5353
* additional command line arguments passed to `mongod`
5454
* @default []
@@ -255,29 +255,58 @@ export class MongoMemoryReplSet extends EventEmitter implements ManagerAdvanced
255255

256256
assertion(this._replSetOpts.count > 0, new ReplsetCountLowError(this._replSetOpts.count));
257257

258-
if (typeof this._replSetOpts.auth === 'object') {
258+
// setting this for sanity
259+
if (typeof this._replSetOpts.auth === 'boolean') {
260+
this._replSetOpts.auth = { disable: !this._replSetOpts.auth };
261+
}
262+
263+
// do not set default when "disable" is "true" to save execution and memory
264+
if (!this._replSetOpts.auth.disable) {
259265
this._replSetOpts.auth = authDefault(this._replSetOpts.auth);
260266
}
261267
}
262268

269+
/**
270+
* Helper function to determine if "auth" should be enabled
271+
* This function expectes to be run after the auth object has been transformed to a object
272+
* @returns "true" when "auth" should be enabled
273+
*/
274+
protected enableAuth(): boolean {
275+
if (isNullOrUndefined(this._replSetOpts.auth)) {
276+
return false;
277+
}
278+
279+
assertion(typeof this._replSetOpts.auth === 'object', new AuthNotObjectError());
280+
281+
return typeof this._replSetOpts.auth.disable === 'boolean' // if "this._replSetOpts.auth.disable" is defined, use that
282+
? !this._replSetOpts.auth.disable // invert the disable boolean, because "auth" should only be disabled if "disabled = true"
283+
: true; // if "this._replSetOpts.auth.disable" is not defined, default to true because "this._replSetOpts.auth" is defined
284+
}
285+
263286
/**
264287
* Returns instance options suitable for a MongoMemoryServer.
265288
* @param baseOpts Options to merge with
289+
* @param keyfileLocation The Keyfile location if "auth" is used
266290
*/
267-
protected getInstanceOpts(baseOpts: MongoMemoryInstanceOptsBase = {}): MongoMemoryInstanceOpts {
291+
protected getInstanceOpts(
292+
baseOpts: MongoMemoryInstanceOptsBase = {},
293+
keyfileLocation?: string
294+
): MongoMemoryInstanceOpts {
295+
const enableAuth: boolean = this.enableAuth();
296+
268297
const opts: MongoMemoryInstanceOpts = {
269-
// disable "auth" if replsetopts has an object-auth
270-
auth:
271-
typeof this._replSetOpts.auth === 'object' && !this._ranCreateAuth
272-
? false
273-
: !!this._replSetOpts.auth,
298+
auth: enableAuth,
274299
args: this._replSetOpts.args,
275300
dbName: this._replSetOpts.dbName,
276301
ip: this._replSetOpts.ip,
277302
replSet: this._replSetOpts.name,
278303
storageEngine: this._replSetOpts.storageEngine,
279304
};
280305

306+
if (!isNullOrUndefined(keyfileLocation)) {
307+
opts.keyfileLocation = keyfileLocation;
308+
}
309+
281310
if (baseOpts.args) {
282311
opts.args = this._replSetOpts.args.concat(baseOpts.args);
283312
}
@@ -410,6 +439,12 @@ export class MongoMemoryReplSet extends EventEmitter implements ManagerAdvanced
410439
return;
411440
}
412441

442+
let keyfilePath: string | undefined = undefined;
443+
444+
if (this.enableAuth()) {
445+
keyfilePath = resolve((await this.ensureKeyFile()).name, 'keyfile');
446+
}
447+
413448
// Any servers defined within `_instanceOpts` should be started first as
414449
// the user could have specified a `dbPath` in which case we would want to perform
415450
// the `replSetInitiate` command against that server.
@@ -420,15 +455,15 @@ export class MongoMemoryReplSet extends EventEmitter implements ManagerAdvanced
420455
}" from instanceOpts (count: ${this.servers.length + 1}):`,
421456
opts
422457
);
423-
this.servers.push(this._initServer(this.getInstanceOpts(opts)));
458+
this.servers.push(this._initServer(this.getInstanceOpts(opts, keyfilePath)));
424459
});
425460
while (this.servers.length < this._replSetOpts.count) {
426461
log(
427462
`initAllServers: starting extra server "${this.servers.length + 1}" of "${
428463
this._replSetOpts.count
429464
}" (count: ${this.servers.length + 1})`
430465
);
431-
this.servers.push(this._initServer(this.getInstanceOpts()));
466+
this.servers.push(this._initServer(this.getInstanceOpts(undefined, keyfilePath)));
432467
}
433468

434469
log('initAllServers: waiting for all servers to finish starting');
@@ -638,15 +673,15 @@ export class MongoMemoryReplSet extends EventEmitter implements ManagerAdvanced
638673
const uris = this.servers.map((server) => server.getUri());
639674
const isInMemory = this.servers[0].instanceInfo?.storageEngine === 'ephemeralForTest';
640675

641-
let con: MongoClient = await MongoClient.connect(uris[0], {
676+
const con: MongoClient = await MongoClient.connect(uris[0], {
642677
// somehow since mongodb-nodejs 4.0, this option is needed when the server is set to be in a replset
643678
directConnection: true,
644679
});
645680
log('_initReplSet: connected');
646681

647682
// try-finally to close connection in any case
648683
try {
649-
let adminDb = con.db('admin');
684+
const adminDb = con.db('admin');
650685

651686
const members = uris.map((uri, index) => ({
652687
_id: index,
@@ -682,34 +717,9 @@ export class MongoMemoryReplSet extends EventEmitter implements ManagerAdvanced
682717
new InstanceInfoError('_initReplSet authIsObject primary')
683718
);
684719

720+
await con.close(); // just ensuring that no timeouts happen or conflicts happen
685721
await primary.createAuth(primary.instanceInfo);
686722
this._ranCreateAuth = true;
687-
688-
// TODO: maybe change the static "isInMemory" to be for each server individually, based on "storageEngine", not just the first one
689-
if (!isInMemory) {
690-
log('_initReplSet: closing connection for restart');
691-
await con.close(); // close connection in preparation for "stop"
692-
await this.stop({ doCleanup: false, force: false }); // stop all servers for enabling auth
693-
log('_initReplSet: starting all server again with auth');
694-
await this.initAllServers(); // start all servers again with "auth" enabled
695-
await this._waitForPrimary(); // wait for a primary to come around, because otherwise server selection may time out, this may take more than 30s
696-
697-
con = await MongoClient.connect(this.getUri(), {
698-
authSource: 'admin',
699-
authMechanism: 'SCRAM-SHA-256',
700-
auth: {
701-
username: this._replSetOpts.auth.customRootName as string, // cast because these are existing
702-
password: this._replSetOpts.auth.customRootPwd as string,
703-
},
704-
});
705-
adminDb = con.db('admin');
706-
log('_initReplSet: auth restart finished');
707-
} else {
708-
console.warn(
709-
'Not Restarting ReplSet for Auth\n' +
710-
'Storage engine of current PRIMARY is ephemeralForTest, which does not write data on shutdown, and mongodb does not allow changing "auth" runtime'
711-
);
712-
}
713723
}
714724
} catch (e) {
715725
if (e instanceof MongoError && e.errmsg == 'already initialized') {

packages/mongodb-memory-server-core/src/MongoMemoryServer.ts

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export interface StartupInstanceData {
8282
storageEngine: NonNullable<MongoMemoryInstanceOpts['storageEngine']>;
8383
replSet?: NonNullable<MongoMemoryInstanceOpts['replSet']>;
8484
tmpDir?: tmp.DirResult;
85+
keyfileLocation?: NonNullable<MongoMemoryInstanceOpts['keyfileLocation']>;
8586
}
8687

8788
/**
@@ -193,6 +194,7 @@ export interface CreateUser extends CreateUserMongoDB {
193194
}
194195

195196
export interface MongoMemoryServerGetStartOptions {
197+
/** Defines wheter should {@link MongoMemoryServer.createAuth} be run */
196198
createAuth: boolean;
197199
data: StartupInstanceData;
198200
mongodOptions: Partial<MongodOpts>;
@@ -361,6 +363,7 @@ export class MongoMemoryServer extends EventEmitter implements ManagerAdvanced {
361363
replSet: instOpts.replSet,
362364
dbPath: instOpts.dbPath,
363365
tmpDir: undefined,
366+
keyfileLocation: instOpts.keyfileLocation,
364367
};
365368

366369
if (isNullOrUndefined(this._instanceInfo)) {
@@ -386,12 +389,16 @@ export class MongoMemoryServer extends EventEmitter implements ManagerAdvanced {
386389
isNew = false;
387390
}
388391

389-
const createAuth: boolean =
392+
const enableAuth: boolean =
390393
(typeof instOpts.auth === 'boolean' ? instOpts.auth : true) && // check if auth is even meant to be enabled
391394
!isNullOrUndefined(this.auth) && // check if "this.auth" is defined
392-
!this.auth.disable && // check that "this.auth.disable" is falsey
395+
!this.auth.disable; // check that "this.auth.disable" is falsey
396+
397+
const createAuth: boolean =
398+
enableAuth && // re-use all the checks from "enableAuth"
399+
!isNullOrUndefined(this.auth) && // needs to be re-checked because typescript complains
393400
(this.auth.force || isNew) && // check that either "isNew" or "this.auth.force" is "true"
394-
!instOpts.replSet; // dont run "createAuth" when its an replset
401+
!instOpts.replSet; // dont run "createAuth" when its an replset, it will be run by the replset controller
395402

396403
return {
397404
data: data,
@@ -400,7 +407,7 @@ export class MongoMemoryServer extends EventEmitter implements ManagerAdvanced {
400407
instance: {
401408
...data,
402409
args: instOpts.args,
403-
auth: createAuth ? false : instOpts.auth, // disable "auth" for "createAuth"
410+
auth: enableAuth,
404411
},
405412
binary: this.opts.binary,
406413
spawn: this.opts.spawn,
@@ -436,24 +443,28 @@ export class MongoMemoryServer extends EventEmitter implements ManagerAdvanced {
436443
const instance = await MongoInstance.create(mongodOptions);
437444
this.debug(`_startUpInstance: Instance Started, createAuth: "${createAuth}"`);
438445

446+
this._instanceInfo = {
447+
...data,
448+
dbPath: data.dbPath as string, // because otherwise the types would be incompatible
449+
instance,
450+
};
451+
452+
// always set the "extraConnectionOptions" when "auth" is enabled, regardless of if "createAuth" gets run
453+
if (!isNullOrUndefined(this.auth) && !isNullOrUndefined(mongodOptions.instance?.auth)) {
454+
instance.extraConnectionOptions = {
455+
authSource: 'admin',
456+
authMechanism: 'SCRAM-SHA-256',
457+
auth: {
458+
username: this.auth.customRootName,
459+
password: this.auth.customRootPwd,
460+
},
461+
};
462+
}
463+
439464
// "isNullOrUndefined" because otherwise typescript complains about "this.auth" possibly being not defined
440465
if (!isNullOrUndefined(this.auth) && createAuth) {
441466
this.debug(`_startUpInstance: Running "createAuth" (force: "${this.auth.force}")`);
442467
await this.createAuth(data);
443-
444-
if (data.storageEngine !== 'ephemeralForTest') {
445-
this.debug('_startUpInstance: Killing No-Auth instance');
446-
await instance.stop();
447-
448-
this.debug('_startUpInstance: Starting Auth Instance');
449-
instance.instanceOpts.auth = true;
450-
await instance.start();
451-
} else {
452-
console.warn(
453-
'Not Restarting MongoInstance for Auth\n' +
454-
'Storage engine is "ephemeralForTest", which does not write data on shutdown, and mongodb does not allow changing "auth" runtime'
455-
);
456-
}
457468
} else {
458469
// extra "if" to log when "disable" is set to "true"
459470
if (this.opts.auth?.disable) {
@@ -462,12 +473,6 @@ export class MongoMemoryServer extends EventEmitter implements ManagerAdvanced {
462473
);
463474
}
464475
}
465-
466-
this._instanceInfo = {
467-
...data,
468-
dbPath: data.dbPath as string, // because otherwise the types would be incompatible
469-
instance,
470-
};
471476
}
472477

473478
/**
@@ -715,7 +720,7 @@ export class MongoMemoryServer extends EventEmitter implements ManagerAdvanced {
715720
}
716721

717722
/**
718-
* Create Users and restart instance to enable auth
723+
* Create the Root user and additional users using the [localhost exception](https://www.mongodb.com/docs/manual/core/localhost-exception/#std-label-localhost-exception)
719724
* This Function assumes "this.opts.auth" is already processed into "this.auth"
720725
* @param data Used to get "ip" and "port"
721726
* @internal
@@ -725,11 +730,9 @@ export class MongoMemoryServer extends EventEmitter implements ManagerAdvanced {
725730
!isNullOrUndefined(this.auth),
726731
new Error('"createAuth" got called, but "this.auth" is undefined!')
727732
);
733+
assertionInstanceInfo(this._instanceInfo);
728734
this.debug('createAuth: options:', this.auth);
729-
const con: MongoClient = await MongoClient.connect(
730-
uriTemplate(data.ip, data.port, 'admin'),
731-
{}
732-
);
735+
let con: MongoClient = await MongoClient.connect(uriTemplate(data.ip, data.port, 'admin'));
733736

734737
try {
735738
let db = con.db('admin'); // just to ensure it is actually the "admin" database AND to have the "Db" data
@@ -761,6 +764,14 @@ export class MongoMemoryServer extends EventEmitter implements ManagerAdvanced {
761764
return a.database === b.database ? 0 : 1; // "0" to sort all databases that are the same after each other, and "1" to for pushing it back
762765
});
763766

767+
// reconnecting the database because the root user now exists and the "localhost exception" only allows the first user
768+
await con.close();
769+
con = await MongoClient.connect(
770+
this.getUri('admin'),
771+
this._instanceInfo.instance.extraConnectionOptions ?? {}
772+
);
773+
db = con.db('admin');
774+
764775
for (const user of this.auth.extraUsers) {
765776
user.database = isNullOrUndefined(user.database) ? 'admin' : user.database;
766777

@@ -782,6 +793,10 @@ export class MongoMemoryServer extends EventEmitter implements ManagerAdvanced {
782793
authenticationRestrictions: user.authenticationRestrictions ?? [],
783794
mechanisms: user.mechanisms ?? ['SCRAM-SHA-256'],
784795
digestPassword: user.digestPassword ?? true,
796+
// "writeConcern" is needced, otherwise replset servers might fail with "auth failed: such user does not exist"
797+
writeConcern: {
798+
w: 'majority',
799+
},
785800
} as CreateUserMongoDB);
786801
}
787802
}

0 commit comments

Comments
 (0)