Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8423998
chore: prevent invite links from federated rooms that are not native
ggazzo Sep 16, 2025
1c14896
chore: prevent send message for non native federations rooms
ggazzo Sep 16, 2025
8befb77
chore: update AddUsers component to utilize isRoomNativeFederated for…
ggazzo Sep 16, 2025
11932f8
chore: enhance user addition logic to account for non-native federate…
ggazzo Sep 16, 2025
8697684
chore: update moderator change logic to consider non-native federated…
ggazzo Sep 16, 2025
6d56dd4
chore: refine change owner logic to incorporate checks for non-native…
ggazzo Sep 16, 2025
6e5f6db
chore: update user removal logic to account for non-native federated …
ggazzo Sep 16, 2025
bdea01a
chore: implement composer for invalid version in non-native federated…
ggazzo Sep 16, 2025
aeecfcf
chore: add federation checks to message action components and members…
ggazzo Sep 16, 2025
f98df35
chore: add federation support to RoomsCachedStore and SubscriptionsCa…
ggazzo Sep 16, 2025
6bf35ea
feat: enhance federation support by adding native federated room and …
ggazzo Sep 16, 2025
2ee1b0d
chore: add INativeFederatedMessage interface and related type guard f…
ggazzo Sep 18, 2025
a2c2adc
refactor: update federation action logic to differentiate between nat…
ggazzo Sep 18, 2025
772a086
fix: update type check for federation version in IUser interface to e…
ggazzo Sep 18, 2025
c83b6a8
chore: remove unit tests for BeforeFederationActions as part of code …
ggazzo Sep 18, 2025
f0184b6
fix lint
ggazzo Sep 18, 2025
28c4551
feat: update ComposerFederationInvalidVersion to include external lin…
ggazzo Sep 19, 2025
c487d56
Apply suggestions from code review
ggazzo Sep 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion apps/meteor/client/cachedStores/RoomsCachedStore.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IOmnichannelRoom, IRoom, IRoomWithRetentionPolicy } from '@rocket.chat/core-typings';
import { DEFAULT_SLA_CONFIG, LivechatPriorityWeight } from '@rocket.chat/core-typings';
import { DEFAULT_SLA_CONFIG, isRoomNativeFederated, LivechatPriorityWeight } from '@rocket.chat/core-typings';
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';

