RocketChat/Rocket.Chat

View on GitHub
apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts

Summary

Maintainability
D
2 days
Test Coverage
import type { IUser, INewOutgoingIntegration, IOutgoingIntegration, IUpdateOutgoingIntegration } from '@rocket.chat/core-typings';
import { Subscriptions, Users, Rooms } from '@rocket.chat/models';
import { pick } from '@rocket.chat/tools';
import { Babel } from 'meteor/babel-compiler';
import { Match } from 'meteor/check';
import { Meteor } from 'meteor/meteor';

import { parseCSV } from '../../../../lib/utils/parseCSV';
import { hasPermissionAsync, hasAllPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { outgoingEvents } from '../../lib/outgoingEvents';
import { isScriptEngineFrozen } from './validateScriptEngine';

const scopedChannels = ['all_public_channels', 'all_private_groups', 'all_direct_messages'];
const validChannelChars = ['@', '#'];

function _verifyRequiredFields(integration: INewOutgoingIntegration | IUpdateOutgoingIntegration): void {
    if (
        !integration.event ||
        !Match.test(integration.event, String) ||
        integration.event.trim() === '' ||
        !outgoingEvents[integration.event]
    ) {
        throw new Meteor.Error('error-invalid-event-type', 'Invalid event type', {
            function: 'validateOutgoing._verifyRequiredFields',
        });
    }

    if (!integration.username || !Match.test(integration.username, String) || integration.username.trim() === '') {
        throw new Meteor.Error('error-invalid-username', 'Invalid username', {
            function: 'validateOutgoing._verifyRequiredFields',
        });
    }

    if (outgoingEvents[integration.event].use.targetRoom && !integration.targetRoom) {
        throw new Meteor.Error('error-invalid-targetRoom', 'Invalid Target Room', {
            function: 'validateOutgoing._verifyRequiredFields',
        });
    }

    if (!Match.test(integration.urls, [String])) {
        throw new Meteor.Error('error-invalid-urls', 'Invalid URLs', {
            function: 'validateOutgoing._verifyRequiredFields',
        });
    }

    integration.urls = integration.urls.filter((url) => url && url.trim() !== '');

    if (integration.urls.length === 0) {
        throw new Meteor.Error('error-invalid-urls', 'Invalid URLs', {
            function: 'validateOutgoing._verifyRequiredFields',
        });
    }
}

async function _verifyUserHasPermissionForChannels(userId: IUser['_id'], channels: string[]): Promise<void> {
    for await (let channel of channels) {
        if (scopedChannels.includes(channel)) {
            if (channel === 'all_public_channels') {
                // No special permissions needed to add integration to public channels
            } else if (!(await hasPermissionAsync(userId, 'manage-outgoing-integrations'))) {
                throw new Meteor.Error('error-invalid-channel', 'Invalid Channel', {
                    function: 'validateOutgoing._verifyUserHasPermissionForChannels',
                });
            }
        } else {
            let record;
            const channelType = channel[0];
            channel = channel.substr(1);

            switch (channelType) {
                case '#':
                    record = await Rooms.findOne({
                        $or: [{ _id: channel }, { name: channel }],
                    });
                    break;
                case '@':
                    record = await Users.findOne({
                        $or: [{ _id: channel }, { username: channel }],
                    });
                    break;
            }

            if (!record) {
                throw new Meteor.Error('error-invalid-room', 'Invalid room', {
                    function: 'validateOutgoing._verifyUserHasPermissionForChannels',
                });
            }

            if (
                !(await hasAllPermissionAsync(userId, ['manage-outgoing-integrations', 'manage-own-outgoing-integrations'])) &&
                !(await Subscriptions.findOneByRoomIdAndUserId(record._id, userId, { projection: { _id: 1 } }))
            ) {
                throw new Meteor.Error('error-invalid-channel', 'Invalid Channel', {
                    function: 'validateOutgoing._verifyUserHasPermissionForChannels',
                });
            }
        }
    }
}

function _verifyRetryInformation(integration: IOutgoingIntegration): void {
    if (!integration.retryFailedCalls) {
        return;
    }

    // Don't allow negative retry counts
    integration.retryCount =
        integration.retryCount && parseInt(String(integration.retryCount)) > 0 ? parseInt(String(integration.retryCount)) : 4;
    integration.retryDelay = !integration.retryDelay?.trim() ? 'powers-of-ten' : integration.retryDelay.toLowerCase();
}

export const validateOutgoingIntegration = async function (
    integration: INewOutgoingIntegration | IUpdateOutgoingIntegration,
    userId: IUser['_id'],
): Promise<IOutgoingIntegration> {
    if (integration.channel && Match.test(integration.channel, String) && integration.channel.trim() === '') {
        delete integration.channel;
    }

    // Moved to it's own function to satisfy the complexity rule
    _verifyRequiredFields(integration);

    let channels: string[] = [];
    if (outgoingEvents[integration.event].use.channel) {
        if (!Match.test(integration.channel, String)) {
            throw new Meteor.Error('error-invalid-channel', 'Invalid Channel', {
                function: 'validateOutgoing',
            });
        } else {
            channels = parseCSV(integration.channel);

            for (const channel of channels) {
                if (!validChannelChars.includes(channel[0]) && !scopedChannels.includes(channel.toLowerCase())) {
                    throw new Meteor.Error('error-invalid-channel-start-with-chars', 'Invalid channel. Start with @ or #', {
                        function: 'validateOutgoing',
                    });
                }
            }
        }
    } else if (!(await hasPermissionAsync(userId, 'manage-outgoing-integrations'))) {
        throw new Meteor.Error('error-invalid-permissions', 'Invalid permission for required Integration creation.', {
            function: 'validateOutgoing',
        });
    }

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

    if (!user) {
        throw new Meteor.Error('error-invalid-user', 'Invalid user (did you delete the `rocket.cat` user?)', { function: 'validateOutgoing' });
    }

    const integrationData: IOutgoingIntegration = {
        ...integration,
        scriptEngine: integration.scriptEngine ?? 'isolated-vm',
        type: 'webhook-outgoing',
        channel: channels,
        userId: user._id,
        _createdAt: new Date(),
        _createdBy: await Users.findOne(userId, { projection: { username: 1 } }),
    };

    if (outgoingEvents[integration.event].use.triggerWords && integration.triggerWords) {
        if (!Match.test(integration.triggerWords, [String])) {
            throw new Meteor.Error('error-invalid-triggerWords', 'Invalid triggerWords', {
                function: 'validateOutgoing',
            });
        }

        integrationData.triggerWords = integration.triggerWords.filter((word) => word && word.trim() !== '');
    } else {
        delete integrationData.triggerWords;
    }

    // Only compile the script if it is enabled and using a sandbox that is not frozen
    if (
        !isScriptEngineFrozen(integrationData.scriptEngine) &&
        integration.scriptEnabled === true &&
        integration.script &&
        integration.script.trim() !== ''
    ) {
        try {
            const babelOptions = Object.assign(Babel.getDefaultOptions({ runtime: false }), {
                compact: true,
                minified: true,
                comments: false,
            });

            integrationData.scriptCompiled = Babel.compile(integration.script, babelOptions).code;
            integrationData.scriptError = undefined;
        } catch (e) {
            integrationData.scriptCompiled = undefined;
            integrationData.scriptError = e instanceof Error ? pick(e, 'name', 'message', 'stack') : undefined;
        }
    }

    if (typeof integration.runOnEdits !== 'undefined') {
        // Verify this value is only true/false
        integrationData.runOnEdits = integration.runOnEdits === true;
    }

    await _verifyUserHasPermissionForChannels(userId, channels);
    _verifyRetryInformation(integrationData);

    return integrationData;
};