diff --git a/adminforth/commands/createApp/templates/index.ts.hbs b/adminforth/commands/createApp/templates/index.ts.hbs index d612a963a..56215c8db 100644 --- a/adminforth/commands/createApp/templates/index.ts.hbs +++ b/adminforth/commands/createApp/templates/index.ts.hbs @@ -26,6 +26,13 @@ export const admin = new AdminForth({ return "Please use adminforth as username and adminforth as password" } }, + websocketTopicAuth: async (topic: string, adminUser: AdminUser) => { + if (!adminUser) { + // don't allow anonymous users to subscribe + return false; + } + return true; + } }, customization: { brandName: "{{appName}}", diff --git a/adminforth/dataConnectors/baseConnector.ts b/adminforth/dataConnectors/baseConnector.ts index c51b77e6e..8128aadce 100644 --- a/adminforth/dataConnectors/baseConnector.ts +++ b/adminforth/dataConnectors/baseConnector.ts @@ -3,13 +3,14 @@ import { AdminForthResourceColumn, IAdminForthSort, IAdminForthSingleFilter, IAdminForthAndOrFilter, AdminForthConfig, - IAggregationRule, IGroupByRule, IGroupByDateTrunc, + IAggregationRule, IGroupByRule, IGroupByDateTrunc } from "../types/Back.js"; - +import type { AdminUser } from "../types/Common.js" import { suggestIfTypo } from "../modules/utils.js"; -import { AdminForthDataTypes, AdminForthFilterOperators, AdminForthSortDirections } from "../types/Common.js"; +import { interpretResource } from "../modules/restApi.js"; +import { ActionCheckSource, AdminForthDataTypes, AdminForthFilterOperators, AdminForthSortDirections, AllowedActionsEnum } from "../types/Common.js"; import { randomUUID } from "crypto"; import dayjs from "dayjs"; import { afLogger } from '../modules/logger.js'; @@ -24,11 +25,32 @@ type AdminForthFilterNormalizationResult = { }; async function publishShowPageUpdate(resource: AdminForthResource, recordId: string, updates: Record) { - await global.adminforth.websocket.publish(`/showPage/${resource.resourceId}/${String(recordId)}`, { - resourceId: resource.resourceId, - recordId, - updates, - }); + await global.adminforth.websocket.publish(`/showPage/${resource.resourceId}/${String(recordId)}`, + { + resourceId: resource.resourceId, + recordId, + updates, + }, + async (adminUser: AdminUser): Promise => { + if (!adminUser) { + // anonymous clients should never receive record updates + return false; + } + try { + const { allowedActions } = await interpretResource( + adminUser, + resource, + { requestBody: null, pk: recordId }, + ActionCheckSource.ShowRequest, + global.adminforth, + ); + return allowedActions[AllowedActionsEnum.show] === true; + } catch (e) { + afLogger.error(`Error while checking show access for ${resource.resourceId} record ${recordId}, assuming update should not be sent: ${e}`); + return false; + } + } + ); } diff --git a/adminforth/modules/configValidator.ts b/adminforth/modules/configValidator.ts index 797ac3b2d..01f611dce 100644 --- a/adminforth/modules/configValidator.ts +++ b/adminforth/modules/configValidator.ts @@ -28,6 +28,7 @@ import { AdminForthResourcePages, AdminForthDataTypes, Predicate, + AdminUser, } from "../types/Common.js"; import AdminForth from "adminforth"; import { AdminForthConfigMenuItem } from "adminforth"; @@ -1216,6 +1217,17 @@ export default class ConfigValidator implements IConfigValidator { } } + if (!newConfig.auth.websocketTopicAuth) { + newConfig.auth.websocketTopicAuth = async (topic: string, adminUser: AdminUser) => { + afLogger.warn('websocketTopicAuth is not provided. Public access to websocket topics (except /opentopic) is blocked.'); + if (!adminUser) { + // don't allow anonymous users to subscribe + return false; + } + return true; + } + } + newConfig.auth.rateLimit = newConfig.auth.rateLimit || [...DEFAULT_AUTH_RATE_LIMIT]; if (!Array.isArray(newConfig.auth.rateLimit)) { errors.push(`auth.rateLimit must be an array of strings in format "500/5m"`);