fix(security): M2-008 Valkey key audit — SCAN over KEYS, restrict /gc to admin (#298)
Some checks failed
ci/woodpecker/push/ci Pipeline failed
Some checks failed
ci/woodpecker/push/ci Pipeline failed
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #298.
This commit is contained in:
@@ -5,7 +5,7 @@ import type { LogService } from '@mosaic/log';
|
||||
import { SessionGCService } from './session-gc.service.js';
|
||||
|
||||
type MockRedis = {
|
||||
keys: ReturnType<typeof vi.fn>;
|
||||
scan: ReturnType<typeof vi.fn>;
|
||||
del: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
@@ -14,9 +14,17 @@ describe('SessionGCService', () => {
|
||||
let mockRedis: MockRedis;
|
||||
let mockLogService: { logs: { promoteToWarm: ReturnType<typeof vi.fn> } };
|
||||
|
||||
/**
|
||||
* Helper: build a scan mock that returns all provided keys in a single
|
||||
* cursor iteration (cursor '0' in → ['0', keys] out).
|
||||
*/
|
||||
function makeScanMock(keys: string[]): ReturnType<typeof vi.fn> {
|
||||
return vi.fn().mockResolvedValue(['0', keys]);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockRedis = {
|
||||
keys: vi.fn().mockResolvedValue([]),
|
||||
scan: makeScanMock([]),
|
||||
del: vi.fn().mockResolvedValue(0),
|
||||
};
|
||||
|
||||
@@ -36,7 +44,7 @@ describe('SessionGCService', () => {
|
||||
});
|
||||
|
||||
it('collect() deletes Valkey keys for session', async () => {
|
||||
mockRedis.keys.mockResolvedValue(['mosaic:session:abc:system', 'mosaic:session:abc:foo']);
|
||||
mockRedis.scan = makeScanMock(['mosaic:session:abc:system', 'mosaic:session:abc:foo']);
|
||||
const result = await service.collect('abc');
|
||||
expect(mockRedis.del).toHaveBeenCalledWith(
|
||||
'mosaic:session:abc:system',
|
||||
@@ -46,7 +54,7 @@ describe('SessionGCService', () => {
|
||||
});
|
||||
|
||||
it('collect() with no keys returns empty cleaned valkeyKeys', async () => {
|
||||
mockRedis.keys.mockResolvedValue([]);
|
||||
mockRedis.scan = makeScanMock([]);
|
||||
const result = await service.collect('abc');
|
||||
expect(result.cleaned.valkeyKeys).toBeUndefined();
|
||||
});
|
||||
@@ -57,14 +65,14 @@ describe('SessionGCService', () => {
|
||||
});
|
||||
|
||||
it('fullCollect() deletes all session keys', async () => {
|
||||
mockRedis.keys.mockResolvedValue(['mosaic:session:abc:system', 'mosaic:session:xyz:foo']);
|
||||
mockRedis.scan = makeScanMock(['mosaic:session:abc:system', 'mosaic:session:xyz:foo']);
|
||||
const result = await service.fullCollect();
|
||||
expect(mockRedis.del).toHaveBeenCalled();
|
||||
expect(result.valkeyKeys).toBe(2);
|
||||
});
|
||||
|
||||
it('fullCollect() with no keys returns 0 valkeyKeys', async () => {
|
||||
mockRedis.keys.mockResolvedValue([]);
|
||||
mockRedis.scan = makeScanMock([]);
|
||||
const result = await service.fullCollect();
|
||||
expect(result.valkeyKeys).toBe(0);
|
||||
expect(mockRedis.del).not.toHaveBeenCalled();
|
||||
@@ -76,11 +84,18 @@ describe('SessionGCService', () => {
|
||||
});
|
||||
|
||||
it('sweepOrphans() extracts unique session IDs and collects them', async () => {
|
||||
mockRedis.keys.mockResolvedValue([
|
||||
'mosaic:session:abc:system',
|
||||
'mosaic:session:abc:messages',
|
||||
'mosaic:session:xyz:system',
|
||||
]);
|
||||
// First scan call returns the global session list; subsequent calls return
|
||||
// per-session keys during collect().
|
||||
mockRedis.scan = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce([
|
||||
'0',
|
||||
['mosaic:session:abc:system', 'mosaic:session:abc:messages', 'mosaic:session:xyz:system'],
|
||||
])
|
||||
// collect('abc') scan
|
||||
.mockResolvedValueOnce(['0', ['mosaic:session:abc:system', 'mosaic:session:abc:messages']])
|
||||
// collect('xyz') scan
|
||||
.mockResolvedValueOnce(['0', ['mosaic:session:xyz:system']]);
|
||||
mockRedis.del.mockResolvedValue(1);
|
||||
|
||||
const result = await service.sweepOrphans();
|
||||
@@ -89,7 +104,7 @@ describe('SessionGCService', () => {
|
||||
});
|
||||
|
||||
it('sweepOrphans() returns empty when no session keys', async () => {
|
||||
mockRedis.keys.mockResolvedValue([]);
|
||||
mockRedis.scan = makeScanMock([]);
|
||||
const result = await service.sweepOrphans();
|
||||
expect(result.orphanedSessions).toBe(0);
|
||||
expect(result.totalCleaned).toHaveLength(0);
|
||||
|
||||
Reference in New Issue
Block a user