import { PrivateCachedStore } from '../lib/cachedStores';
Expand Down Expand Up @@ -53,6 +53,10 @@ class RoomsCachedStore extends PrivateCachedStore<IRoom> {
source: (room as IOmnichannelRoom | undefined)?.source,
queuedAt: (room as IOmnichannelRoom | undefined)?.queuedAt,
federated: room.federated,

...(isRoomNativeFederated(room) && {
federation: room.federation,
}),
...(() => {
const name = room.name || sub.name;
const fname = room.fname || sub.fname || name;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IOmnichannelRoom, IRoomWithRetentionPolicy, ISubscription } from '@rocket.chat/core-typings';
import { DEFAULT_SLA_CONFIG, LivechatPriorityWeight } from '@rocket.chat/core-typings';
import { DEFAULT_SLA_CONFIG, isRoomNativeFederated, LivechatPriorityWeight } from '@rocket.chat/core-typings';
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';

import { PrivateCachedStore } from '../lib/cachedStores';
Expand Down Expand Up @@ -65,6 +65,12 @@ class SubscriptionsCachedStore extends PrivateCachedStore<SubscriptionWithRoom,
source: (room as IOmnichannelRoom | undefined)?.source,
queuedAt: (room as IOmnichannelRoom | undefined)?.queuedAt,
federated: room?.federated,

...(room &&
isRoomNativeFederated(room) && {
federation: room.federation,
}),

lm: subscription.lr ? new Date(Math.max(subscription.lr.getTime(), lastRoomUpdate?.getTime() || 0)) : lastRoomUpdate,
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import type { ITranslatedMessage, IMessage, ISubscription } from '@rocket.chat/core-typings';
import {
type ITranslatedMessage,
type IMessage,
type ISubscription,
isRoomFederated,
isRoomNativeFederated,
} from '@rocket.chat/core-typings';
import { useTranslation } from 'react-i18next';

import { useChat } from '../../../../../views/room/contexts/ChatContext';
import { useRoom } from '../../../../../views/room/contexts/RoomContext';
import { useMessageListAutoTranslate } from '../../../list/MessageListContext';
import MessageToolbarItem from '../../MessageToolbarItem';

Expand All @@ -15,6 +22,15 @@ const QuoteMessageAction = ({ message, subscription }: QuoteMessageActionProps)
const autoTranslateOptions = useMessageListAutoTranslate();
const { t } = useTranslation();

const room = useRoom();

const isFederated = room && isRoomFederated(room);
const isFederationBlocked = isFederated && !isRoomNativeFederated(room);

if (isFederationBlocked) {
return null;
}

if (!chat || !subscription) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { isOmnichannelRoom, type IMessage, type IRoom, type ISubscription } from '@rocket.chat/core-typings';
import {
isOmnichannelRoom,
isRoomFederated,
isRoomNativeFederated,
type IMessage,
type IRoom,
type ISubscription,
} from '@rocket.chat/core-typings';
import { useFeaturePreview } from '@rocket.chat/ui-client';
import { useUser, useEndpoint } from '@rocket.chat/ui-contexts';
import { useCallback } from 'react';
Expand All @@ -25,8 +32,15 @@ const ReactionMessageAction = ({ message, room, subscription }: ReactionMessageA
const { quickReactions, addRecentEmoji } = useEmojiPickerData();
const { t } = useTranslation();

const isFederated = room && isRoomFederated(room);
const isFederationBlocked = isFederated && !isRoomNativeFederated(room);

const enabled = useReactiveValue(
useCallback(() => {
if (isFederationBlocked) {
return false;
}

if (!chat || isOmnichannelRoom(room) || !subscription || message.private || !user) {
return false;
}
Expand All @@ -36,7 +50,7 @@ const ReactionMessageAction = ({ message, room, subscription }: ReactionMessageA
}

return true;
}, [chat, room, subscription, message.private, user]),
}, [chat, room, subscription, message.private, user, isFederationBlocked]),
);

if (!enabled) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { type IMessage, type ISubscription, type IRoom, isOmnichannelRoom } from '@rocket.chat/core-typings';
import {
type IMessage,
type ISubscription,
type IRoom,
isOmnichannelRoom,
isRoomFederated,
isRoomNativeFederated,
} from '@rocket.chat/core-typings';
import { useRouter, useSetting } from '@rocket.chat/ui-contexts';
import { useTranslation } from 'react-i18next';

Expand All @@ -18,6 +25,12 @@ const ReplyInThreadMessageAction = ({ message, room, subscription }: ReplyInThre
if (!threadsEnabled || isOmnichannelRoom(room) || !subscription) {
return null;
}
const isFederated = room && isRoomFederated(room);
const isFederationBlocked = isFederated && !isRoomNativeFederated(room);

if (isFederationBlocked) {
return null;
}

return (
<MessageToolbarItem
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isRoomFederated, isRoomNativeFederated } from '@rocket.chat/core-typings';
import { usePermission } from '@rocket.chat/ui-contexts';
import { useMemo } from 'react';

Expand All @@ -11,11 +12,18 @@ export const useMembersListRoomAction = () => {
const team = !!room.teamMain;
const permittedToViewBroadcastMemberList = usePermission('view-broadcast-member-list', room._id);

const isFederated = room && isRoomFederated(room);
const isFederationBlocked = isFederated && !isRoomNativeFederated(room);

return useMemo((): RoomToolboxActionConfig | undefined => {
if (broadcast && !permittedToViewBroadcastMemberList) {
return undefined;
}

if (isFederationBlocked) {
return undefined;
}

return {
id: 'members-list',
groups: ['channel', 'group', 'team'],
Expand All @@ -24,5 +32,5 @@ export const useMembersListRoomAction = () => {
tabComponent: MemberListRouter,
order: 7,
};
}, [broadcast, permittedToViewBroadcastMemberList, team]);
}, [broadcast, permittedToViewBroadcastMemberList, team, isFederationBlocked]);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isOmnichannelRoom, isRoomFederated, isVoipRoom } from '@rocket.chat/core-typings';
import { isOmnichannelRoom, isRoomFederated, isRoomNativeFederated, isVoipRoom } from '@rocket.chat/core-typings';
import { usePermission } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import { memo } from 'react';
Expand All @@ -8,6 +8,7 @@ import ComposerAnonymous from './ComposerAnonymous';
import ComposerArchived from './ComposerArchived';
import ComposerBlocked from './ComposerBlocked';
import ComposerFederation from './ComposerFederation';
import ComposerFederationInvalidVersion from './ComposerFederation/ComposerFederationInvalidVersion';
import ComposerJoinWithPassword from './ComposerJoinWithPassword';
import type { ComposerMessageProps } from './ComposerMessage';
import ComposerMessage from './ComposerMessage';
Expand Down Expand Up @@ -37,6 +38,8 @@ const ComposerContainer = ({ children, ...props }: ComposerMessageProps): ReactE

const isOmnichannel = isOmnichannelRoom(room);
const isFederation = isRoomFederated(room);

const isFederationBlocked = !isRoomNativeFederated(room);
const isVoip = isVoipRoom(room);

const [isAirGappedRestricted] = useAirGappedRestriction();
Expand All @@ -54,6 +57,10 @@ const ComposerContainer = ({ children, ...props }: ComposerMessageProps): ReactE
}

