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/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 7024b9eed3b4b..770f1182880c2 100644 --- a/apps/meteor/server/api/v1/users.ts +++ b/apps/meteor/server/api/v1/users.ts @@ -1699,7 +1699,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/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..a51523a8c6aff 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(name); + await this.listbox.selectOption(name); + } + getIntegrationByName(name: string): Locator { return this.page.getByRole('table', { name: 'Integrations table' }).locator('tr', { hasText: name }); } 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]', () => { 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); + }); + }); }); 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-client/src/components/UserAutoComplete/UserAutoComplete.tsx b/packages/ui-client/src/components/UserAutoComplete/UserAutoComplete.tsx index fe4090254e429..52fd6490413f6 100644 --- a/packages/ui-client/src/components/UserAutoComplete/UserAutoComplete.tsx +++ b/packages/ui-client/src/components/UserAutoComplete/UserAutoComplete.tsx @@ -28,7 +28,14 @@ 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 items = data?.items.map((user) => ({ value: user.username, label: user.name || user.username })) ?? []; + + 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]); return ( 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';