Files
upstream-rocketchat/packages/apps/tests/server/runtime/SecureFieldsCodecCompatibility.test.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

238 lines
8.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as assert from 'node:assert';
import * as fs from 'node:fs/promises';
import * as os from 'node:os';
import * as path from 'node:path';
import { describe, it, before, after } from 'node:test';
import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus';
import { RoomType } from '@rocket.chat/apps-engine/definition/rooms';
import { kSecureFields } from '../../../src/lib/SecureFields';
import type { AppManager } from '../../../src/server/AppManager';
import type { IParseAppPackageResult } from '../../../src/server/compiler';
import { AppApiManager } from '../../../src/server/managers';
import { DenoRuntimeSubprocessController } from '../../../src/server/runtime/deno/AppsEngineDenoRuntime';
import type { IAppStorageItem } from '../../../src/server/storage';
import { TestInfastructureSetup } from '../../test-data/utilities';
/**
* These tests verify end-to-end codec compatibility between Node and Deno, specifically
* the '@@SecureFields' mechanism introduced to guard access to sensitive room fields
* (e.g. abacAttributes) behind app permissions.
*
* The flow being tested:
* 1. Node encodes a room object that includes a '@@SecureFields' descriptor for abacAttributes
* using the SECURE_FIELDS_HANDLER_EXT msgpack extension type.
* 2. Deno receives and decodes the message. Its codec calls applySecureFields(), which:
* - Checks the running app's declared permissions.
* - If the app has 'abac.read', it merges abacAttributes into the plain room object.
* - Otherwise it strips the field entirely.
* 3. The Deno app's checkPreRoomCreatePrevent handler returns Array.isArray(room.abacAttributes),
* letting Node observe whether the field was received or withheld.
*
* Two app fixtures are used:
* - secure-fields-test-with-abac_0.0.1.zip → declares { name: 'abac.read' }
* - secure-fields-test-no-abac_0.0.1.zip → declares no permissions
*/
describe('@@SecureFields codec compatibility (Node → Deno)', () => {
/** A minimal room that carries abacAttributes as a secure field. */
const roomWithSecureField = {
id: 'room-secure-fields-test',
type: RoomType.CHANNEL,
slugifiedName: 'general',
displayName: 'General',
[kSecureFields]: [
{
permission: 'abac.read',
name: 'abacAttributes',
value: [{ key: 'department', values: ['support', 'engineering'] }],
},
],
};
/** A room with several regular (non-secured) fields plus the secure abacAttributes field. */
const roomWithMixedFields = {
id: 'room-mixed-fields-test',
type: RoomType.CHANNEL,
slugifiedName: 'mixed',
displayName: 'Mixed',
customFields: { foo: 'bar' },
messageCount: 42,
[kSecureFields]: [
{
permission: 'abac.read',
name: 'abacAttributes',
value: [{ key: 'tenant', values: ['alpha'] }],
},
],
};
// -------------------------------------------------------------------------
// Shared helpers
// -------------------------------------------------------------------------
function buildManager(): AppManager {
const infrastructure = new TestInfastructureSetup();
const manager = infrastructure.getMockManager();
const api = new AppApiManager(manager);
manager.getApiManager = () => api;
return manager;
}
async function parseAppPackage(manager: AppManager, zipName: string): Promise<IParseAppPackageResult> {
const buf = await fs.readFile(path.join(__dirname, '../../test-data/apps', zipName));
return manager.getParser().unpackageApp(buf);
}
// -------------------------------------------------------------------------
// App WITH abac.read permission
// -------------------------------------------------------------------------
describe('app that declares abac.read permission', () => {
let manager: AppManager;
let controller: DenoRuntimeSubprocessController;
let appPackage: IParseAppPackageResult;
let appStorageItem: IAppStorageItem;
before(
async () => {
manager = buildManager();
appPackage = await parseAppPackage(manager, 'secure-fields-test-with-abac_0.0.1.zip');
appStorageItem = {
id: 'secure-fields-test-with-abac',
status: AppStatus.MANUALLY_ENABLED,
} as IAppStorageItem;
controller = new DenoRuntimeSubprocessController(manager, appPackage, appStorageItem);
await controller.setupApp();
},
{ timeout: 60_000 },
);
after(
async () => {
await controller?.stopApp();
await fs.unlink(path.join(os.tmpdir(), 'deno-runtime')).catch(() => undefined);
},
{ timeout: 30_000 },
);
it('receives abacAttributes when the room is encoded with @@SecureFields', { timeout: 15_000 }, async () => {
/**
* The app's checkPreRoomCreatePrevent returns Array.isArray(room.abacAttributes).
* Because the app has abac.read, Deno's applySecureFields should attach
* abacAttributes to the decoded room object, making the result `true`.
*/
const result = await controller.sendRequest({
method: 'app:checkPreRoomCreatePrevent',
params: [roomWithSecureField],
});
assert.strictEqual(result, true, 'App with abac.read should receive abacAttributes from a @@SecureFields-encoded room');
});
it('still receives regular (non-secured) room fields alongside the secure field', { timeout: 15_000 }, async () => {
/**
* Verify that applying secure fields does not discard ordinary room properties.
* The handler returns `true` only when abacAttributes is an array AND the
* room reached the handler with its other fields intact (the Room constructor
* uses Object.assign, so any missing fields would surface as assertion
* failures in a more detailed handler – here we at minimum confirm abacAttributes
* was applied without corrupting the object).
*/
const result = await controller.sendRequest({
method: 'app:checkPreRoomCreatePrevent',
params: [roomWithMixedFields],
});
assert.strictEqual(result, true, 'abacAttributes should be applied to a room that also carries non-secure fields');
});
it('does not throw when the room carries no @@SecureFields descriptor', { timeout: 15_000 }, async () => {
/**
* Plain rooms (without @@SecureFields) must still be decodable and routable
* to the handler. abacAttributes will be absent, so the handler returns false,
* but no codec error should be raised.
*/
const plainRoom = {
id: 'plain-room',
type: RoomType.CHANNEL,
slugifiedName: 'plain',
};
const result = await controller.sendRequest({
method: 'app:checkPreRoomCreatePrevent',
params: [plainRoom],
});
assert.strictEqual(result, false, 'A plain room (no @@SecureFields) should be decoded normally; abacAttributes absent → false');
});
});
// -------------------------------------------------------------------------
// App WITHOUT abac.read permission
// -------------------------------------------------------------------------
describe('app that does NOT declare abac.read permission', () => {
let manager: AppManager;
let controller: DenoRuntimeSubprocessController;
let appPackage: IParseAppPackageResult;
let appStorageItem: IAppStorageItem;
before(
async () => {
manager = buildManager();
appPackage = await parseAppPackage(manager, 'secure-fields-test-no-abac_0.0.1.zip');
appStorageItem = {
id: 'secure-fields-test-no-abac',
status: AppStatus.MANUALLY_ENABLED,
} as IAppStorageItem;
controller = new DenoRuntimeSubprocessController(manager, appPackage, appStorageItem);
await controller.setupApp();
},
{ timeout: 60_000 },
);
after(
async () => {
await controller?.stopApp();
await fs.unlink(path.join(os.tmpdir(), 'deno-runtime')).catch(() => undefined);
},
{ timeout: 30_000 },
);
it('does not receive abacAttributes when the app lacks abac.read permission', { timeout: 15_000 }, async () => {
/**
* The room is encoded with @@SecureFields for abacAttributes, but this app
* does not declare abac.read. Deno's applySecureFields should withhold the
* field, so the handler returns `false`.
*/
const result = await controller.sendRequest({
method: 'app:checkPreRoomCreatePrevent',
params: [roomWithSecureField],
});
assert.strictEqual(result, false, 'App without abac.read must not receive abacAttributes from a @@SecureFields-encoded room');
});
it('still decodes regular (non-secured) room fields correctly', { timeout: 15_000 }, async () => {
/**
* Even though abacAttributes is withheld, the remaining room properties
* (id, type, slugifiedName, customFields, messageCount, …) must survive
* the round-trip unaltered. The handler only inspects abacAttributes, so
* we verify this indirectly: if the room object were corrupted entirely,
* Deno would throw an error instead of returning a clean `false`.
*/
const result = await controller.sendRequest({
method: 'app:checkPreRoomCreatePrevent',
params: [roomWithMixedFields],
});
assert.strictEqual(result, false, 'Regular room fields should survive the codec round-trip even when a secure field is withheld');
});
});
});