mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-10-26 22:37:07 +00:00
db07c5c5af
Server-side session policy replaces long-lived idle console logins: sliding idle timeout, absolute max age, atomic replacement of prior sessions, IP/UA audit metadata, and consistent expired-login redirects. Thanks: INSOLVE (Honorary); Marco Jakobs (@jacotec); MyNameisStitch (@MyNameisStitch); Redspin (@playerumpknow); muopc (@muopc)
146 lines
5.2 KiB
JavaScript
146 lines
5.2 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
const { DatabaseSessionStore } = require('../services/databaseSessionStore');
|
|
const { END_REASONS } = require('../lib/sessionPolicy');
|
|
|
|
function invoke(method, ...args) {
|
|
return new Promise((resolve, reject) => {
|
|
method(...args, (err, value) => (err ? reject(err) : resolve(value)));
|
|
});
|
|
}
|
|
|
|
describe('DatabaseSessionStore', () => {
|
|
let tempDir;
|
|
let store;
|
|
let previousDbType;
|
|
let previousDatabaseUrl;
|
|
|
|
beforeEach(async () => {
|
|
previousDbType = process.env.DB_TYPE;
|
|
previousDatabaseUrl = process.env.DATABASE_URL;
|
|
process.env.DB_TYPE = 'sqlite';
|
|
delete process.env.DATABASE_URL;
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'betterdesk-session-store-'));
|
|
store = new DatabaseSessionStore({
|
|
config: { dbPath: path.join(tempDir, 'db_v2.sqlite3') },
|
|
ttlMs: 24 * 60 * 60 * 1000,
|
|
idleTimeoutMs: 60_000,
|
|
maxAgeMs: 24 * 60 * 60 * 1000
|
|
});
|
|
await store.ready;
|
|
});
|
|
|
|
afterEach(async () => {
|
|
if (store) await store.close();
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
store = null;
|
|
if (previousDbType === undefined) delete process.env.DB_TYPE;
|
|
else process.env.DB_TYPE = previousDbType;
|
|
if (previousDatabaseUrl === undefined) delete process.env.DATABASE_URL;
|
|
else process.env.DATABASE_URL = previousDatabaseUrl;
|
|
});
|
|
|
|
it('persists, expires and destroys console sessions in SQLite', async () => {
|
|
const sid = 'session-id';
|
|
const session = {
|
|
user: { id: 7, username: 'alice', role: 'operator' },
|
|
userId: 7,
|
|
createdAt: Date.now(),
|
|
lastActivityAt: Date.now(),
|
|
cookie: { expires: new Date(Date.now() + 60_000).toISOString() }
|
|
};
|
|
|
|
await invoke(store.set.bind(store), sid, session);
|
|
expect(await invoke(store.get.bind(store), sid)).toEqual(session);
|
|
|
|
await invoke(store.touch.bind(store), sid, {
|
|
...session,
|
|
lastActivityAt: Date.now(),
|
|
cookie: { expires: new Date(Date.now() + 120_000).toISOString() }
|
|
});
|
|
expect(await invoke(store.get.bind(store), sid)).toEqual(session);
|
|
|
|
await invoke(store.destroy.bind(store), sid);
|
|
expect(await invoke(store.get.bind(store), sid)).toBeNull();
|
|
});
|
|
|
|
it('enforces one active session per account and keeps history', async () => {
|
|
const now = Date.now();
|
|
const base = {
|
|
userId: 42,
|
|
user: { id: 42, username: 'bob', role: 'admin' },
|
|
createdAt: now,
|
|
lastActivityAt: now,
|
|
loginIp: '203.0.113.10',
|
|
loginUserAgent: 'TestAgent/1.0',
|
|
cookie: { expires: new Date(now + 30_000).toISOString() }
|
|
};
|
|
|
|
await invoke(store.set.bind(store), 'sid-a', base);
|
|
await store.activateExclusiveSession('sid-a', {
|
|
userId: 42,
|
|
username: 'bob',
|
|
ip: '203.0.113.10',
|
|
userAgent: 'TestAgent/1.0',
|
|
createdAt: now,
|
|
lastActivityAt: now
|
|
});
|
|
|
|
await invoke(store.set.bind(store), 'sid-b', {
|
|
...base,
|
|
loginIp: '198.51.100.20',
|
|
loginUserAgent: 'OtherAgent/2.0'
|
|
});
|
|
const result = await store.activateExclusiveSession('sid-b', {
|
|
userId: 42,
|
|
username: 'bob',
|
|
ip: '198.51.100.20',
|
|
userAgent: 'OtherAgent/2.0',
|
|
createdAt: now,
|
|
lastActivityAt: now
|
|
});
|
|
|
|
expect(result.replaced).toHaveLength(1);
|
|
expect(result.replaced[0].sid).toBe('sid-a');
|
|
expect(await invoke(store.get.bind(store), 'sid-a')).toBeNull();
|
|
expect(await invoke(store.get.bind(store), 'sid-b')).toBeTruthy();
|
|
|
|
const history = await store.listUserSessions(42, { includeEnded: true });
|
|
expect(history.length).toBeGreaterThanOrEqual(2);
|
|
const ended = history.find((r) => r.sid === 'sid-a');
|
|
expect(ended.end_reason).toBe(END_REASONS.REPLACED);
|
|
expect(Number(ended.is_active)).toBe(0);
|
|
});
|
|
|
|
it('rejects idle sessions on get()', async () => {
|
|
const sid = 'idle-sid';
|
|
const created = Date.now() - 10 * 60 * 1000;
|
|
const session = {
|
|
userId: 9,
|
|
user: { id: 9, username: 'idle', role: 'viewer' },
|
|
createdAt: created,
|
|
lastActivityAt: created,
|
|
cookie: { expires: new Date(Date.now() + 60_000).toISOString() }
|
|
};
|
|
await invoke(store.set.bind(store), sid, session);
|
|
await store.activateExclusiveSession(sid, {
|
|
userId: 9,
|
|
username: 'idle',
|
|
createdAt: created,
|
|
lastActivityAt: created
|
|
});
|
|
// Force last activity beyond idleTimeoutMs (60s floor)
|
|
store.sqlite.prepare(
|
|
'UPDATE panel_sessions SET last_activity_at = ?, created_at = ? WHERE sid = ?'
|
|
).run(Date.now() - 5 * 60 * 1000, created, sid);
|
|
|
|
expect(await invoke(store.get.bind(store), sid)).toBeNull();
|
|
const row = await store.getSessionRecord(sid);
|
|
expect(row.end_reason).toBe(END_REASONS.IDLE);
|
|
});
|
|
});
|