feat: add TOTP multifactor authentication
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
database: {
|
||||
prepare: vi.fn(),
|
||||
exec: vi.fn(),
|
||||
},
|
||||
getDatabase: vi.fn(),
|
||||
getPermissions: vi.fn(),
|
||||
hasRecentAal2: vi.fn(),
|
||||
requirePermission: vi.fn(),
|
||||
getSessionUser: vi.fn(),
|
||||
recordIsacAudit: vi.fn(),
|
||||
countActiveFullAdministrators: vi.fn(),
|
||||
runs: [] as unknown[][],
|
||||
}));
|
||||
|
||||
vi.mock('$lib/server/auth/session', () => ({
|
||||
getSessionUser: mocks.getSessionUser,
|
||||
}));
|
||||
|
||||
vi.mock('$lib/server/auth/authorization', () => ({
|
||||
getPermissions: mocks.getPermissions,
|
||||
hasRecentAal2: mocks.hasRecentAal2,
|
||||
requirePermission: mocks.requirePermission,
|
||||
}));
|
||||
|
||||
vi.mock('$lib/server/auth/bootstrap', () => ({
|
||||
countActiveFullAdministrators: mocks.countActiveFullAdministrators,
|
||||
}));
|
||||
|
||||
vi.mock('$lib/server/database', () => ({
|
||||
getDatabase: mocks.getDatabase,
|
||||
}));
|
||||
|
||||
vi.mock('$lib/server/audit', () => ({
|
||||
recordIsacAudit: mocks.recordIsacAudit,
|
||||
}));
|
||||
|
||||
import { actions } from './+page.server';
|
||||
|
||||
const postRole = (id = '', permissions: string[] = []) => {
|
||||
const form = new FormData();
|
||||
form.set('id', id);
|
||||
form.set('name', 'Field Operators');
|
||||
form.set('description', 'Ordinary access role');
|
||||
for (const permission of permissions)
|
||||
form.append('permissionIds', permission);
|
||||
return new Request('http://localhost/isac/roles?/save', {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
});
|
||||
};
|
||||
|
||||
const postDelete = () => {
|
||||
const form = new FormData();
|
||||
form.set('id', 'role-user-operator');
|
||||
form.set('confirmation', 'Existing');
|
||||
return new Request('http://localhost/isac/roles?/delete', {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
});
|
||||
};
|
||||
|
||||
describe('ISAC role authority actions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.runs.length = 0;
|
||||
mocks.getSessionUser.mockResolvedValue({ id: 'operator-1' });
|
||||
mocks.getPermissions.mockResolvedValue(['users.roles']);
|
||||
mocks.hasRecentAal2.mockReturnValue(true);
|
||||
mocks.requirePermission.mockImplementation(async (user) => user);
|
||||
mocks.getDatabase.mockResolvedValue(mocks.database);
|
||||
mocks.countActiveFullAdministrators.mockResolvedValue(1);
|
||||
mocks.database.prepare.mockImplementation((sql: string) => ({
|
||||
get: vi.fn(async (...args: unknown[]) => {
|
||||
if (sql.includes('SELECT id,name,description FROM roles WHERE id=?'))
|
||||
return args[0]
|
||||
? { id: args[0], name: 'Existing', description: null }
|
||||
: null;
|
||||
if (sql.includes('SELECT id FROM roles WHERE name=')) return null;
|
||||
if (sql.includes('COUNT(DISTINCT users.id)')) return { count: 1 };
|
||||
return null;
|
||||
}),
|
||||
all: vi.fn(async (...args: unknown[]) => {
|
||||
if (sql.includes('SELECT id,name FROM permissions'))
|
||||
return args.map((id) => ({ id, name: 'users.view' }));
|
||||
if (sql.includes('SELECT permissions.name FROM role_permissions'))
|
||||
return [];
|
||||
return [];
|
||||
}),
|
||||
run: vi.fn(async (...args: unknown[]) => {
|
||||
mocks.runs.push(args);
|
||||
return { changes: 1 };
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
it('rolls back role writes when audit recording fails', async () => {
|
||||
mocks.recordIsacAudit.mockRejectedValueOnce(new Error('audit failed'));
|
||||
|
||||
await expect(
|
||||
actions.save?.({
|
||||
cookies: {},
|
||||
request: postRole('', ['permission-users-view']),
|
||||
} as never),
|
||||
).rejects.toThrow('audit failed');
|
||||
|
||||
expect(mocks.database.exec.mock.calls).toEqual([
|
||||
['BEGIN IMMEDIATE'],
|
||||
['ROLLBACK'],
|
||||
]);
|
||||
expect(mocks.database.exec).not.toHaveBeenCalledWith('COMMIT');
|
||||
expect(mocks.recordIsacAudit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ database: mocks.database }),
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a role mutation that would leave no active full administrator', async () => {
|
||||
mocks.countActiveFullAdministrators.mockResolvedValue(0);
|
||||
|
||||
const result = await actions.save?.({
|
||||
cookies: {},
|
||||
request: postRole('', ['permission-users-view']),
|
||||
} as never);
|
||||
|
||||
expect(result).toMatchObject({ status: 409 });
|
||||
expect(mocks.database.exec.mock.calls).toEqual([
|
||||
['BEGIN IMMEDIATE'],
|
||||
['ROLLBACK'],
|
||||
]);
|
||||
expect(mocks.recordIsacAudit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps role deletion and its audit evidence in one transaction', async () => {
|
||||
mocks.getPermissions.mockResolvedValue(['*']);
|
||||
|
||||
const result = await actions.delete?.({
|
||||
cookies: {},
|
||||
request: postDelete(),
|
||||
} as never);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
roleDeleted: true,
|
||||
resultToken: expect.any(String),
|
||||
});
|
||||
expect(mocks.database.exec.mock.calls).toEqual([
|
||||
['BEGIN IMMEDIATE'],
|
||||
['COMMIT'],
|
||||
]);
|
||||
expect(mocks.recordIsacAudit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: 'role.deleted',
|
||||
database: mocks.database,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user