snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
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
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
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
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
import { expect } from 'chai';
|
||||
import proxyquire from 'proxyquire';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { Streamer, StreamerCentral } from '../../../../../server/modules/streamer/streamer.module';
|
||||
|
||||
class TestStreamer extends Streamer<any> {
|
||||
registerPublication(): void {
|
||||
// noop
|
||||
}
|
||||
|
||||
registerMethod(): void {
|
||||
// noop
|
||||
}
|
||||
|
||||
changedPayload(): string {
|
||||
return 'payload';
|
||||
}
|
||||
}
|
||||
const validateActionStub = sinon.stub();
|
||||
const processSerializedSignalStub = sinon.stub();
|
||||
const countByRoomIdAndUserIdStub = sinon.stub();
|
||||
const findSubscriptionsExcludingUserStub = sinon.stub();
|
||||
|
||||
const { NotificationsModule } = proxyquire.noCallThru().load('../../../../../server/modules/notifications/notifications.module', {
|
||||
'@rocket.chat/core-services': {
|
||||
VideoConf: { validateAction: validateActionStub },
|
||||
MediaCall: { processSerializedSignal: processSerializedSignalStub },
|
||||
},
|
||||
'@rocket.chat/models': {
|
||||
Subscriptions: {
|
||||
countByRoomIdAndUserId: countByRoomIdAndUserIdStub,
|
||||
findByRoomIdAndNotUserId: findSubscriptionsExcludingUserStub,
|
||||
},
|
||||
Rooms: {},
|
||||
Users: {},
|
||||
},
|
||||
});
|
||||
|
||||
describe('NotificationsModule', () => {
|
||||
let notifications: any;
|
||||
|
||||
beforeEach(() => {
|
||||
notifications = new NotificationsModule(TestStreamer as any);
|
||||
notifications.configure();
|
||||
});
|
||||
|
||||
describe('notify-user allowWrite', () => {
|
||||
// `isWriteAllowed` is public on the concrete Streamer class but not on the IStreamer
|
||||
// interface that `streamUser` is typed as, so cast to reach it.
|
||||
const writeAllowed = (eventName: string, ...args: unknown[]) =>
|
||||
(notifications.streamUser as unknown as Streamer<'notify-user'>).isWriteAllowed({ userId: 'userId' } as any, eventName, args);
|
||||
|
||||
beforeEach(() => {
|
||||
validateActionStub.reset();
|
||||
validateActionStub.resolves(true);
|
||||
processSerializedSignalStub.reset();
|
||||
processSerializedSignalStub.resolves(undefined);
|
||||
countByRoomIdAndUserIdStub.reset();
|
||||
findSubscriptionsExcludingUserStub.reset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.keys(StreamerCentral.instances).forEach((name) => delete StreamerCentral.instances[name]);
|
||||
});
|
||||
|
||||
['force_logout', 'notification', 'message', 'uiInteraction', 'subscriptions-changed', 'webdav', 'banners'].forEach((event) => {
|
||||
it(`should deny a logged-in client writing "${event}" to another user's stream`, async () => {
|
||||
expect(await writeAllowed(`victim/${event}`, { foo: 'bar' })).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("should deny writes even to the client's own stream", async () => {
|
||||
expect(await writeAllowed(`userId/force_logout`, undefined)).to.equal(false);
|
||||
});
|
||||
|
||||
it('should accept "video-conference" and delegate authorization to VideoConf.validateAction', async () => {
|
||||
const result = await writeAllowed(`userId/video-conference`, {
|
||||
action: 'call-start',
|
||||
params: { callId: '123', uid: '456', rid: '789' },
|
||||
});
|
||||
|
||||
expect(result).to.be.true;
|
||||
expect(validateActionStub.calledOnceWith('call-start', 'userId', { callId: '123', uid: '456', rid: '789' })).to.be.true;
|
||||
});
|
||||
|
||||
it('should process "media-calls" signals server-side and never broadcast them', async () => {
|
||||
const signal = '{"type":"offer"}';
|
||||
const result = await writeAllowed(`userId/media-calls`, signal);
|
||||
|
||||
expect(result).to.equal(false);
|
||||
expect(processSerializedSignalStub.calledOnceWith('userId', signal)).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('notify-room-users allowWrite', () => {
|
||||
const writeAllowed = (eventName: string, ...args: unknown[]) =>
|
||||
(notifications.streamRoomUsers as unknown as Streamer<'notify-room-users'>).isWriteAllowed(
|
||||
{ userId: 'attacker' } as any,
|
||||
eventName,
|
||||
args,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
countByRoomIdAndUserIdStub.reset();
|
||||
countByRoomIdAndUserIdStub.resolves(1); // attacker is subscribed to the room
|
||||
findSubscriptionsExcludingUserStub.reset();
|
||||
findSubscriptionsExcludingUserStub.returns({ toArray: async () => [{ u: { _id: 'victim' } }] });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.keys(StreamerCentral.instances).forEach((name) => delete StreamerCentral.instances[name]);
|
||||
});
|
||||
|
||||
['force_logout', 'notification', 'message', 'uiInteraction', 'subscriptions-changed', 'webdav', 'banners'].forEach((event) => {
|
||||
it(`should deny and not relay an arbitrary "${event}" event to other room members`, async () => {
|
||||
const emitSpy = sinon.spy(notifications.streamUser, 'emit');
|
||||
|
||||
const result = await writeAllowed(`room1/${event}`, { foo: 'bar' });
|
||||
|
||||
expect(result).to.equal(false);
|
||||
expect(findSubscriptionsExcludingUserStub.called).to.equal(false);
|
||||
expect(emitSpy.called).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not relay anything for a user not subscribed to the room', async () => {
|
||||
countByRoomIdAndUserIdStub.resolves(0);
|
||||
const emitSpy = sinon.spy(notifications.streamUser, 'emit');
|
||||
|
||||
const result = await writeAllowed('room1/video-conference', {
|
||||
action: 'call-start',
|
||||
params: { callId: '123', uid: 'victim', rid: 'room1' },
|
||||
});
|
||||
|
||||
expect(result).to.equal(false);
|
||||
expect(findSubscriptionsExcludingUserStub.called).to.equal(false);
|
||||
expect(emitSpy.called).to.equal(false);
|
||||
});
|
||||
|
||||
['video-conference', 'userData'].forEach((event) => {
|
||||
it(`should relay "${event}" event to other room members`, async () => {
|
||||
const emitSpy = sinon.spy(notifications.streamUser, 'emit');
|
||||
|
||||
const result = await writeAllowed(`room1/${event}`, { foo: 'bar' });
|
||||
|
||||
expect(result).to.equal(false);
|
||||
expect(findSubscriptionsExcludingUserStub.called).to.equal(true);
|
||||
expect(emitSpy.called).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import { expect } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { Streamer, StreamerCentral } from '../../../../../server/modules/streamer/streamer.module';
|
||||
|
||||
class TestStreamer extends Streamer<any> {
|
||||
registerPublication(): void {
|
||||
// no-op for unit test subclass
|
||||
}
|
||||
|
||||
registerMethod(): void {
|
||||
// no-op for unit test subclass
|
||||
}
|
||||
|
||||
changedPayload(): string {
|
||||
return 'payload';
|
||||
}
|
||||
}
|
||||
|
||||
type TestSubscription = {
|
||||
entry: any;
|
||||
connection: Record<string, unknown>;
|
||||
send: sinon.SinonSpy;
|
||||
};
|
||||
|
||||
const makeSubscription = (connectionId: string): TestSubscription => {
|
||||
const send = sinon.spy();
|
||||
const connection = { id: connectionId };
|
||||
|
||||
return {
|
||||
connection,
|
||||
send,
|
||||
entry: {
|
||||
eventName: 'event',
|
||||
subscription: {
|
||||
connection,
|
||||
_session: {
|
||||
socket: { send },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('Streamer.sendToManySubscriptions', () => {
|
||||
let streamer: TestStreamer;
|
||||
let streamerNameSeed = 0;
|
||||
|
||||
beforeEach(() => {
|
||||
streamer = new TestStreamer(`streamer-test-${streamerNameSeed++}`);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
delete StreamerCentral.instances[streamer.name];
|
||||
});
|
||||
|
||||
it('waits for async permission checks before resolving', async () => {
|
||||
const sub = makeSubscription('conn-1');
|
||||
|
||||
const isEmitAllowed = sinon.stub(streamer, 'isEmitAllowed').resolves(true);
|
||||
|
||||
const sendPromise = streamer.sendToManySubscriptions(new Set([sub.entry]), undefined, 'event', [], 'test-msg');
|
||||
|
||||
expect(sub.send.called).to.equal(false);
|
||||
|
||||
await sendPromise;
|
||||
|
||||
expect(isEmitAllowed.calledOnceWithExactly(sub.entry.subscription, 'event')).to.equal(true);
|
||||
expect(sub.send.calledOnceWithExactly('test-msg')).to.equal(true);
|
||||
});
|
||||
|
||||
it('skips origin subscription and sends only to allowed subscriptions', async () => {
|
||||
const originSub = makeSubscription('origin');
|
||||
const allowedSub = makeSubscription('allowed');
|
||||
const deniedSub = makeSubscription('denied');
|
||||
|
||||
const isEmitAllowed = sinon.stub(streamer, 'isEmitAllowed');
|
||||
isEmitAllowed.onFirstCall().resolves(true);
|
||||
isEmitAllowed.onSecondCall().resolves(false);
|
||||
|
||||
await streamer.sendToManySubscriptions(
|
||||
new Set([originSub.entry, allowedSub.entry, deniedSub.entry]),
|
||||
originSub.connection as any,
|
||||
'event',
|
||||
[],
|
||||
'test-msg',
|
||||
);
|
||||
|
||||
expect(originSub.send.called).to.equal(false);
|
||||
expect(allowedSub.send.calledOnceWithExactly('test-msg')).to.equal(true);
|
||||
expect(deniedSub.send.called).to.equal(false);
|
||||
});
|
||||
|
||||
it('continues dispatching to other subscribers when a permission check rejects', async () => {
|
||||
const failingSub = makeSubscription('failing');
|
||||
const successSub = makeSubscription('success');
|
||||
const error = new Error('boom');
|
||||
|
||||
const isEmitAllowed = sinon.stub(streamer, 'isEmitAllowed');
|
||||
isEmitAllowed.onFirstCall().rejects(error);
|
||||
isEmitAllowed.onSecondCall().resolves(true);
|
||||
|
||||
await streamer.sendToManySubscriptions(new Set([failingSub.entry, successSub.entry]), undefined, 'event-name', [], 'test-msg');
|
||||
|
||||
expect(isEmitAllowed.calledTwice).to.equal(true);
|
||||
expect(isEmitAllowed.firstCall.calledWithExactly(failingSub.entry.subscription, 'event-name')).to.equal(true);
|
||||
expect(isEmitAllowed.secondCall.calledWithExactly(successSub.entry.subscription, 'event-name')).to.equal(true);
|
||||
expect(failingSub.send.called).to.equal(false);
|
||||
expect(successSub.send.calledOnceWithExactly('test-msg')).to.equal(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user