153 lines
6.6 KiB
JavaScript
153 lines
6.6 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
require('../src/common');
|
|
|
|
const pages = require('../src/web/pages');
|
|
const registerViewAsRoutes = require('../src/web/routes/settings/view-as');
|
|
const renderUsersPage = require('../src/web/routes/settings/users/list');
|
|
const renderUsersEditPage = require('../src/web/routes/settings/users/edit');
|
|
const renderRssFeedEditPage = require('../src/web/routes/data-sources/rss-feeds/edit');
|
|
const renderWeatherLocationEditPage = require('../src/web/routes/data-sources/weather/edit');
|
|
const { hasAssignedPermission, hasPermission, PERMISSIONS } = require('../src/rbac');
|
|
|
|
test('preview permission is Impersonate > Allow and read-only execution stays denied', () => {
|
|
const permission = PERMISSIONS.find(function (item) { return item.key === 'impersonate.allow'; });
|
|
assert.ok(permission);
|
|
assert.equal(permission.resourceName, 'Impersonate');
|
|
assert.equal(permission.actionName, 'Allow');
|
|
assert.ok(!PERMISSIONS.some(function (item) {
|
|
return item.key === 'users.allow' || item.key === 'users.impersonate' || item.key === 'dashboard.allow';
|
|
}));
|
|
|
|
const previewUser = { isViewingAs: true, permissionKeys: ['clients.allow'] };
|
|
assert.equal(hasAssignedPermission(previewUser, 'clients.allow'), true);
|
|
assert.equal(hasPermission(previewUser, 'clients.allow'), false);
|
|
assert.equal(hasPermission({ permissionKeys: ['clients.allow'] }, 'clients.allow'), true);
|
|
});
|
|
|
|
test('View as is on the user edit footer, not the Users table', () => {
|
|
const user = { id: 19, name: 'Screen operator', username: 'operator', roleNames: 'Operators' };
|
|
const administrator = { id: 7, permissionKeys: ['users.read', 'impersonate.allow'] };
|
|
const adminPage = renderUsersPage({ users: [user] }, '', administrator);
|
|
const editPage = renderUsersEditPage(user, '', administrator, [], []);
|
|
const previewPage = renderUsersPage({ users: [user] }, '', Object.assign({}, administrator, { isViewingAs: true }));
|
|
const previewEditPage = renderUsersEditPage(user, '', Object.assign({}, administrator, { isViewingAs: true }), [], []);
|
|
|
|
assert.doesNotMatch(adminPage, /view-as/);
|
|
assert.match(editPage, /form="view-as-user-form"/);
|
|
assert.match(editPage, /<form id="view-as-user-form" method="post" action="\/settings\/users\/19\/view-as"/);
|
|
assert.ok(editPage.indexOf('View as') < editPage.indexOf('Profile actions'));
|
|
assert.doesNotMatch(previewPage, /action="\/settings\/users\/19\/view-as"/);
|
|
assert.doesNotMatch(previewEditPage, /view-as-user-form/);
|
|
});
|
|
|
|
test('view-as banner identifies both users and keeps an exit control on regular and error pages', () => {
|
|
const previewUser = {
|
|
id: 19,
|
|
name: 'Screen operator',
|
|
username: 'operator',
|
|
isViewingAs: true,
|
|
authenticatedUser: { id: 7, name: 'Administrator', username: 'admin' }
|
|
};
|
|
|
|
const regularPage = pages.renderAboutPage(previewUser);
|
|
const errorPage = pages.renderErrorPage({ statusCode: 403 }, previewUser);
|
|
|
|
[regularPage, errorPage].forEach(function (html) {
|
|
assert.match(html, /Read-only preview/);
|
|
assert.match(html, /Screen operator/);
|
|
assert.match(html, /signed in as Administrator/);
|
|
assert.match(html, /action="\/settings\/view-as\/stop"/);
|
|
assert.match(html, /class="btn btn-sm btn-dark"/);
|
|
assert.doesNotMatch(html, /btn-outline-dark/);
|
|
});
|
|
});
|
|
|
|
test('view-as start and stop routes audit the authenticated administrator', async () => {
|
|
const handlers = {};
|
|
const permissionKeys = [];
|
|
const auditEvents = [];
|
|
const helperCalls = [];
|
|
const app = {
|
|
post(path, ...routeHandlers) {
|
|
handlers[path] = routeHandlers;
|
|
}
|
|
};
|
|
const administrator = {
|
|
id: 7,
|
|
name: 'Administrator',
|
|
username: 'admin',
|
|
permissionKeys: ['users.read', 'impersonate.allow']
|
|
};
|
|
|
|
registerViewAsRoutes(app, {
|
|
pool: {
|
|
async query(sql, params) {
|
|
assert.match(sql, /FROM a_users/);
|
|
assert.deepEqual(params, [19]);
|
|
return [[{ id: 19, name: 'Screen operator', username: 'operator' }]];
|
|
}
|
|
},
|
|
requirePermission(permissionKey) {
|
|
permissionKeys.push(permissionKey);
|
|
return function (_req, _res, next) { next(); };
|
|
},
|
|
async startViewAsUser(_pool, _req, actorId, targetId) {
|
|
helperCalls.push(['start', actorId, targetId]);
|
|
return true;
|
|
},
|
|
async stopViewAsUser(_pool, _req, actorId) {
|
|
helperCalls.push(['stop', actorId]);
|
|
return true;
|
|
},
|
|
async recordRequestAuditEvent(_pool, _req, event) {
|
|
auditEvents.push(event);
|
|
}
|
|
});
|
|
|
|
const startResponse = {
|
|
redirect(path) { this.redirectedTo = path; },
|
|
status(statusCode) { this.statusCode = statusCode; return this; },
|
|
send(body) { this.body = body; return this; },
|
|
sendStatus(statusCode) { this.statusCode = statusCode; return this; }
|
|
};
|
|
await handlers['/settings/users/:id/view-as'][2]({
|
|
params: { id: '19' },
|
|
authenticatedUser: administrator,
|
|
currentUser: administrator
|
|
}, startResponse, (error) => { throw error; });
|
|
|
|
const stopResponse = {
|
|
redirect(path) { this.redirectedTo = path; },
|
|
status(statusCode) { this.statusCode = statusCode; return this; },
|
|
send(body) { this.body = body; return this; }
|
|
};
|
|
await handlers['/settings/view-as/stop'][0]({
|
|
authenticatedUser: administrator,
|
|
currentUser: { id: 19, username: 'operator', isViewingAs: true }
|
|
}, stopResponse, (error) => { throw error; });
|
|
|
|
assert.deepEqual(permissionKeys, ['users.read', 'impersonate.allow']);
|
|
assert.deepEqual(helperCalls, [['start', 7, 19], ['stop', 7]]);
|
|
assert.deepEqual(auditEvents.map(function (event) { return event.actorUserId; }), [7, 7]);
|
|
assert.deepEqual(auditEvents.map(function (event) { return event.eventType; }), ['user.view_as_started', 'user.view_as_stopped']);
|
|
assert.equal(startResponse.redirectedTo, '/dashboard');
|
|
assert.equal(stopResponse.redirectedTo, '/settings/users');
|
|
});
|
|
|
|
test('RSS and weather refresh actions stay visible but disabled when authorized previewing', () => {
|
|
const rssPage = renderRssFeedEditPage({ id: 31, name: 'Feed', enabled: 1 }, {}, '', {
|
|
isViewingAs: true,
|
|
permissionKeys: ['rss-feeds.update', 'rss-feeds.allow']
|
|
});
|
|
const weatherPage = renderWeatherLocationEditPage({ id: 12, name: 'Forecast', enabled: 1 }, '', {
|
|
isViewingAs: true,
|
|
permissionKeys: ['weather.update', 'weather.allow']
|
|
});
|
|
|
|
assert.match(rssPage, /data-manual-refresh-url="\/data-sources\/rss-feeds\/31\/refresh"[^>]*disabled/);
|
|
assert.match(rssPage, /data-async-data-source-toggle[^>]*disabled/);
|
|
assert.match(weatherPage, /data-weather-refresh-url="\/data-sources\/weather\/12\/refresh"[^>]*disabled/);
|
|
assert.match(weatherPage, /data-async-data-source-toggle[^>]*disabled/);
|
|
}); |