Extract API, migrations and portable NAS deployment
This commit is contained in:
commit
673255fa4f
57 files changed
+3799
No files matched your search
@@ -0,0 +1,65 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { Client } from 'ldapts';
|
||||
import { createServer } from 'node:net';
|
||||
import { defaultDirectorySettings, directoryUsername, lookupDirectoryName, validateDirectorySettings, type DirectoryConfig } from '../../src/directory';
|
||||
|
||||
const config: DirectoryConfig = { id: 'test-config', issuer: 'https://sso.example.invalid', settings: { ...defaultDirectorySettings(), enabled: true, url: 'ldaps://nas.example.invalid:636', baseDn: 'DC=example,DC=invalid', bindDn: 'reader@example.invalid', bindPassword: 'test-only-secret' } };
|
||||
const identity = { issuer: config.issuer, subject: 'REZNIK\\tom.ruff' };
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
function mockDirectory(entries: Record<string, string>[], references: string[] = []) {
|
||||
const bind = vi.spyOn(Client.prototype, 'bind').mockResolvedValue();
|
||||
const search = vi.spyOn(Client.prototype, 'search').mockResolvedValue({ searchEntries: entries.map(entry => ({ dn: 'CN=test', ...entry })), searchReferences: references });
|
||||
const unbind = vi.spyOn(Client.prototype, 'unbind').mockResolvedValue();
|
||||
return { bind, search, unbind };
|
||||
}
|
||||
describe('AD display-name enrichment', () => {
|
||||
it('requires the exact configured issuer and domain-qualified subject before connecting', async () => {
|
||||
const ldap = mockDirectory([]);
|
||||
expect(directoryUsername(config, { ...identity, subject: 'reznik\\tom.ruff' })).toBe('tom.ruff');
|
||||
for (const subject of ['tom.ruff', 'RUFF\\tom.ruff', 'REZNIK\\', 'REZNIK\\tom\\other']) expect(await lookupDirectoryName(config, { ...identity, subject })).toEqual({ code: 'IDENTITY_MISMATCH' });
|
||||
expect(await lookupDirectoryName(config, { ...identity, issuer: 'https://other.invalid' })).toEqual({ code: 'IDENTITY_MISMATCH' });
|
||||
expect(await lookupDirectoryName({ ...config, settings: defaultDirectorySettings() }, identity)).toEqual({ code: 'DISABLED' });
|
||||
expect(ldap.bind).not.toHaveBeenCalled();
|
||||
});
|
||||
it('rejects plaintext, embedded credentials, URL paths and missing bind credentials', () => {
|
||||
for (const url of ['ldap://nas:389', 'https://nas', 'ldaps://user:password@nas', 'ldaps://nas/directory', 'ldaps://nas?query=1']) expect(() => validateDirectorySettings({ ...config.settings, url })).toThrow();
|
||||
expect(() => validateDirectorySettings({ ...config.settings, bindPassword: '' })).toThrow();
|
||||
expect(() => validateDirectorySettings(config.settings)).not.toThrow();
|
||||
});
|
||||
it('reads Hebrew displayName and encodes special characters as a literal equality value', async () => {
|
||||
const username = 'tom*)(sAMAccountName=*)';
|
||||
const ldap = mockDirectory([{ sAMAccountName: username, displayName: 'תום רוף' }]);
|
||||
expect(await lookupDirectoryName(config, { ...identity, subject: `REZNIK\\${username}` })).toEqual({ code: 'FOUND', displayName: 'תום רוף' });
|
||||
const options = ldap.search.mock.calls[0]![1]!;
|
||||
expect(String(options.filter)).toContain('tom\\2a\\29\\28sAMAccountName=\\2a\\29');
|
||||
expect(options).toMatchObject({ scope: 'sub', sizeLimit: 2, timeLimit: 2 });
|
||||
expect(ldap.unbind).toHaveBeenCalledOnce();
|
||||
});
|
||||
it('joins first/surname and refuses ambiguous or mismatched results', async () => {
|
||||
const ldap = mockDirectory([{ samaccountname: 'TOM.RUFF', givenName: 'תום', sn: 'רוף' }]);
|
||||
expect(await lookupDirectoryName(config, identity)).toEqual({ code: 'FOUND', displayName: 'תום רוף' });
|
||||
ldap.search.mockResolvedValue({ searchEntries: [{ dn: 'a' }, { dn: 'b' }], searchReferences: [] });
|
||||
expect(await lookupDirectoryName(config, identity)).toEqual({ code: 'AMBIGUOUS' });
|
||||
ldap.search.mockResolvedValue({ searchEntries: [{ dn: 'a', sAMAccountName: 'other', displayName: 'Someone else' }], searchReferences: [] });
|
||||
expect(await lookupDirectoryName(config, identity)).toEqual({ code: 'NOT_FOUND' });
|
||||
ldap.search.mockResolvedValue({ searchEntries: [], searchReferences: ['ldaps://other.invalid'] });
|
||||
expect(await lookupDirectoryName(config, identity)).toEqual({ code: 'AMBIGUOUS' });
|
||||
});
|
||||
it('sanitizes failures and always releases the connection', async () => {
|
||||
const ldap = mockDirectory([]);
|
||||
ldap.bind.mockRejectedValue(new Error('test-only-secret: bind rejected'));
|
||||
expect(await lookupDirectoryName(config, identity)).toEqual({ code: 'UNAVAILABLE' });
|
||||
expect(ldap.unbind).toHaveBeenCalledOnce();
|
||||
});
|
||||
it('times out when a reachable host never completes TLS', async () => {
|
||||
const sockets = new Set<import('node:net').Socket>();
|
||||
const server = createServer(socket => { sockets.add(socket); socket.on('close', () => sockets.delete(socket)); });
|
||||
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||
try {
|
||||
const port = (server.address() as import('node:net').AddressInfo).port;
|
||||
const started = Date.now();
|
||||
expect(await lookupDirectoryName({ ...config, settings: { ...config.settings, url: `ldaps://127.0.0.1:${port}` } }, identity)).toEqual({ code: 'UNAVAILABLE' });
|
||||
expect(Date.now() - started).toBeLessThan(5000);
|
||||
} finally { for (const socket of sockets) socket.destroy(); await new Promise<void>(resolve => server.close(() => resolve())); }
|
||||
}, 7000);
|
||||
});
|
||||
Reference in new issue
Block a user