if (isFederation) {
if (isFederationBlocked) {
return <ComposerFederationInvalidVersion />;
}

return <ComposerFederation {...props} />;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ExternalLink } from '@rocket.chat/ui-client';
import { MessageFooterCallout, MessageFooterCalloutContent } from '@rocket.chat/ui-composer';
import type { ReactElement } from 'react';
import { Trans } from 'react-i18next';

const ComposerFederationInvalidVersion = (): ReactElement => {
return (
<MessageFooterCallout>
<MessageFooterCalloutContent>
<Trans
i18nKey='Federation_Matrix_Federated_Description_invalid_version'
components={{
1: <ExternalLink to='https://go.rocket.chat/i/matrix-federation' />,
}}
/>
</MessageFooterCalloutContent>
</MessageFooterCallout>
);
};

export default ComposerFederationInvalidVersion;
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable complexity */
import { isRoomFederated, type IMessage, type ISubscription } from '@rocket.chat/core-typings';
import { isRoomFederated, isRoomNativeFederated, type IMessage, type ISubscription } from '@rocket.chat/core-typings';
import { useContentBoxSize, useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { useSafeRefCallback } from '@rocket.chat/ui-client';
import {
Expand Down Expand Up @@ -293,10 +293,15 @@ const MessageBox = ({
}

if (isRoomFederated(room)) {
// we are dropping the non native federation for now
if (!isRoomNativeFederated(room)) {
return false;
}

return federationMatrixEnabled;
}
return true;
}, [federationMatrixEnabled, room]),
}, [room, federationMatrixEnabled]),
);

const sizes = useContentBoxSize(textareaRef);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { isRoomFederated } from '@rocket.chat/core-typings';
import { isRoomFederated, isRoomNativeFederated } from '@rocket.chat/core-typings';
import { Field, FieldLabel, Button, ButtonGroup, FieldGroup } from '@rocket.chat/fuselage';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { useToastMessageDispatch, useMethod } from '@rocket.chat/ui-contexts';
Expand Down Expand Up @@ -56,6 +56,10 @@ const AddUsers = ({ rid, onClickBack, reload }: AddUsersProps): ReactElement =>

const addClickHandler = useAddMatrixUsers();

const roomIsFederated = isRoomFederated(room);
// we are dropping the non native federation for now
const isFederationBlocked = room && !isRoomNativeFederated(room);

