Skip to content

Commit

Permalink
feat: ability to freeze or completely disable integration scripts thr…
Browse files Browse the repository at this point in the history
…ough envvars (#30053)

Co-authored-by: Diego Sampaio <chinello@gmail.com>
  • Loading branch information
pierre-lehnen-rc and sampaiodiego committed Aug 10, 2023
1 parent f5a886a commit ff7e181
Show file tree
Hide file tree
Showing 9 changed files with 115 additions and 57 deletions.
5 changes: 5 additions & 0 deletions .changeset/curly-shoes-burn.md
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Added ability to freeze or completely disable integration scripts through envvars
13 changes: 12 additions & 1 deletion apps/meteor/app/integrations/server/api/api.js
Expand Up @@ -16,6 +16,8 @@ import { incomingLogger } from '../logger';
import { addOutgoingIntegration } from '../methods/outgoing/addOutgoingIntegration';
import { deleteOutgoingIntegration } from '../methods/outgoing/deleteOutgoingIntegration';

const DISABLE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(String(process.env.DISABLE_INTEGRATION_SCRIPTS).toLowerCase());

export const forbiddenModelMethods = ['registerModel', 'getCollectionName'];

const compiledScripts = {};
Expand Down Expand Up @@ -64,6 +66,10 @@ function buildSandbox(store = {}) {
}

function getIntegrationScript(integration) {
if (DISABLE_INTEGRATION_SCRIPTS) {
throw API.v1.failure('integration-scripts-disabled');
}

const compiledScript = compiledScripts[integration._id];
if (compiledScript && +compiledScript._updatedAt === +integration._updatedAt) {
return compiledScript.script;
Expand Down Expand Up @@ -172,7 +178,12 @@ async function executeIntegrationRest() {
emoji: this.integration.emoji,
};

if (this.integration.scriptEnabled && this.integration.scriptCompiled && this.integration.scriptCompiled.trim() !== '') {
if (
!DISABLE_INTEGRATION_SCRIPTS &&
this.integration.scriptEnabled &&
this.integration.scriptCompiled &&
this.integration.scriptCompiled.trim() !== ''
) {
let script;
try {
script = getIntegrationScript(this.integration);
Expand Down
17 changes: 16 additions & 1 deletion apps/meteor/app/integrations/server/lib/triggerHandler.js
Expand Up @@ -18,6 +18,8 @@ import { outgoingEvents } from '../../lib/outgoingEvents';
import { forbiddenModelMethods } from '../api/api';
import { outgoingLogger } from '../logger';

const DISABLE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(String(process.env.DISABLE_INTEGRATION_SCRIPTS).toLowerCase());

class RocketChatIntegrationHandler {
constructor() {
this.successResults = [200, 201, 202];
Expand Down Expand Up @@ -270,6 +272,10 @@ class RocketChatIntegrationHandler {
}

getIntegrationScript(integration) {
if (DISABLE_INTEGRATION_SCRIPTS) {
throw new Meteor.Error('integration-scripts-disabled');
}

const compiledScript = this.compiledScripts[integration._id];
if (compiledScript && +compiledScript._updatedAt === +integration._updatedAt) {
return compiledScript.script;
Expand Down Expand Up @@ -313,7 +319,12 @@ class RocketChatIntegrationHandler {
}

hasScriptAndMethod(integration, method) {
if (integration.scriptEnabled !== true || !integration.scriptCompiled || integration.scriptCompiled.trim() === '') {
if (
DISABLE_INTEGRATION_SCRIPTS ||
integration.scriptEnabled !== true ||
!integration.scriptCompiled ||
integration.scriptCompiled.trim() === ''
) {
return false;
}

Expand All @@ -328,6 +339,10 @@ class RocketChatIntegrationHandler {
}

async executeScript(integration, method, params, historyId) {
if (DISABLE_INTEGRATION_SCRIPTS) {
return;
}

let script;
try {
script = this.getIntegrationScript(integration);
Expand Down
Expand Up @@ -12,6 +12,8 @@ import { outgoingEvents } from '../../lib/outgoingEvents';
const scopedChannels = ['all_public_channels', 'all_private_groups', 'all_direct_messages'];
const validChannelChars = ['@', '#'];

const FREEZE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(String(process.env.FREEZE_INTEGRATION_SCRIPTS).toLowerCase());

function _verifyRequiredFields(integration: INewOutgoingIntegration | IUpdateOutgoingIntegration): void {
if (
!integration.event ||
Expand Down Expand Up @@ -169,7 +171,7 @@ export const validateOutgoingIntegration = async function (
delete integrationData.triggerWords;
}

if (integration.scriptEnabled === true && integration.script && integration.script.trim() !== '') {
if (!FREEZE_INTEGRATION_SCRIPTS && integration.scriptEnabled === true && integration.script && integration.script.trim() !== '') {
try {
const babelOptions = Object.assign(Babel.getDefaultOptions({ runtime: false }), {
compact: true,
Expand Down
Expand Up @@ -11,6 +11,8 @@ import { hasPermissionAsync, hasAllPermissionAsync } from '../../../../authoriza

const validChannelChars = ['@', '#'];

const FREEZE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(String(process.env.FREEZE_INTEGRATION_SCRIPTS).toLowerCase());

declare module '@rocket.chat/ui-contexts' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
Expand Down Expand Up @@ -74,6 +76,10 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn
});
}

if (FREEZE_INTEGRATION_SCRIPTS && integration.script?.trim()) {
throw new Meteor.Error('integration-scripts-disabled');
}

const user = await Users.findOne({ username: integration.username });

if (!user) {
Expand Down
Expand Up @@ -9,6 +9,8 @@ import { hasAllPermissionAsync, hasPermissionAsync } from '../../../../authoriza

const validChannelChars = ['@', '#'];

const FREEZE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(String(process.env.FREEZE_INTEGRATION_SCRIPTS).toLowerCase());

declare module '@rocket.chat/ui-contexts' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
Expand Down Expand Up @@ -64,42 +66,48 @@ Meteor.methods<ServerMethods>({
});
}

let scriptCompiled: string | undefined;
let scriptError: Pick<Error, 'name' | 'message' | 'stack'> | undefined;

if (integration.scriptEnabled === true && integration.script && integration.script.trim() !== '') {
try {
let babelOptions = Babel.getDefaultOptions({ runtime: false });
babelOptions = _.extend(babelOptions, { compact: true, minified: true, comments: false });

scriptCompiled = Babel.compile(integration.script, babelOptions).code;
scriptError = undefined;
await Integrations.updateOne(
{ _id: integrationId },
{
$set: {
scriptCompiled,
},
$unset: { scriptError: 1 as const },
},
);
} catch (e) {
scriptCompiled = undefined;
if (e instanceof Error) {
const { name, message, stack } = e;
scriptError = { name, message, stack };
}
await Integrations.updateOne(
{ _id: integrationId },
{
$set: {
scriptError,
if (FREEZE_INTEGRATION_SCRIPTS) {
if (currentIntegration.script?.trim() !== integration.script?.trim()) {
throw new Meteor.Error('integration-scripts-disabled');
}
} else {
let scriptCompiled: string | undefined;
let scriptError: Pick<Error, 'name' | 'message' | 'stack'> | undefined;

if (integration.scriptEnabled === true && integration.script && integration.script.trim() !== '') {
try {
let babelOptions = Babel.getDefaultOptions({ runtime: false });
babelOptions = _.extend(babelOptions, { compact: true, minified: true, comments: false });

scriptCompiled = Babel.compile(integration.script, babelOptions).code;
scriptError = undefined;
await Integrations.updateOne(
{ _id: integrationId },
{
$set: {
scriptCompiled,
},
$unset: { scriptError: 1 as const },
},
$unset: {
scriptCompiled: 1 as const,
);
} catch (e) {
scriptCompiled = undefined;
if (e instanceof Error) {
const { name, message, stack } = e;
scriptError = { name, message, stack };
}
await Integrations.updateOne(
{ _id: integrationId },
{
$set: {
scriptError,
},
$unset: {
scriptCompiled: 1 as const,
},
},
},
);
);
}
}
}

Expand Down Expand Up @@ -157,8 +165,12 @@ Meteor.methods<ServerMethods>({
emoji: integration.emoji,
alias: integration.alias,
channel: channels,
script: integration.script,
scriptEnabled: integration.scriptEnabled,
...(FREEZE_INTEGRATION_SCRIPTS
? {}
: {
script: integration.script,
scriptEnabled: integration.scriptEnabled,
}),
overrideDestinationChannelEnabled: integration.overrideDestinationChannelEnabled,
_updatedAt: new Date(),
_updatedBy: await Users.findOne({ _id: this.userId }, { projection: { username: 1 } }),
Expand Down
Expand Up @@ -14,6 +14,8 @@ declare module '@rocket.chat/ui-contexts' {
}
}

const FREEZE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(String(process.env.FREEZE_INTEGRATION_SCRIPTS).toLowerCase());

export const addOutgoingIntegration = async (userId: string, integration: INewOutgoingIntegration): Promise<IOutgoingIntegration> => {
check(
integration,
Expand Down Expand Up @@ -50,6 +52,10 @@ export const addOutgoingIntegration = async (userId: string, integration: INewOu
throw new Meteor.Error('not_authorized');
}

if (FREEZE_INTEGRATION_SCRIPTS && integration.script?.trim()) {
throw new Meteor.Error('integration-scripts-disabled');
}

const integrationData = await validateOutgoingIntegration(integration, userId);

const result = await Integrations.insertOne(integrationData);
Expand Down
Expand Up @@ -16,6 +16,8 @@ declare module '@rocket.chat/ui-contexts' {
}
}

const FREEZE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(String(process.env.FREEZE_INTEGRATION_SCRIPTS).toLowerCase());

Meteor.methods<ServerMethods>({
async updateOutgoingIntegration(integrationId, _integration) {
if (!this.userId) {
Expand Down Expand Up @@ -51,22 +53,8 @@ Meteor.methods<ServerMethods>({
throw new Meteor.Error('invalid_integration', '[methods] updateOutgoingIntegration -> integration not found');
}

if (integration.scriptCompiled) {
await Integrations.updateOne(
{ _id: integrationId },
{
$set: { scriptCompiled: integration.scriptCompiled },
$unset: { scriptError: 1 as const },
},
);
} else {
await Integrations.updateOne(
{ _id: integrationId },
{
$set: { scriptError: integration.scriptError },
$unset: { scriptCompiled: 1 as const },
},
);
if (FREEZE_INTEGRATION_SCRIPTS && integration.script?.trim() !== currentIntegration.script?.trim()) {
throw new Meteor.Error('integration-scripts-disabled');
}

await Integrations.updateOne(
Expand All @@ -86,8 +74,13 @@ Meteor.methods<ServerMethods>({
userId: integration.userId,
urls: integration.urls,
token: integration.token,
script: integration.script,
scriptEnabled: integration.scriptEnabled,
...(FREEZE_INTEGRATION_SCRIPTS
? {}
: {
script: integration.script,
scriptEnabled: integration.scriptEnabled,
...(integration.scriptCompiled ? { scriptCompiled: integration.scriptCompiled } : { scriptError: integration.scriptError }),
}),
triggerWords: integration.triggerWords,
retryFailedCalls: integration.retryFailedCalls,
retryCount: integration.retryCount,
Expand All @@ -97,6 +90,13 @@ Meteor.methods<ServerMethods>({
_updatedAt: new Date(),
_updatedBy: await Users.findOne({ _id: this.userId }, { projection: { username: 1 } }),
},
...(FREEZE_INTEGRATION_SCRIPTS
? {}
: {
$unset: {
...(integration.scriptCompiled ? { scriptError: 1 as const } : { scriptCompiled: 1 as const }),
},
}),
},
);

Expand Down
1 change: 1 addition & 0 deletions apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
Expand Up @@ -2585,6 +2585,7 @@
"Integration_History_Cleared": "Integration History Successfully Cleared",
"Integration_Incoming_WebHook": "Incoming WebHook Integration",
"Integration_New": "New Integration",
"integration-scripts-disabled": "Integration Scripts are Disabled",
"Integration_Outgoing_WebHook": "Outgoing WebHook Integration",
"Integration_Outgoing_WebHook_History": "Outgoing WebHook Integration History",
"Integration_Outgoing_WebHook_History_Data_Passed_To_Trigger": "Data Passed to Integration",
Expand Down

0 comments on commit ff7e181

Please sign in to comment.