Files
upstream-rocketchat/apps/meteor/server/lib/settingValidationRules.ts
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

157 lines
5.3 KiB
TypeScript

import type { ISetting, SettingValidationRule } from '@rocket.chat/core-typings';
import { isSettingCode } from '@rocket.chat/core-typings';
import { Logger } from '@rocket.chat/logger';
import { createPredicateFromFilter } from '@rocket.chat/mongo-adapter';
import { isRecord } from '@rocket.chat/tools';
import { settings } from '../settings';
import { getSettingSchemaValidator } from '../settings/functions/settingSchemas';
const logger = new Logger('SettingValidation');
export class SettingValidationError extends Error {}
const isAppliesWhenCondition = (value: unknown): value is { _id: ISetting['_id']; value: unknown } =>
isRecord(value) && typeof value._id === 'string' && 'value' in value;
const isValidationRule = (rule: unknown): rule is SettingValidationRule =>
isRecord(rule) &&
isRecord(rule.query) &&
(rule.appliesWhen === undefined ||
isAppliesWhenCondition(rule.appliesWhen) ||
(Array.isArray(rule.appliesWhen) && rule.appliesWhen.every(isAppliesWhenCondition)));
const isValidationRuleArray = (value: unknown): value is SettingValidationRule[] => Array.isArray(value) && value.every(isValidationRule);
const parseValidationRules = (settingId: ISetting['_id'], validation: NonNullable<ISetting['validation']>): SettingValidationRule[] => {
if (typeof validation !== 'string') {
return validation;
}
let parsed: unknown;
try {
parsed = JSON.parse(validation);
} catch (err) {
logger.error({ msg: 'Failed to parse setting validation rules', settingId, err });
return [];
}
if (!isValidationRuleArray(parsed)) {
logger.error({ msg: 'Setting validation rules are not well-formed', settingId });
return [];
}
return parsed;
};
const isSettingReference = (value: unknown): value is { $setting: ISetting['_id'] } =>
isRecord(value) && typeof value.$setting === 'string';
// an empty value means the setting is unconfigured and always passes
const validatesSchema = (setting: ISetting, value: unknown): boolean => {
const validate = getSettingSchemaValidator(setting._id);
if (!validate || !isSettingCode(setting) || setting.code !== 'application/json' || value === '') {
return true;
}
if (typeof value !== 'string') {
return false;
}
try {
return !!validate(JSON.parse(value));
} catch {
return false;
}
};
/**
* Evaluates a setting's validation rule against the value being saved. Returns `false` only when the rule's filter
* rejects the candidate. A rule that references a setting which does not exist is logged and treated as passing, so a
* broken declaration never blocks a save.
*/
const evaluateSettingValidationRule = (
settingId: ISetting['_id'],
rule: SettingValidationRule,
get: (id: ISetting['_id']) => unknown,
): boolean => {
const references: Record<string, unknown> = {};
// true when `filter` matches the current value of the setting `id`
const matches = (filter: Record<string, unknown>, id: ISetting['_id']): boolean =>
createPredicateFromFilter<{ _id: ISetting['_id']; value: unknown }>(filter)({ _id: id, value: get(id) });
// replaces every `{ $setting: '<id>' }` in the filter with that setting's value,
// recording each in `references`
const resolveObject = (filter: Record<string, unknown>): Record<string, unknown> =>
Object.fromEntries(Object.entries(filter).map(([key, value]) => [key, resolveNode(value)]));
const resolveNode = (node: unknown): unknown => {
if (isSettingReference(node)) {
references[node.$setting] = get(node.$setting);
return references[node.$setting];
}
if (Array.isArray(node)) {
return node.map(resolveNode);
}
if (isRecord(node)) {
return resolveObject(node);
}
return node;
};
const conditions = [rule.appliesWhen ?? []].flat();
const query = resolveObject(rule.query);
for (const condition of conditions) {
references[condition._id] = get(condition._id);
}
const unknownReferences = Object.keys(references).filter((id) => references[id] === undefined);
if (unknownReferences.length) {
logger.error({ msg: 'Setting validation rule references unknown settings', settingId, references: unknownReferences });
return true;
}
// the rule only applies while all of its `appliesWhen` conditions hold,
// otherwise it is not relevant and passes
const conditionHolds = (condition: { _id: ISetting['_id']; value: unknown }): boolean =>
matches({ value: condition.value }, condition._id);
if (!conditions.every(conditionHolds)) {
return true;
}
return matches(query, settingId);
};
export const validateSettingRules = (changes: { _id: ISetting['_id']; value: ISetting['value'] }[]): void => {
// a value being saved in this batch wins over the stored one,
// so rules never compare against stale values
const getValueOf = (id: ISetting['_id']): unknown => {
const beingSaved = changes.find(({ _id }) => _id === id);
return beingSaved ? beingSaved.value : settings.get(id);
};
for (const { _id, value } of changes) {
const setting = settings.getSetting(_id);
if (!setting) {
continue;
}
if (!validatesSchema(setting, value)) {
throw new SettingValidationError(`${setting._id}_Invalid`);
}
if (!setting.validation) {
continue;
}
for (const rule of parseValidationRules(setting._id, setting.validation)) {
if (evaluateSettingValidationRule(setting._id, rule, getValueOf)) {
continue;
}
throw new SettingValidationError(`${setting._id}_Invalid`);
}
}
};