179 lines
5.2 KiB
TypeScript
179 lines
5.2 KiB
TypeScript
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);
|
|
|
|
const status = await authedClient.db('admin').command({ connectionStatus: 1 });
|
|
expect(status.ok).toEqual(1);
|
|
expect(status.authInfo.authenticatedUsers[0]).toEqual({ user: 'root', db: 'admin' });
|
|
expect(status.authInfo.authenticatedUserRoles[0]).toEqual({ role: 'root', db: 'admin' });
|
|
});
|
|
|
|
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();
|