From 6f3c0f4427254e22c11caaf6613a4d9e12a6fe80 Mon Sep 17 00:00:00 2001 From: yash-rajpal Date: Thu, 9 Jul 2026 01:13:03 +0530 Subject: [PATCH 1/8] allow operator usage in user autocomplete query --- apps/meteor/server/api/lib/isValidQuery.ts | 7 ++++++- apps/meteor/server/api/v1/users.ts | 2 +- .../tests/unit/server/api/lib/isValidQuery.spec.ts | 12 ++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/apps/meteor/server/api/lib/isValidQuery.ts b/apps/meteor/server/api/lib/isValidQuery.ts index ef8a0716505cb..69ab0c1a7d8bf 100644 --- a/apps/meteor/server/api/lib/isValidQuery.ts +++ b/apps/meteor/server/api/lib/isValidQuery.ts @@ -33,7 +33,12 @@ const verifyQuery = (query: Query, allowedAttributes: string[], allowedOperation } if (Array.isArray(value)) { - return value.every((v) => verifyQuery(v, allowedAttributes, allowedOperations)); + return value.every((v) => { + if (isRecord(v)) { + return verifyQuery(v, allowedAttributes, allowedOperations); + } + return true; + }); } if (isRecord(value)) { diff --git a/apps/meteor/server/api/v1/users.ts b/apps/meteor/server/api/v1/users.ts index 080f2cb400a1f..53e2290b9181d 100644 --- a/apps/meteor/server/api/v1/users.ts +++ b/apps/meteor/server/api/v1/users.ts @@ -1688,7 +1688,7 @@ API.v1.get( const canViewFullInfo = await hasPermissionAsync(this.userId, 'view-full-other-user-info'); const allowedFields = canViewFullInfo ? [...Object.keys(defaultFields), ...Object.keys(fullFields)] : Object.keys(defaultFields); - if (!isValidQuery(selector.conditions, allowedFields, ['$and', '$ne', '$exists'])) { + if (!isValidQuery(selector.conditions, allowedFields, ['$and', '$ne', '$exists', '$in'])) { throw new Error('error-invalid-query'); } } diff --git a/apps/meteor/tests/unit/server/api/lib/isValidQuery.spec.ts b/apps/meteor/tests/unit/server/api/lib/isValidQuery.spec.ts index a0d2dcd967648..9328743090b69 100644 --- a/apps/meteor/tests/unit/server/api/lib/isValidQuery.spec.ts +++ b/apps/meteor/tests/unit/server/api/lib/isValidQuery.spec.ts @@ -223,4 +223,16 @@ describe('isValidQuery', () => { expect(isValidQuery.errors.length).to.be.equals(1); }); }); + + describe('primitive values in query array', () => { + it('should return true if the query contains primitive array', () => { + const props = ['roles']; + const allowedOps = ['$in']; + const query = { + roles: { $in: ['admin', 'user'] }, + }; + expect(isValidQuery(query, props, allowedOps)).to.be.true; + expect(isValidQuery.errors.length).to.be.equals(0); + }); + }); }); From 1509cd92fa1579d70d0afbbb4a73e4c0c9d2fa20 Mon Sep 17 00:00:00 2001 From: yash-rajpal Date: Thu, 9 Jul 2026 01:13:48 +0530 Subject: [PATCH 2/8] use UserAutoComplete in integrations form --- apps/meteor/client/providers/AuthorizationProvider.tsx | 1 + .../integrations/incoming/IncomingWebhookForm.tsx | 8 +++++--- packages/mock-providers/src/MockedAppRootBuilder.tsx | 1 + .../mock-providers/src/MockedAuthorizationContext.tsx | 1 + packages/ui-contexts/src/AuthorizationContext.ts | 4 +++- packages/ui-contexts/src/hooks/useGetPermission.ts | 10 ++++++++++ packages/ui-contexts/src/index.ts | 1 + 7 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 packages/ui-contexts/src/hooks/useGetPermission.ts diff --git a/apps/meteor/client/providers/AuthorizationProvider.tsx b/apps/meteor/client/providers/AuthorizationProvider.tsx index 8c3d576e45597..450ed83bb0b1e 100644 --- a/apps/meteor/client/providers/AuthorizationProvider.tsx +++ b/apps/meteor/client/providers/AuthorizationProvider.tsx @@ -102,6 +102,7 @@ const AuthorizationProvider = ({ children }: AuthorizationProviderProps) => { ], getRoles: () => Roles.state.records, subscribeToRoles: (callback) => Roles.use.subscribe(callback), + getPermission: (permissionId: string) => Permissions.state.get(permissionId), }), [auth, userId], ); diff --git a/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx b/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx index bf78c8d6cc8ea..20a3a768aa630 100644 --- a/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx +++ b/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx @@ -17,7 +17,8 @@ import { FieldRow, FieldHint, } from '@rocket.chat/fuselage'; -import { useAbsoluteUrl } from '@rocket.chat/ui-contexts'; +import { UserAutoComplete } from '@rocket.chat/ui-client'; +import { useAbsoluteUrl, useGetPermission } from '@rocket.chat/ui-contexts'; import DOMPurify from 'dompurify'; import { useId, useMemo } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; @@ -33,6 +34,7 @@ export type IncomingWebhookFormProps = { webhookData?: Serialized { const { t } = useTranslation(); const absoluteUrl = useAbsoluteUrl(); + const permission = useGetPermission('message-impersonate'); const { control, @@ -202,10 +204,10 @@ const IncomingWebhookForm = ({ webhookData }: IncomingWebhookFormProps) => { control={control} rules={{ required: t('Required_field', { field: t('Post_as') }) }} render={({ field }) => ( - } + conditions={permission?.roles ? { roles: { $in: permission.roles } } : undefined} aria-describedby={`${usernameField}-hint-1 ${usernameField}-hint-2 ${usernameField}-error`} aria-required={true} aria-invalid={Boolean(errors?.username)} diff --git a/packages/mock-providers/src/MockedAppRootBuilder.tsx b/packages/mock-providers/src/MockedAppRootBuilder.tsx index ff4860936ae20..a6dd7b6a0e154 100644 --- a/packages/mock-providers/src/MockedAppRootBuilder.tsx +++ b/packages/mock-providers/src/MockedAppRootBuilder.tsx @@ -241,6 +241,7 @@ export class MockedAppRootBuilder { queryRole: () => [() => () => undefined, () => false], getRoles: () => dummyRolesMap, subscribeToRoles: () => () => undefined, + getPermission: () => undefined, }; })(); diff --git a/packages/mock-providers/src/MockedAuthorizationContext.tsx b/packages/mock-providers/src/MockedAuthorizationContext.tsx index d340bbb3133be..3a75cc0aeccb2 100644 --- a/packages/mock-providers/src/MockedAuthorizationContext.tsx +++ b/packages/mock-providers/src/MockedAuthorizationContext.tsx @@ -24,6 +24,7 @@ export const MockedAuthorizationContext = ({ queryRole: (id: string) => [() => (): void => undefined, (): boolean => roles.includes(id)], getRoles: () => dummyRolesMap, subscribeToRoles: (): (() => void) => (): void => undefined, + getPermission: () => undefined, }} > {children} diff --git a/packages/ui-contexts/src/AuthorizationContext.ts b/packages/ui-contexts/src/AuthorizationContext.ts index 1941391e7ec0e..80c9a73887e8a 100644 --- a/packages/ui-contexts/src/AuthorizationContext.ts +++ b/packages/ui-contexts/src/AuthorizationContext.ts @@ -1,4 +1,4 @@ -import type { IRole, IRoom } from '@rocket.chat/core-typings'; +import type { IRole, IRoom, IPermission } from '@rocket.chat/core-typings'; import type { ObjectId } from 'mongodb'; import { createContext } from 'react'; @@ -24,6 +24,7 @@ export type AuthorizationContextValue = { ): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => boolean]; getRoles(): ReadonlyMap; subscribeToRoles(callback: () => void): () => void; + getPermission(permissionId: string): IPermission | undefined; }; const dummyRolesMap: ReadonlyMap = new Map(); @@ -35,4 +36,5 @@ export const AuthorizationContext = createContext({ queryRole: () => [() => (): void => undefined, (): boolean => false], getRoles: (): ReadonlyMap => dummyRolesMap, subscribeToRoles: (): (() => void) => (): void => undefined, + getPermission: (): IPermission | undefined => undefined, }); diff --git a/packages/ui-contexts/src/hooks/useGetPermission.ts b/packages/ui-contexts/src/hooks/useGetPermission.ts new file mode 100644 index 0000000000000..6e5cfe7dd7d24 --- /dev/null +++ b/packages/ui-contexts/src/hooks/useGetPermission.ts @@ -0,0 +1,10 @@ +import type { IPermission } from '@rocket.chat/core-typings'; +import { useContext } from 'react'; + +import { AuthorizationContext } from '../AuthorizationContext'; + +export const useGetPermission = (permission: string): IPermission | undefined => { + const { getPermission } = useContext(AuthorizationContext); + + return getPermission(permission); +}; diff --git a/packages/ui-contexts/src/index.ts b/packages/ui-contexts/src/index.ts index 7b070cbd6f940..059f1f0b0a1e4 100644 --- a/packages/ui-contexts/src/index.ts +++ b/packages/ui-contexts/src/index.ts @@ -115,3 +115,4 @@ export { useMediaDeviceMicrophonePermission } from './hooks/useMediaDevicePermis export { useWriteStream } from './hooks/useWriteStream'; export { useUserCard } from './hooks/useUserCard'; export type { SubscriptionWithRoom } from './types/SubscriptionWithRoom'; +export { useGetPermission } from './hooks/useGetPermission'; From 8fa0f5626a970f567bc7c2c4d9789736122d8350 Mon Sep 17 00:00:00 2001 From: yash-rajpal Date: Thu, 9 Jul 2026 20:56:48 +0530 Subject: [PATCH 3/8] preselected value in options --- .../UserAutoComplete/UserAutoComplete.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/ui-client/src/components/UserAutoComplete/UserAutoComplete.tsx b/packages/ui-client/src/components/UserAutoComplete/UserAutoComplete.tsx index dbafc807842fc..1659d98e9ba5e 100644 --- a/packages/ui-client/src/components/UserAutoComplete/UserAutoComplete.tsx +++ b/packages/ui-client/src/components/UserAutoComplete/UserAutoComplete.tsx @@ -28,7 +28,21 @@ const UserAutoComplete = ({ value, onChange, ...props }: UserAutoCompleteProps) queryFn: async () => usersAutoCompleteEndpoint(query(debouncedFilter, conditions)), }); - const options = useMemo(() => data?.items.map((user) => ({ value: user.username, label: user.name || user.username })) || [], [data]); + const options = useMemo(() => { + const apiOptions = data?.items.map((user) => ({ value: user.username, label: user.name || user.username })) ?? []; + + if (!value || typeof value !== 'string') { + return apiOptions; + } + + const valueInOptions = apiOptions.find((o) => o.value === value); + + if (valueInOptions) { + return apiOptions; + } + + return [{ value, label: value }, ...apiOptions]; + }, [data, value]); return ( Date: Thu, 9 Jul 2026 21:08:57 +0530 Subject: [PATCH 4/8] refactor --- .../UserAutoComplete/UserAutoComplete.tsx | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/ui-client/src/components/UserAutoComplete/UserAutoComplete.tsx b/packages/ui-client/src/components/UserAutoComplete/UserAutoComplete.tsx index 1659d98e9ba5e..af0a438410d5f 100644 --- a/packages/ui-client/src/components/UserAutoComplete/UserAutoComplete.tsx +++ b/packages/ui-client/src/components/UserAutoComplete/UserAutoComplete.tsx @@ -29,19 +29,14 @@ const UserAutoComplete = ({ value, onChange, ...props }: UserAutoCompleteProps) }); const options = useMemo(() => { - const apiOptions = data?.items.map((user) => ({ value: user.username, label: user.name || user.username })) ?? []; + const items = data?.items.map((user) => ({ value: user.username, label: user.name || user.username })) || []; - if (!value || typeof value !== 'string') { - return apiOptions; - } + // as the label until the real name is fetched. + // eslint-disable-next-line no-nested-ternary + const values = Array.isArray(value) ? value : value ? [value] : []; + const missing = values.filter((v) => !items.some((item) => item.value === v)).map((v) => ({ value: v, label: v })); - const valueInOptions = apiOptions.find((o) => o.value === value); - - if (valueInOptions) { - return apiOptions; - } - - return [{ value, label: value }, ...apiOptions]; + return [...items, ...missing]; }, [data, value]); return ( From b7dfdc63f20be2da24395fd1916f3a10a6d26770 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Thu, 9 Jul 2026 14:29:21 -0300 Subject: [PATCH 5/8] chore: avoid nested ternary --- .../src/components/UserAutoComplete/UserAutoComplete.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/ui-client/src/components/UserAutoComplete/UserAutoComplete.tsx b/packages/ui-client/src/components/UserAutoComplete/UserAutoComplete.tsx index af0a438410d5f..29ebd6f740c47 100644 --- a/packages/ui-client/src/components/UserAutoComplete/UserAutoComplete.tsx +++ b/packages/ui-client/src/components/UserAutoComplete/UserAutoComplete.tsx @@ -29,12 +29,10 @@ const UserAutoComplete = ({ value, onChange, ...props }: UserAutoCompleteProps) }); const options = useMemo(() => { - const items = data?.items.map((user) => ({ value: user.username, label: user.name || user.username })) || []; + const items = data?.items.map((user) => ({ value: user.username, label: user.name || user.username })) ?? []; - // as the label until the real name is fetched. - // eslint-disable-next-line no-nested-ternary - const values = Array.isArray(value) ? value : value ? [value] : []; - const missing = values.filter((v) => !items.some((item) => item.value === v)).map((v) => ({ value: v, label: v })); + const selectedValues = ([] as string[]).concat(value ?? []); + const missing = selectedValues.filter((v) => !items.some((item) => item.value === v)).map((v) => ({ value: v, label: v })); return [...items, ...missing]; }, [data, value]); From 8d1f590ad368055e70230ad4605d82c413a1bf6f Mon Sep 17 00:00:00 2001 From: yash-rajpal Date: Thu, 16 Jul 2026 19:59:16 +0530 Subject: [PATCH 6/8] update tests to use auto select --- apps/meteor/tests/e2e/administration.spec.ts | 2 +- .../tests/e2e/page-objects/admin-integrations.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/meteor/tests/e2e/administration.spec.ts b/apps/meteor/tests/e2e/administration.spec.ts index 0afad3ca97a3a..b79ecbe6eb2e2 100644 --- a/apps/meteor/tests/e2e/administration.spec.ts +++ b/apps/meteor/tests/e2e/administration.spec.ts @@ -442,7 +442,7 @@ test.describe.parallel('administration', () => { await poAdminIntegrations.btnNew.click(); await poAdminIntegrations.inputName.fill(incomingIntegrationName); await poAdminIntegrations.inputPostToChannel.fill('#general'); - await poAdminIntegrations.inputPostAs.fill('rocket.cat'); + await poAdminIntegrations.selectPostAs('rocket.cat'); await poAdminIntegrations.btnSave.click(); await expect(poAdminIntegrations.inputWebhookUrl).not.toHaveValue('Will be available here after saving.'); diff --git a/apps/meteor/tests/e2e/page-objects/admin-integrations.ts b/apps/meteor/tests/e2e/page-objects/admin-integrations.ts index 0013265cf642f..e64c41e99205b 100644 --- a/apps/meteor/tests/e2e/page-objects/admin-integrations.ts +++ b/apps/meteor/tests/e2e/page-objects/admin-integrations.ts @@ -1,10 +1,14 @@ import type { Locator, Page } from '@playwright/test'; import { Admin } from './admin'; +import { Listbox } from './fragments/listbox'; export class AdminIntegrations extends Admin { + readonly listbox: Listbox; + constructor(page: Page) { super(page); + this.listbox = new Listbox(page); } get btnInstructions(): Locator { @@ -27,6 +31,12 @@ export class AdminIntegrations extends Admin { return this.page.getByRole('textbox', { name: 'Post as' }); } + async selectPostAs(name: string) { + await this.inputPostAs.click(); + await this.inputPostAs.fill('rocket.cat'); + await this.listbox.selectOption(name); + } + getIntegrationByName(name: string): Locator { return this.page.getByRole('table', { name: 'Integrations table' }).locator('tr', { hasText: name }); } From 996f632fd2b234d5a23e6771ca78c1716de2e013 Mon Sep 17 00:00:00 2001 From: yash-rajpal Date: Thu, 16 Jul 2026 20:12:31 +0530 Subject: [PATCH 7/8] add api tests --- apps/meteor/tests/end-to-end/api/users.ts | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/apps/meteor/tests/end-to-end/api/users.ts b/apps/meteor/tests/end-to-end/api/users.ts index 69c9e0b863207..a421ac02d5aff 100644 --- a/apps/meteor/tests/end-to-end/api/users.ts +++ b/apps/meteor/tests/end-to-end/api/users.ts @@ -5642,6 +5642,30 @@ describe('[Users]', () => { expect(res.body).to.have.property('status', 'error'); }); }); + + it('should filter results when using allowed $in operator', (done) => { + void request + .get(api('users.autocomplete')) + .set(credentials) + .query({ + selector: JSON.stringify({ + conditions: { + roles: { + $in: ['bot'], + }, + }, + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + + expect(res.body).to.have.property('items').and.to.be.an('array').with.lengthOf(1); + expect(res.body.items[0]).to.have.property('username', 'rocket.cat'); + }) + .end(done); + }); }); describe('[/users.getStatus]', () => { From 75011650b9a6a5566218d2a1039d148c8e05f41f Mon Sep 17 00:00:00 2001 From: yash-rajpal Date: Thu, 16 Jul 2026 20:21:45 +0530 Subject: [PATCH 8/8] oops --- apps/meteor/tests/e2e/page-objects/admin-integrations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/e2e/page-objects/admin-integrations.ts b/apps/meteor/tests/e2e/page-objects/admin-integrations.ts index e64c41e99205b..a51523a8c6aff 100644 --- a/apps/meteor/tests/e2e/page-objects/admin-integrations.ts +++ b/apps/meteor/tests/e2e/page-objects/admin-integrations.ts @@ -33,7 +33,7 @@ export class AdminIntegrations extends Admin { async selectPostAs(name: string) { await this.inputPostAs.click(); - await this.inputPostAs.fill('rocket.cat'); + await this.inputPostAs.fill(name); await this.listbox.selectOption(name); }