return (
<ContextualbarDialog>
<ContextualbarHeader>
Expand All @@ -67,12 +71,14 @@ const AddUsers = ({ rid, onClickBack, reload }: AddUsersProps): ReactElement =>
<FieldGroup>
<Field>
<FieldLabel flexGrow={0}>{t('Choose_users')}</FieldLabel>
{isRoomFederated(room) ? (
<Controller
name='users'
control={control}
render={({ field }) => <UserAutoCompleteMultipleFederated {...field} placeholder={t('Choose_users')} />}
/>
{roomIsFederated ? (
!isFederationBlocked && (
<Controller
name='users'
control={control}
render={({ field }) => <UserAutoCompleteMultipleFederated {...field} placeholder={t('Choose_users')} />}
/>
)
) : (
<Controller
name='users'
Expand All @@ -85,19 +91,21 @@ const AddUsers = ({ rid, onClickBack, reload }: AddUsersProps): ReactElement =>
</ContextualbarScrollableContent>
<ContextualbarFooter>
<ButtonGroup stretch>
{isRoomFederated(room) ? (
<Button
primary
disabled={addClickHandler.isPending}
onClick={() =>
addClickHandler.mutate({
users: getValues('users'),
handleSave,
})
}
>
{t('Add_users')}
</Button>
{roomIsFederated ? (
!isFederationBlocked && (
<Button
primary
disabled={addClickHandler.isPending}
onClick={() =>
addClickHandler.mutate({
users: getValues('users'),
handleSave,
})
}
>
{t('Add_users')}
</Button>
)
) : (
<Button primary loading={isSubmitting} disabled={!isDirty} onClick={handleSubmit(handleSave)}>
{t('Add_users')}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IRoom, IUser } from '@rocket.chat/core-typings';
import { isRoomFederated, isDirectMessageRoom, isTeamRoom } from '@rocket.chat/core-typings';
import { isRoomFederated, isDirectMessageRoom, isTeamRoom, isRoomNativeFederated } from '@rocket.chat/core-typings';
import { useEffectEvent, useDebouncedValue, useLocalStorage } from '@rocket.chat/fuselage-hooks';
import { useUserRoom, useAtLeastOnePermission, useUser, usePermission, useUserSubscription } from '@rocket.chat/ui-contexts';
import type { ChangeEvent, MouseEvent, ReactElement } from 'react';
Expand Down Expand Up @@ -35,8 +35,13 @@ const RoomMembersWithData = ({ rid }: { rid: IRoom['_id'] }): ReactElement => {
const hasPermissionToCreateInviteLinks = usePermission('create-invite-links', rid);
const isFederated = room && isRoomFederated(room);

// we are dropping the non native federation for now
const isFederationBlocked = room && !isRoomNativeFederated(room);

const canCreateInviteLinks =
room && user && isFederated ? Federation.canCreateInviteLinks(user, room, subscription) : hasPermissionToCreateInviteLinks;
room && user && isFederated && !isFederationBlocked
? Federation.canCreateInviteLinks(user, room, subscription)
: hasPermissionToCreateInviteLinks;

Comment on lines 41 to 45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Blocked legacy federation still falls back to non‑federated permissions (allows Invite/Add).

When isFederationBlocked is true, we should not fall back to local permissions—deep links can still expose these actions.

-const canCreateInviteLinks =
-  room && user && isFederated && !isFederationBlocked
-    ? Federation.canCreateInviteLinks(user, room, subscription)
-    : hasPermissionToCreateInviteLinks;
+const canCreateInviteLinks =
+  room && user && isFederated
+    ? (!isFederationBlocked && Federation.canCreateInviteLinks(user, room, subscription))
+    : hasPermissionToCreateInviteLinks;
 
-const canAddUsers =
-  room && user && isFederated && !isFederationBlocked
-    ? Federation.isEditableByTheUser(user, room, subscription)
-    : hasPermissionToAddUsers;
+const canAddUsers =
+  room && user && isFederated
+    ? (!isFederationBlocked && Federation.isEditableByTheUser(user, room, subscription))
+    : hasPermissionToAddUsers;

Also applies to: 62-66

🤖 Prompt for AI Agents
In
apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersWithData.tsx
around lines 41-45 (and similarly 62-66), the current ternary falls back to
non-federated local permissions when isFederationBlocked is true, which
incorrectly allows invite/add actions; change the logic to explicitly return
false when isFederationBlocked is true, otherwise if federated evaluate
Federation.canCreateInviteLinks(user, room, subscription), and only if not
federated evaluate hasPermissionToCreateInviteLinks; apply the same explicit
isFederationBlocked check to the other block at lines 62-66.

const [state, setState] = useState<{ tab: ROOM_MEMBERS_TABS; userId?: IUser['_id'] }>({
tab: ROOM_MEMBERS_TABS.LIST,
Expand All @@ -54,7 +59,10 @@ const RoomMembersWithData = ({ rid }: { rid: IRoom['_id'] }): ReactElement => {
rid,
);

const canAddUsers = room && user && isFederated ? Federation.isEditableByTheUser(user, room, subscription) : hasPermissionToAddUsers;
const canAddUsers =
room && user && isFederated && !isFederationBlocked
? Federation.isEditableByTheUser(user, room, subscription)
: hasPermissionToAddUsers;

const handleTextChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setText(event.currentTarget.value);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IRoom, IUser } from '@rocket.chat/core-typings';
import { isRoomFederated } from '@rocket.chat/core-typings';
import { isRoomFederated, isRoomNativeFederated } from '@rocket.chat/core-typings';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import {
useTranslation,
Expand Down Expand Up @@ -46,9 +46,11 @@ export const useAddUserAction = (

const roomIsFederated = isRoomFederated(room);

const isFederationBlocked = room && !isRoomNativeFederated(room);

const userCanAdd =
room && user && roomIsFederated
? Federation.isEditableByTheUser(currentUser || undefined, room, subscription)
? !isFederationBlocked && Federation.isEditableByTheUser(currentUser || undefined, room, subscription)
: hasPermissionToAddUsers;

const { roomCanInvite } = getRoomDirectives({ room, showingUserId: uid, userSubscription: subscription });
Expand Down
Loading
Loading