feat(enterprise): add auth TLS and recovery hardening
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartdb from '../ts/index.js';
|
||||
import { MongoClient } from 'mongodb';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
let server: smartdb.SmartdbServer;
|
||||
let authedClient: MongoClient;
|
||||
let openClient: MongoClient;
|
||||
let readerClient: MongoClient;
|
||||
let tmpDir: string;
|
||||
let usersPath: string;
|
||||
|
||||
function makeTmpDir(): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'smartdb-auth-test-'));
|
||||
}
|
||||
|
||||
function cleanTmpDir(dir: string): void {
|
||||
if (fs.existsSync(dir)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
tap.test('auth: should start server with SCRAM-SHA-256 auth enabled', async () => {
|
||||
tmpDir = makeTmpDir();
|
||||
usersPath = path.join(tmpDir, 'users.json');
|
||||
server = new smartdb.SmartdbServer({
|
||||
port: 27118,
|
||||
auth: {
|
||||
enabled: true,
|
||||
usersPath,
|
||||
scramIterations: 4096,
|
||||
users: [
|
||||
{
|
||||
username: 'root',
|
||||
password: 'secret',
|
||||
database: 'admin',
|
||||
roles: ['root'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await server.start();
|
||||
expect(server.running).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('auth: should reject protected commands before authentication', async () => {
|
||||
openClient = new MongoClient('mongodb://127.0.0.1:27118', {
|
||||
directConnection: true,
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
});
|
||||
await openClient.connect();
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
await openClient.db('admin').command({ ping: 1 });
|
||||
} catch (err: any) {
|
||||
threw = true;
|
||||
expect(err.code).toEqual(13);
|
||||
}
|
||||
expect(threw).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('auth: should reject invalid credentials', async () => {
|
||||
const badClient = new MongoClient('mongodb://root:wrong@127.0.0.1:27118/admin?authSource=admin', {
|
||||
directConnection: true,
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
});
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
await badClient.connect();
|
||||
await badClient.db('admin').command({ ping: 1 });
|
||||
} catch {
|
||||
threw = true;
|
||||
} finally {
|
||||
await badClient.close().catch(() => undefined);
|
||||
}
|
||||
expect(threw).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('auth: should authenticate valid credentials', async () => {
|
||||
authedClient = new MongoClient('mongodb://root:secret@127.0.0.1:27118/admin?authSource=admin', {
|
||||
directConnection: true,
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
});
|
||||
await authedClient.connect();
|
||||
const result = await authedClient.db('admin').command({ ping: 1 });
|
||||
expect(result.ok).toEqual(1);
|
||||
});
|
||||
|
||||
tap.test('auth: should allow CRUD after authentication', async () => {
|
||||
const coll = authedClient.db('securedb').collection('notes');
|
||||
const inserted = await coll.insertOne({ title: 'enterprise auth' });
|
||||
expect(inserted.acknowledged).toBeTrue();
|
||||
|
||||
const doc = await coll.findOne({ _id: inserted.insertedId });
|
||||
expect(doc).toBeTruthy();
|
||||
expect(doc!.title).toEqual('enterprise auth');
|
||||
});
|
||||
|
||||
tap.test('auth: root should create a read-only user', async () => {
|
||||
const result = await authedClient.db('admin').command({
|
||||
createUser: 'reader',
|
||||
pwd: 'readpass',
|
||||
roles: [{ role: 'read', db: 'securedb' }],
|
||||
});
|
||||
expect(result.ok).toEqual(1);
|
||||
|
||||
const usersInfo = await authedClient.db('admin').command({ usersInfo: 'reader' });
|
||||
expect(usersInfo.ok).toEqual(1);
|
||||
expect(usersInfo.users.length).toEqual(1);
|
||||
expect(usersInfo.users[0].user).toEqual('reader');
|
||||
});
|
||||
|
||||
tap.test('auth: read-only user should read but not write', async () => {
|
||||
readerClient = new MongoClient('mongodb://reader:readpass@127.0.0.1:27118/admin?authSource=admin', {
|
||||
directConnection: true,
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
});
|
||||
await readerClient.connect();
|
||||
|
||||
const doc = await readerClient.db('securedb').collection('notes').findOne({ title: 'enterprise auth' });
|
||||
expect(doc).toBeTruthy();
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
await readerClient.db('securedb').collection('notes').insertOne({ title: 'denied write' });
|
||||
} catch (err: any) {
|
||||
threw = true;
|
||||
expect(err.code).toEqual(13);
|
||||
}
|
||||
expect(threw).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('auth: persisted users should survive server restart', async () => {
|
||||
await readerClient.close();
|
||||
await authedClient.close();
|
||||
await server.stop();
|
||||
|
||||
// Simulates a crash after writing the temporary auth metadata file but before rename.
|
||||
fs.writeFileSync(path.join(tmpDir, 'users.tmp'), '{ invalid json');
|
||||
|
||||
server = new smartdb.SmartdbServer({
|
||||
port: 27118,
|
||||
auth: {
|
||||
enabled: true,
|
||||
usersPath,
|
||||
users: [],
|
||||
scramIterations: 4096,
|
||||
},
|
||||
});
|
||||
await server.start();
|
||||
|
||||
readerClient = new MongoClient('mongodb://reader:readpass@127.0.0.1:27118/admin?authSource=admin', {
|
||||
directConnection: true,
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
});
|
||||
await readerClient.connect();
|
||||
const result = await readerClient.db('admin').command({ ping: 1 });
|
||||
expect(result.ok).toEqual(1);
|
||||
});
|
||||
|
||||
tap.test('auth: cleanup', async () => {
|
||||
await openClient.close();
|
||||
await readerClient.close();
|
||||
await server.stop();
|
||||
expect(server.running).toBeFalse();
|
||||
cleanTmpDir(tmpDir);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user