From 3154908458ce6948c077e6ca738a51d7ee9a739c Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 5 Jun 2026 10:48:55 -0600 Subject: [PATCH 01/15] feat(abac): add reevaluateUsers PDP contract and identifier types --- ee/packages/abac/src/pdp/types.ts | 6 ++++++ packages/core-typings/src/Abac.ts | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/ee/packages/abac/src/pdp/types.ts b/ee/packages/abac/src/pdp/types.ts index 39073421d656c..f322cb2ea9895 100644 --- a/ee/packages/abac/src/pdp/types.ts +++ b/ee/packages/abac/src/pdp/types.ts @@ -28,6 +28,8 @@ export interface IGetDecisionBulkResponse { }>; } +export type ReevaluationUser = Pick; + export interface IPolicyDecisionPoint { isAvailable(): Promise; @@ -53,6 +55,10 @@ export interface IPolicyDecisionPoint { rooms: AtLeast[]; }>, ): Promise; room: IRoom }>>; + + reevaluateUsers( + users: ReevaluationUser[], + ): Promise; room: IRoom }>>; } export interface IVirtruPDPConfig { diff --git a/packages/core-typings/src/Abac.ts b/packages/core-typings/src/Abac.ts index 80bff941e9adb..6cc78712e4b1e 100644 --- a/packages/core-typings/src/Abac.ts +++ b/packages/core-typings/src/Abac.ts @@ -13,3 +13,10 @@ export enum AbacObjectType { export const isAbacPdpType = (value: unknown): value is AbacPdpType => value === 'local' || value === 'virtru'; export const isAbacAttributeStoreType = (value: unknown): value is AbacAttributeStoreType => value === 'local' || value === 'virtru'; + +export type AbacUserIdentifiers = { + usernames?: string[]; + ids?: string[]; + emails?: string[]; + ldapIds?: string[]; +}; From 33aeca0ce7276f28ce6de1f235054efdb2f50cd2 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 5 Jun 2026 11:03:08 -0600 Subject: [PATCH 02/15] feat(ldap-enterprise): add id-based ABAC sync and core-services proxy --- apps/meteor/ee/server/local-services/ldap/service.ts | 5 +++++ packages/core-services/src/types/ILDAPEEService.ts | 1 + 2 files changed, 6 insertions(+) diff --git a/apps/meteor/ee/server/local-services/ldap/service.ts b/apps/meteor/ee/server/local-services/ldap/service.ts index 1f756be0d48b8..cba54b832b044 100644 --- a/apps/meteor/ee/server/local-services/ldap/service.ts +++ b/apps/meteor/ee/server/local-services/ldap/service.ts @@ -1,5 +1,6 @@ import { ServiceClassInternal, type ILDAPEEService } from '@rocket.chat/core-services'; import type { IUser } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; import type { FindCursor } from 'mongodb'; import { LDAPEEManager } from '../../lib/ldap/Manager'; @@ -30,4 +31,8 @@ export class LDAPEEService extends ServiceClassInternal implements ILDAPEEServic async syncUsersAbacAttributes(users: FindCursor): Promise { return LDAPEEManager.syncUsersAbacAttributes(users); } + + async syncUsersAbacAttributesByIds(userIds: string[]): Promise { + return LDAPEEManager.syncUsersAbacAttributes(Users.findUsersByIdentifiers({ ids: userIds })); + } } diff --git a/packages/core-services/src/types/ILDAPEEService.ts b/packages/core-services/src/types/ILDAPEEService.ts index 0879ea5c266b1..b8ef92d8c8a04 100644 --- a/packages/core-services/src/types/ILDAPEEService.ts +++ b/packages/core-services/src/types/ILDAPEEService.ts @@ -8,4 +8,5 @@ export interface ILDAPEEService { syncLogout(): Promise; syncAbacAttributes(): Promise; syncUsersAbacAttributes(users: FindCursor): Promise; + syncUsersAbacAttributesByIds(userIds: string[]): Promise; } From 22914f2c499b6b528ab1508429a07a7dc3bdb86b Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 5 Jun 2026 11:08:44 -0600 Subject: [PATCH 03/15] feat(abac): implement VirtruPDP.reevaluateUsers --- ee/packages/abac/src/pdp/VirtruPDP.spec.ts | 32 ++++++++++++++++++++++ ee/packages/abac/src/pdp/VirtruPDP.ts | 31 ++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/ee/packages/abac/src/pdp/VirtruPDP.spec.ts b/ee/packages/abac/src/pdp/VirtruPDP.spec.ts index 3a4b15e722f1f..fb3ce52bd4c5a 100644 --- a/ee/packages/abac/src/pdp/VirtruPDP.spec.ts +++ b/ee/packages/abac/src/pdp/VirtruPDP.spec.ts @@ -470,6 +470,38 @@ describe('VirtruPDP — PDP unreachable (decision call rejects)', () => { }); }); +describe('reevaluateUsers', () => { + const room = { _id: 'r1', abacAttributes: [{ key: 'clearance', values: ['secret'] }] }; + + it('returns no pairs when users have no ABAC rooms', async () => { + const pdp = new VirtruPDP(mkClient()); + const result = await pdp.reevaluateUsers([user({ _id: 'u1', __rooms: [] })]); + expect(result).toEqual([]); + expect(roomsFindPrivateRoomsByIdsWithAbacAttributes).not.toHaveBeenCalled(); + }); + + it('returns non-compliant {user, room} pairs for denied users', async () => { + roomsFindPrivateRoomsByIdsWithAbacAttributes.mockReturnValue(cursor([room])); + const apiCall = jest.fn().mockResolvedValue(denyFor(['r1'])); + const pdp = new VirtruPDP(mkClient({ apiCall })); + + const u = user({ _id: 'u1', __rooms: ['r1'] }); + const result = await pdp.reevaluateUsers([u]); + + expect(result).toEqual([{ user: u, room }]); + }); + + it('returns no pairs when the user is permitted', async () => { + roomsFindPrivateRoomsByIdsWithAbacAttributes.mockReturnValue(cursor([room])); + const apiCall = jest.fn().mockResolvedValue(permitFor(['r1'])); + const pdp = new VirtruPDP(mkClient({ apiCall })); + + const result = await pdp.reevaluateUsers([user({ _id: 'u1', __rooms: ['r1'] })]); + + expect(result).toEqual([]); + }); +}); + describe('VirtruPDP.getHealthStatus', () => { const platformOk = () => okJson({ status: 'SERVING' }); const authOk = () => okJson({}); diff --git a/ee/packages/abac/src/pdp/VirtruPDP.ts b/ee/packages/abac/src/pdp/VirtruPDP.ts index 671e91642daf7..21d10b13625a0 100644 --- a/ee/packages/abac/src/pdp/VirtruPDP.ts +++ b/ee/packages/abac/src/pdp/VirtruPDP.ts @@ -1,11 +1,12 @@ import type { IAbacAttributeDefinition, IRoom, IUser, AtLeast } from '@rocket.chat/core-typings'; import { Rooms, Users } from '@rocket.chat/models'; import { serverFetch } from '@rocket.chat/server-fetch'; +import { isTruthy } from '@rocket.chat/tools'; import pLimit from 'p-limit'; import { OnlyCompliantCanBeAddedToRoomError, PdpHealthCheckError } from '../errors'; import { logger } from '../logger'; -import type { IPolicyDecisionPoint, IGetDecisionBulkRequest, IGetDecisionBulkResponse, IResourceDecision } from './types'; +import type { IPolicyDecisionPoint, IGetDecisionBulkRequest, IGetDecisionBulkResponse, IResourceDecision, ReevaluationUser } from './types'; import { HEALTH_CHECK_TIMEOUT } from '../clients/virtru/VirtruClient'; import type { VirtruClient } from '../clients/virtru/VirtruClient'; import { buildEntityIdentifier, buildAttributeFqns, getUserEntityKey } from '../clients/virtru/identity'; @@ -350,6 +351,34 @@ export class VirtruPDP implements IPolicyDecisionPoint { return nonCompliant; } + async reevaluateUsers( + users: ReevaluationUser[], + ): Promise; room: IRoom }>> { + const roomIds = [...new Set(users.flatMap((u) => u.__rooms ?? []))]; + if (!roomIds.length) { + return []; + } + + const abacRooms = await Rooms.findPrivateRoomsByIdsWithAbacAttributes(roomIds, { + projection: { _id: 1, abacAttributes: 1 }, + }).toArray(); + + const abacRoomById = Object.fromEntries(abacRooms.map((room) => [room._id, room])); + + const entries = users + .map((user) => { + const rooms = (user.__rooms ?? []).map((rid) => abacRoomById[rid]).filter(Boolean); + return rooms.length ? { user, rooms } : null; + }) + .filter(isTruthy); + + if (!entries.length) { + return []; + } + + return this.evaluateUserRooms(entries); + } + async onSubjectAttributesChanged(user: IUser, _next: IAbacAttributeDefinition[]): Promise { const roomIds = user.__rooms; if (!roomIds?.length) { From 1187842a672a834c8c641385864a0aa2242c2e21 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 5 Jun 2026 11:10:10 -0600 Subject: [PATCH 04/15] feat(abac): implement LocalPDP.reevaluateUsers via ldap-enterprise broker --- ee/packages/abac/src/pdp/LocalPDP.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ee/packages/abac/src/pdp/LocalPDP.ts b/ee/packages/abac/src/pdp/LocalPDP.ts index 2b1d3ed51174e..2e4ce3d14249e 100644 --- a/ee/packages/abac/src/pdp/LocalPDP.ts +++ b/ee/packages/abac/src/pdp/LocalPDP.ts @@ -1,9 +1,10 @@ import type { IAbacAttributeDefinition, IRoom, AtLeast, IUser } from '@rocket.chat/core-typings'; +import { LDAPEnterprise } from '@rocket.chat/core-services'; import { Rooms, Users } from '@rocket.chat/models'; import { OnlyCompliantCanBeAddedToRoomError } from '../errors'; import { buildCompliantConditions, buildNonCompliantConditions, buildRoomNonCompliantConditionsFromSubject } from '../helper'; -import type { IPolicyDecisionPoint } from './types'; +import type { IPolicyDecisionPoint, ReevaluationUser } from './types'; export class LocalPDP implements IPolicyDecisionPoint { async isAvailable(): Promise { @@ -81,6 +82,13 @@ export class LocalPDP implements IPolicyDecisionPoint { throw new Error('evaluateUserRooms is not implemented for LocalPDP'); } + async reevaluateUsers( + users: ReevaluationUser[], + ): Promise; room: IRoom }>> { + await LDAPEnterprise.syncUsersAbacAttributesByIds(users.map((user) => user._id)); + return []; + } + async checkUsernamesMatchAttributes(usernames: string[], attributes: IAbacAttributeDefinition[], _object: IRoom): Promise { const nonCompliantUsersFromList = await Users.find( { From 475cb3ad90047d808c6a87cb03d9301ceef7ba6c Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 5 Jun 2026 11:19:10 -0600 Subject: [PATCH 05/15] feat(abac): add AbacService.reevaluateUsers dispatch --- ee/packages/abac/src/index.ts | 22 +++++++++ ee/packages/abac/src/service.spec.ts | 48 +++++++++++++++++++ .../core-services/src/types/IAbacService.ts | 2 + 3 files changed, 72 insertions(+) diff --git a/ee/packages/abac/src/index.ts b/ee/packages/abac/src/index.ts index 1b73bb16b49a1..50069c994db3f 100644 --- a/ee/packages/abac/src/index.ts +++ b/ee/packages/abac/src/index.ts @@ -12,6 +12,7 @@ import type { AbacAuditReason, AbacAttributeStoreType, AbacPdpType, + AbacUserIdentifiers, } from '@rocket.chat/core-typings'; import { Rooms, AbacAttributes, Users, Subscriptions } from '@rocket.chat/models'; import { escapeRegExp } from '@rocket.chat/string-helpers'; @@ -956,6 +957,27 @@ export class AbacService extends ServiceClass implements IAbacService { logger.error({ msg: 'Failed to evaluate room membership', err }); } } + + async reevaluateUsers(identifiers: AbacUserIdentifiers): Promise { + if (!this.pdp || !(await this.pdp.isAvailable())) { + return; + } + + const users = await Users.findUsersByIdentifiers(identifiers, { + projection: { _id: 1, emails: 1, username: 1, __rooms: 1 }, + }).toArray(); + + if (!users.length) { + return; + } + + try { + const nonCompliant = await this.pdp.reevaluateUsers(users); + await Promise.all(nonCompliant.map(({ user, room }) => limit(() => this.removeUserFromRoom(room, user as IUser, 'api')))); + } catch (err) { + logger.error({ msg: 'Failed to reevaluate users', err }); + } + } } export { LocalPDP, VirtruPDP } from './pdp'; diff --git a/ee/packages/abac/src/service.spec.ts b/ee/packages/abac/src/service.spec.ts index 49a5726b5ad35..f815171579011 100644 --- a/ee/packages/abac/src/service.spec.ts +++ b/ee/packages/abac/src/service.spec.ts @@ -59,6 +59,8 @@ const mockCreateAuditServerEvent = jest.fn(); const mockRoomsFindAllPrivateAbac = jest.fn(); const mockUsersFindActiveByRoomIds = jest.fn(); const mockRoomRemoveUserFromRoom = jest.fn(); +const mockUsersFindUsersByIdentifiers = jest.fn(); +const mockLdapSyncByIds = jest.fn(); jest.mock('@rocket.chat/models', () => ({ Rooms: { @@ -90,6 +92,7 @@ jest.mock('@rocket.chat/models', () => ({ Users: { find: (...args: any[]) => mockUsersFind(...args), findActiveByRoomIds: (...args: any[]) => mockUsersFindActiveByRoomIds(...args), + findUsersByIdentifiers: (...args: any[]) => mockUsersFindUsersByIdentifiers(...args), setAbacAttributesById: (...args: any[]) => mockUsersSetAbacAttributesById(...args), unsetAbacAttributesById: (...args: any[]) => mockUsersUnsetAbacAttributesById(...args), findOneAndUpdate: (...args: any[]) => mockUsersUpdateOne(...args), @@ -116,6 +119,9 @@ jest.mock('@rocket.chat/core-services', () => { Room: { removeUserFromRoom: (...args: any[]) => mockRoomRemoveUserFromRoom(...args), }, + LDAPEnterprise: { + syncUsersAbacAttributesByIds: (...args: any[]) => mockLdapSyncByIds(...args), + }, api: { broadcast: jest.fn(), }, @@ -1983,4 +1989,46 @@ describe('AbacService (unit)', () => { expect(pdpStrategySpy).toHaveBeenCalledWith('local'); }); }); + + describe('reevaluateUsers', () => { + const usersCursor = (items: any[]) => ({ toArray: () => Promise.resolve(items) }); + + it('local PDP: forwards resolved user ids to the LDAP broker and removes nothing', async () => { + service.setPdpStrategy('local'); + mockUsersFindUsersByIdentifiers.mockReturnValue(usersCursor([{ _id: 'u1' }, { _id: 'u2' }])); + + await service.reevaluateUsers({ usernames: ['bob'] }); + + expect(mockUsersFindUsersByIdentifiers).toHaveBeenCalledWith( + { usernames: ['bob'] }, + { projection: { _id: 1, emails: 1, username: 1, __rooms: 1 } }, + ); + expect(mockLdapSyncByIds).toHaveBeenCalledWith(['u1', 'u2']); + expect(mockRoomRemoveUserFromRoom).not.toHaveBeenCalled(); + }); + + it('virtru PDP: removes the non-compliant pairs the PDP returns', async () => { + service.setPdpStrategy('virtru'); + const u1 = { _id: 'u1', emails: [{ address: 'u1@x.com' }], username: 'u1' }; + const room = { _id: 'r1', abacAttributes: [] }; + mockUsersFindUsersByIdentifiers.mockReturnValue(usersCursor([u1])); + mockRoomRemoveUserFromRoom.mockResolvedValue(undefined); + jest.spyOn((service as any).pdp, 'isAvailable').mockResolvedValue(true); + jest.spyOn((service as any).pdp, 'reevaluateUsers').mockResolvedValue([{ user: u1, room }]); + + await service.reevaluateUsers({ ids: ['u1'] }); + + expect(mockRoomRemoveUserFromRoom).toHaveBeenCalledTimes(1); + }); + + it('no-ops when no users match', async () => { + service.setPdpStrategy('local'); + mockUsersFindUsersByIdentifiers.mockReturnValue(usersCursor([])); + + await service.reevaluateUsers({ ids: ['missing'] }); + + expect(mockLdapSyncByIds).not.toHaveBeenCalled(); + expect(mockRoomRemoveUserFromRoom).not.toHaveBeenCalled(); + }); + }); }); diff --git a/packages/core-services/src/types/IAbacService.ts b/packages/core-services/src/types/IAbacService.ts index 4d558d50d85d1..3eac9e59b8351 100644 --- a/packages/core-services/src/types/IAbacService.ts +++ b/packages/core-services/src/types/IAbacService.ts @@ -7,6 +7,7 @@ import type { AbacAccessOperation, AbacObjectType, ILDAPEntry, + AbacUserIdentifiers, } from '@rocket.chat/core-typings'; export type AbacActor = Pick; @@ -49,6 +50,7 @@ export interface IAbacService { ): Promise; addSubjectAttributes(user: IUser, ldapUser: ILDAPEntry, map: Record, actor: AbacActor | undefined): Promise; evaluateRoomMembership(): Promise; + reevaluateUsers(identifiers: AbacUserIdentifiers): Promise; getPDPHealth(): Promise; isExternalAttributeStore(): Promise; } From 4493fbdc5a78330c62fb8d16f98dcff9a58bec0b Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 5 Jun 2026 11:23:08 -0600 Subject: [PATCH 06/15] feat(abac): route abac/users/sync through PDP reevaluation --- apps/meteor/ee/server/api/abac/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/meteor/ee/server/api/abac/index.ts b/apps/meteor/ee/server/api/abac/index.ts index 0f1abe77f290d..72629cb2a85d2 100644 --- a/apps/meteor/ee/server/api/abac/index.ts +++ b/apps/meteor/ee/server/api/abac/index.ts @@ -1,8 +1,8 @@ import { AbacAttributeStoreExternalError, getPdpHealthErrorCode } from '@rocket.chat/abac'; -import { Abac, LDAPEnterprise } from '@rocket.chat/core-services'; +import { Abac } from '@rocket.chat/core-services'; import type { AbacActor } from '@rocket.chat/core-services'; import type { IServerEvents, IUser } from '@rocket.chat/core-typings'; -import { ServerEvents, Users } from '@rocket.chat/models'; +import { ServerEvents } from '@rocket.chat/models'; import { validateUnauthorizedErrorResponse } from '@rocket.chat/rest-typings/src/v1/Ajv'; import { convertSubObjectsIntoPaths } from '@rocket.chat/tools'; @@ -209,7 +209,7 @@ const abacEndpoints = API.v1 { authRequired: true, permissionsRequired: ['abac-management', 'manage-abac-admin-room-attributes'], - license: ['abac', 'ldap-enterprise'], + license: ['abac'], body: POSTAbacUsersSyncBodySchema, response: { 200: GenericSuccessSchema, @@ -225,7 +225,7 @@ const abacEndpoints = API.v1 const { usernames, ids, emails, ldapIds } = this.bodyParams; - await LDAPEnterprise.syncUsersAbacAttributes(Users.findUsersByIdentifiers({ usernames, ids, emails, ldapIds })); + await Abac.reevaluateUsers({ usernames, ids, emails, ldapIds }); return API.v1.success(); }, From de9e5caa409987c28a946408503f7185641275e9 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 5 Jun 2026 11:30:50 -0600 Subject: [PATCH 07/15] test(abac): cover strategy-agnostic abac/users/sync endpoint --- apps/meteor/tests/end-to-end/api/abac.ts | 32 ++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/apps/meteor/tests/end-to-end/api/abac.ts b/apps/meteor/tests/end-to-end/api/abac.ts index 71a71131d684e..3ad2e0bc11df1 100644 --- a/apps/meteor/tests/end-to-end/api/abac.ts +++ b/apps/meteor/tests/end-to-end/api/abac.ts @@ -1451,6 +1451,17 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I }); }); + it('POST /abac/users/sync should fail with error-abac-not-enabled', async () => { + await request + .post(`${v1}/abac/users/sync`) + .set(credentials) + .send({ usernames: [] }) + .expect(400) + .expect((res) => { + expect(res.body.error).to.include('error-abac-not-enabled'); + }); + }); + after(async () => { await updateSetting('ABAC_Enabled', true); }); @@ -1832,6 +1843,27 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I }); }); + describe('POST /abac/users/sync (strategy-agnostic)', () => { + before(async () => { + await updateSetting('ABAC_Enabled', true); + }); + + after(async () => { + await updateSetting('ABAC_Enabled', false); + }); + + it('responds 200 with success:true when ABAC_Enabled=true and PDP type=local (empty usernames)', async () => { + await request + .post(`${v1}/abac/users/sync`) + .set(credentials) + .send({ usernames: [] }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }); + }); + }); + describe('Room access (invite, addition)', () => { let roomWithoutAttr: IRoom; let roomWithAttr: IRoom; From 3a820f36d8b385dcc57f22304e774c269ee4f125 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 5 Jun 2026 11:41:08 -0600 Subject: [PATCH 08/15] style(abac): lint/format reevaluateUsers signatures --- ee/packages/abac/src/pdp/LocalPDP.ts | 6 ++---- ee/packages/abac/src/pdp/VirtruPDP.ts | 4 +--- ee/packages/abac/src/pdp/types.ts | 4 +--- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/ee/packages/abac/src/pdp/LocalPDP.ts b/ee/packages/abac/src/pdp/LocalPDP.ts index 2e4ce3d14249e..5dbf93fa2a0ed 100644 --- a/ee/packages/abac/src/pdp/LocalPDP.ts +++ b/ee/packages/abac/src/pdp/LocalPDP.ts @@ -1,5 +1,5 @@ -import type { IAbacAttributeDefinition, IRoom, AtLeast, IUser } from '@rocket.chat/core-typings'; import { LDAPEnterprise } from '@rocket.chat/core-services'; +import type { IAbacAttributeDefinition, IRoom, AtLeast, IUser } from '@rocket.chat/core-typings'; import { Rooms, Users } from '@rocket.chat/models'; import { OnlyCompliantCanBeAddedToRoomError } from '../errors'; @@ -82,9 +82,7 @@ export class LocalPDP implements IPolicyDecisionPoint { throw new Error('evaluateUserRooms is not implemented for LocalPDP'); } - async reevaluateUsers( - users: ReevaluationUser[], - ): Promise; room: IRoom }>> { + async reevaluateUsers(users: ReevaluationUser[]): Promise; room: IRoom }>> { await LDAPEnterprise.syncUsersAbacAttributesByIds(users.map((user) => user._id)); return []; } diff --git a/ee/packages/abac/src/pdp/VirtruPDP.ts b/ee/packages/abac/src/pdp/VirtruPDP.ts index 21d10b13625a0..0e5272eafd40f 100644 --- a/ee/packages/abac/src/pdp/VirtruPDP.ts +++ b/ee/packages/abac/src/pdp/VirtruPDP.ts @@ -351,9 +351,7 @@ export class VirtruPDP implements IPolicyDecisionPoint { return nonCompliant; } - async reevaluateUsers( - users: ReevaluationUser[], - ): Promise; room: IRoom }>> { + async reevaluateUsers(users: ReevaluationUser[]): Promise; room: IRoom }>> { const roomIds = [...new Set(users.flatMap((u) => u.__rooms ?? []))]; if (!roomIds.length) { return []; diff --git a/ee/packages/abac/src/pdp/types.ts b/ee/packages/abac/src/pdp/types.ts index f322cb2ea9895..eb3bc4e5cf573 100644 --- a/ee/packages/abac/src/pdp/types.ts +++ b/ee/packages/abac/src/pdp/types.ts @@ -56,9 +56,7 @@ export interface IPolicyDecisionPoint { }>, ): Promise; room: IRoom }>>; - reevaluateUsers( - users: ReevaluationUser[], - ): Promise; room: IRoom }>>; + reevaluateUsers(users: ReevaluationUser[]): Promise; room: IRoom }>>; } export interface IVirtruPDPConfig { From 8bba69ccd854696baf28028be2f61609853cf4a7 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 5 Jun 2026 11:42:26 -0600 Subject: [PATCH 09/15] test(abac): use non-empty identifiers in abac/users/sync e2e tests --- apps/meteor/tests/end-to-end/api/abac.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/abac.ts b/apps/meteor/tests/end-to-end/api/abac.ts index 3ad2e0bc11df1..49e9cb012d40f 100644 --- a/apps/meteor/tests/end-to-end/api/abac.ts +++ b/apps/meteor/tests/end-to-end/api/abac.ts @@ -1455,7 +1455,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I await request .post(`${v1}/abac/users/sync`) .set(credentials) - .send({ usernames: [] }) + .send({ ids: ['no-such-user-id'] }) .expect(400) .expect((res) => { expect(res.body.error).to.include('error-abac-not-enabled'); @@ -1852,11 +1852,11 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I await updateSetting('ABAC_Enabled', false); }); - it('responds 200 with success:true when ABAC_Enabled=true and PDP type=local (empty usernames)', async () => { + it('responds 200 with success:true when ABAC_Enabled=true and PDP type=local (no-match id)', async () => { await request .post(`${v1}/abac/users/sync`) .set(credentials) - .send({ usernames: [] }) + .send({ ids: ['no-such-user-id'] }) .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); From 5e6243d6cc572bb47f3d730df264375de94dcad3 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 8 Jun 2026 11:36:05 -0600 Subject: [PATCH 10/15] test(abac): cover Virtru PDP re-evaluation via abac/users/sync --- apps/meteor/tests/end-to-end/api/abac.ts | 134 +++++++++++++++++++++-- 1 file changed, 123 insertions(+), 11 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/abac.ts b/apps/meteor/tests/end-to-end/api/abac.ts index 49e9cb012d40f..ed6e59ce01e5d 100644 --- a/apps/meteor/tests/end-to-end/api/abac.ts +++ b/apps/meteor/tests/end-to-end/api/abac.ts @@ -190,7 +190,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('POST /abac/users/sync should return 403', async () => { await request - .post(`${v1}/abac/users/sync`) + .post('/api/v1/abac/users/sync') .set(credentials) .send({ usernames: ['x'] }) .expect(403); @@ -1453,7 +1453,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('POST /abac/users/sync should fail with error-abac-not-enabled', async () => { await request - .post(`${v1}/abac/users/sync`) + .post('/api/v1/abac/users/sync') .set(credentials) .send({ ids: ['no-such-user-id'] }) .expect(400) @@ -1854,7 +1854,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('responds 200 with success:true when ABAC_Enabled=true and PDP type=local (no-match id)', async () => { await request - .post(`${v1}/abac/users/sync`) + .post('/api/v1/abac/users/sync') .set(credentials) .send({ ids: ['no-such-user-id'] }) .expect(200) @@ -2535,7 +2535,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('should sync ABAC attributes for SOME users via /abac/users/sync', async () => { await request - .post(`${v1}/abac/users/sync`) + .post('/api/v1/abac/users/sync') .set(credentials) .send({ usernames: ['david.scott', 'gene.cernan'], @@ -2565,7 +2565,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('should fail /abac/users/sync when more than 100 usernames are provided', async () => { const usernames = Array.from({ length: 101 }, (_, i) => `user_${i}@example.com`); await request - .post(`${v1}/abac/users/sync`) + .post('/api/v1/abac/users/sync') .set(credentials) .send({ usernames, @@ -2579,7 +2579,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('should fail /abac/users/sync when more than 100 ids are provided', async () => { const ids = Array.from({ length: 101 }, (_, i) => `id_${i}`); await request - .post(`${v1}/abac/users/sync`) + .post('/api/v1/abac/users/sync') .set(credentials) .send({ ids, @@ -2593,7 +2593,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('should fail /abac/users/sync when more than 100 emails are provided', async () => { const emails = Array.from({ length: 101 }, (_, i) => `user_${i}@example.com`); await request - .post(`${v1}/abac/users/sync`) + .post('/api/v1/abac/users/sync') .set(credentials) .send({ emails, @@ -2607,7 +2607,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('should fail /abac/users/sync when more than 100 ldapIds are provided', async () => { const ldapIds = Array.from({ length: 101 }, (_, i) => `ldap_${i}`); await request - .post(`${v1}/abac/users/sync`) + .post('/api/v1/abac/users/sync') .set(credentials) .send({ ldapIds, @@ -2621,7 +2621,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('should succeed /abac/users/sync when exactly 100 usernames are provided (boundary)', async () => { const usernames = Array.from({ length: 100 }, (_, i) => `boundary_user_${i}`); await request - .post(`${v1}/abac/users/sync`) + .post('/api/v1/abac/users/sync') .set(credentials) .send({ usernames, @@ -2699,7 +2699,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I expect(sergeiInitialAttrs[0].values).to.include(initialDept); await request - .post(`${v1}/abac/users/sync`) + .post('/api/v1/abac/users/sync') .set(credentials) .send({ usernames: ['david.scott', 'sergei.krikalev'], @@ -3456,6 +3456,118 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I }); }); + describe('Re-evaluation via POST /abac/users/sync', () => { + describe('PDP DENY removes the synced user', () => { + let room: IRoom; + let user: IUser; + const username = `abac-sync-deny-${Date.now()}`; + const email = `${username}@rocket.chat`; + + before(async function () { + this.timeout(15000); + + user = await createUser({ username, email }); + room = (await createRoom({ type: 'p', name: `extpdp-sync-deny-${Date.now()}` })).body.group; + await request + .post('/api/v1/groups.invite') + .set(credentials) + .send({ roomId: room._id, usernames: [user.username] }) + .expect(200); + + await mockServerReset(); + await seedDefaultMocks(); + await seedBulkDecisionByEntity([adminEmail, email], 'DECISION_DENY'); + + await request + .post(`/api/v1/abac/rooms/${room._id}/attributes/${attrKey}`) + .set(credentials) + .send({ values: ['alpha'] }) + .expect(200); + }); + + after(async () => { + await Promise.all([deleteRoom({ type: 'p', roomId: room._id }), deleteUser(user)]); + }); + + it('keeps the user before re-evaluation', async () => { + const res = await request.get('/api/v1/groups.members').set(credentials).query({ roomId: room._id }).expect(200); + const usernames = res.body.members.map((m: IUser) => m.username); + expect(usernames).to.include(user.username); + }); + + it('removes the user when the Virtru PDP returns DENY', async () => { + await mockServerReset(); + await seedDefaultMocks(); + await seedBulkDecisionByEntity([adminEmail], 'DECISION_DENY'); + + await request + .post('/api/v1/abac/users/sync') + .set(credentials) + .send({ usernames: [user.username] }) + .expect(200); + + const res = await request.get('/api/v1/groups.members').set(credentials).query({ roomId: room._id }).expect(200); + const usernames = res.body.members.map((m: IUser) => m.username); + expect(usernames).to.not.include(user.username); + }); + + it('keeps the room creator (permitted) after re-evaluation', async () => { + const res = await request.get('/api/v1/groups.members').set(credentials).query({ roomId: room._id }).expect(200); + const memberIds = res.body.members.map((m: IUser) => m._id); + expect(memberIds).to.include(credentials['X-User-Id']); + }); + }); + + describe('PDP PERMIT keeps the synced user', () => { + let room: IRoom; + let user: IUser; + const username = `abac-sync-permit-${Date.now()}`; + const email = `${username}@rocket.chat`; + + before(async function () { + this.timeout(15000); + + user = await createUser({ username, email }); + room = (await createRoom({ type: 'p', name: `extpdp-sync-permit-${Date.now()}` })).body.group; + await request + .post('/api/v1/groups.invite') + .set(credentials) + .send({ roomId: room._id, usernames: [user.username] }) + .expect(200); + + await mockServerReset(); + await seedDefaultMocks(); + await seedBulkDecisionByEntity([adminEmail, email], 'DECISION_DENY'); + + await request + .post(`/api/v1/abac/rooms/${room._id}/attributes/${attrKey}`) + .set(credentials) + .send({ values: ['alpha'] }) + .expect(200); + }); + + after(async () => { + await Promise.all([deleteRoom({ type: 'p', roomId: room._id }), deleteUser(user)]); + }); + + it('keeps the user when the Virtru PDP returns PERMIT', async () => { + await mockServerReset(); + await seedDefaultMocks(); + await seedBulkDecisionByEntity([adminEmail, email], 'DECISION_DENY'); + + await request + .post('/api/v1/abac/users/sync') + .set(credentials) + .send({ usernames: [user.username] }) + .expect(200); + + const res = await request.get('/api/v1/groups.members').set(credentials).query({ roomId: room._id }).expect(200); + const usernames = res.body.members.map((m: IUser) => m.username); + expect(usernames).to.include(user.username); + }); + }); + }); + describe('[GET] /abac/pdp/health', () => { beforeEach(async () => { await mockServerReset(); @@ -3977,7 +4089,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('POST /abac/users/sync is NOT blocked by external attribute store (no error-abac-attribute-store-external)', async () => { const res = await request - .post(`${v1}/abac/users/sync`) + .post('/api/v1/abac/users/sync') .set(credentials) .send({ usernames: ['no-such-user-vstore'] }); expect(res.body?.error).to.not.equal('error-abac-attribute-store-external'); From be7a6970a7fc11b5f9592dd085f3ba35b8e37e16 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 8 Jun 2026 12:56:46 -0600 Subject: [PATCH 11/15] test(abac): use api() base-path helper for sync/re-evaluation requests --- apps/meteor/tests/end-to-end/api/abac.ts | 44 ++++++++++++------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/abac.ts b/apps/meteor/tests/end-to-end/api/abac.ts index ed6e59ce01e5d..c0eaef4208b69 100644 --- a/apps/meteor/tests/end-to-end/api/abac.ts +++ b/apps/meteor/tests/end-to-end/api/abac.ts @@ -4,7 +4,7 @@ import { expect } from 'chai'; import { before, after, describe, it } from 'mocha'; import { MongoClient } from 'mongodb'; -import { getCredentials, request, credentials, methodCall } from '../../data/api-data'; +import { api, getCredentials, request, credentials, methodCall } from '../../data/api-data'; import { sleep } from '../../data/livechat/utils'; import { mockServerHealthy, @@ -190,7 +190,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('POST /abac/users/sync should return 403', async () => { await request - .post('/api/v1/abac/users/sync') + .post(api('abac/users/sync')) .set(credentials) .send({ usernames: ['x'] }) .expect(403); @@ -1453,7 +1453,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('POST /abac/users/sync should fail with error-abac-not-enabled', async () => { await request - .post('/api/v1/abac/users/sync') + .post(api('abac/users/sync')) .set(credentials) .send({ ids: ['no-such-user-id'] }) .expect(400) @@ -1854,7 +1854,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('responds 200 with success:true when ABAC_Enabled=true and PDP type=local (no-match id)', async () => { await request - .post('/api/v1/abac/users/sync') + .post(api('abac/users/sync')) .set(credentials) .send({ ids: ['no-such-user-id'] }) .expect(200) @@ -2535,7 +2535,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('should sync ABAC attributes for SOME users via /abac/users/sync', async () => { await request - .post('/api/v1/abac/users/sync') + .post(api('abac/users/sync')) .set(credentials) .send({ usernames: ['david.scott', 'gene.cernan'], @@ -2565,7 +2565,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('should fail /abac/users/sync when more than 100 usernames are provided', async () => { const usernames = Array.from({ length: 101 }, (_, i) => `user_${i}@example.com`); await request - .post('/api/v1/abac/users/sync') + .post(api('abac/users/sync')) .set(credentials) .send({ usernames, @@ -2579,7 +2579,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('should fail /abac/users/sync when more than 100 ids are provided', async () => { const ids = Array.from({ length: 101 }, (_, i) => `id_${i}`); await request - .post('/api/v1/abac/users/sync') + .post(api('abac/users/sync')) .set(credentials) .send({ ids, @@ -2593,7 +2593,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('should fail /abac/users/sync when more than 100 emails are provided', async () => { const emails = Array.from({ length: 101 }, (_, i) => `user_${i}@example.com`); await request - .post('/api/v1/abac/users/sync') + .post(api('abac/users/sync')) .set(credentials) .send({ emails, @@ -2607,7 +2607,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('should fail /abac/users/sync when more than 100 ldapIds are provided', async () => { const ldapIds = Array.from({ length: 101 }, (_, i) => `ldap_${i}`); await request - .post('/api/v1/abac/users/sync') + .post(api('abac/users/sync')) .set(credentials) .send({ ldapIds, @@ -2621,7 +2621,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('should succeed /abac/users/sync when exactly 100 usernames are provided (boundary)', async () => { const usernames = Array.from({ length: 100 }, (_, i) => `boundary_user_${i}`); await request - .post('/api/v1/abac/users/sync') + .post(api('abac/users/sync')) .set(credentials) .send({ usernames, @@ -2699,7 +2699,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I expect(sergeiInitialAttrs[0].values).to.include(initialDept); await request - .post('/api/v1/abac/users/sync') + .post(api('abac/users/sync')) .set(credentials) .send({ usernames: ['david.scott', 'sergei.krikalev'], @@ -3469,7 +3469,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I user = await createUser({ username, email }); room = (await createRoom({ type: 'p', name: `extpdp-sync-deny-${Date.now()}` })).body.group; await request - .post('/api/v1/groups.invite') + .post(api('groups.invite')) .set(credentials) .send({ roomId: room._id, usernames: [user.username] }) .expect(200); @@ -3479,7 +3479,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I await seedBulkDecisionByEntity([adminEmail, email], 'DECISION_DENY'); await request - .post(`/api/v1/abac/rooms/${room._id}/attributes/${attrKey}`) + .post(api(`abac/rooms/${room._id}/attributes/${attrKey}`)) .set(credentials) .send({ values: ['alpha'] }) .expect(200); @@ -3490,7 +3490,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I }); it('keeps the user before re-evaluation', async () => { - const res = await request.get('/api/v1/groups.members').set(credentials).query({ roomId: room._id }).expect(200); + const res = await request.get(api('groups.members')).set(credentials).query({ roomId: room._id }).expect(200); const usernames = res.body.members.map((m: IUser) => m.username); expect(usernames).to.include(user.username); }); @@ -3501,18 +3501,18 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I await seedBulkDecisionByEntity([adminEmail], 'DECISION_DENY'); await request - .post('/api/v1/abac/users/sync') + .post(api('abac/users/sync')) .set(credentials) .send({ usernames: [user.username] }) .expect(200); - const res = await request.get('/api/v1/groups.members').set(credentials).query({ roomId: room._id }).expect(200); + const res = await request.get(api('groups.members')).set(credentials).query({ roomId: room._id }).expect(200); const usernames = res.body.members.map((m: IUser) => m.username); expect(usernames).to.not.include(user.username); }); it('keeps the room creator (permitted) after re-evaluation', async () => { - const res = await request.get('/api/v1/groups.members').set(credentials).query({ roomId: room._id }).expect(200); + const res = await request.get(api('groups.members')).set(credentials).query({ roomId: room._id }).expect(200); const memberIds = res.body.members.map((m: IUser) => m._id); expect(memberIds).to.include(credentials['X-User-Id']); }); @@ -3530,7 +3530,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I user = await createUser({ username, email }); room = (await createRoom({ type: 'p', name: `extpdp-sync-permit-${Date.now()}` })).body.group; await request - .post('/api/v1/groups.invite') + .post(api('groups.invite')) .set(credentials) .send({ roomId: room._id, usernames: [user.username] }) .expect(200); @@ -3540,7 +3540,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I await seedBulkDecisionByEntity([adminEmail, email], 'DECISION_DENY'); await request - .post(`/api/v1/abac/rooms/${room._id}/attributes/${attrKey}`) + .post(api(`abac/rooms/${room._id}/attributes/${attrKey}`)) .set(credentials) .send({ values: ['alpha'] }) .expect(200); @@ -3556,12 +3556,12 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I await seedBulkDecisionByEntity([adminEmail, email], 'DECISION_DENY'); await request - .post('/api/v1/abac/users/sync') + .post(api('abac/users/sync')) .set(credentials) .send({ usernames: [user.username] }) .expect(200); - const res = await request.get('/api/v1/groups.members').set(credentials).query({ roomId: room._id }).expect(200); + const res = await request.get(api('groups.members')).set(credentials).query({ roomId: room._id }).expect(200); const usernames = res.body.members.map((m: IUser) => m.username); expect(usernames).to.include(user.username); }); @@ -4089,7 +4089,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('POST /abac/users/sync is NOT blocked by external attribute store (no error-abac-attribute-store-external)', async () => { const res = await request - .post('/api/v1/abac/users/sync') + .post(api('abac/users/sync')) .set(credentials) .send({ usernames: ['no-such-user-vstore'] }); expect(res.body?.error).to.not.equal('error-abac-attribute-store-external'); From eb95f27752d15e8622255f05ef83054183505ac8 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 8 Jun 2026 13:11:49 -0600 Subject: [PATCH 12/15] refactor(abac): let PDP.reevaluateUsers return void when it self-handles removal --- ee/packages/abac/src/index.ts | 4 +++- ee/packages/abac/src/pdp/LocalPDP.ts | 3 +-- ee/packages/abac/src/pdp/types.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ee/packages/abac/src/index.ts b/ee/packages/abac/src/index.ts index 50069c994db3f..2a4dbff17492c 100644 --- a/ee/packages/abac/src/index.ts +++ b/ee/packages/abac/src/index.ts @@ -973,7 +973,9 @@ export class AbacService extends ServiceClass implements IAbacService { try { const nonCompliant = await this.pdp.reevaluateUsers(users); - await Promise.all(nonCompliant.map(({ user, room }) => limit(() => this.removeUserFromRoom(room, user as IUser, 'api')))); + if (Array.isArray(nonCompliant) && nonCompliant.length) { + await Promise.all(nonCompliant.map(({ user, room }) => limit(() => this.removeUserFromRoom(room, user as IUser, 'api')))); + } } catch (err) { logger.error({ msg: 'Failed to reevaluate users', err }); } diff --git a/ee/packages/abac/src/pdp/LocalPDP.ts b/ee/packages/abac/src/pdp/LocalPDP.ts index 5dbf93fa2a0ed..6d16d81e16997 100644 --- a/ee/packages/abac/src/pdp/LocalPDP.ts +++ b/ee/packages/abac/src/pdp/LocalPDP.ts @@ -82,9 +82,8 @@ export class LocalPDP implements IPolicyDecisionPoint { throw new Error('evaluateUserRooms is not implemented for LocalPDP'); } - async reevaluateUsers(users: ReevaluationUser[]): Promise; room: IRoom }>> { + async reevaluateUsers(users: ReevaluationUser[]): Promise { await LDAPEnterprise.syncUsersAbacAttributesByIds(users.map((user) => user._id)); - return []; } async checkUsernamesMatchAttributes(usernames: string[], attributes: IAbacAttributeDefinition[], _object: IRoom): Promise { diff --git a/ee/packages/abac/src/pdp/types.ts b/ee/packages/abac/src/pdp/types.ts index eb3bc4e5cf573..a405104ec2d76 100644 --- a/ee/packages/abac/src/pdp/types.ts +++ b/ee/packages/abac/src/pdp/types.ts @@ -56,7 +56,7 @@ export interface IPolicyDecisionPoint { }>, ): Promise; room: IRoom }>>; - reevaluateUsers(users: ReevaluationUser[]): Promise; room: IRoom }>>; + reevaluateUsers(users: ReevaluationUser[]): Promise; room: IRoom }>>; } export interface IVirtruPDPConfig { From 0a2669299770c20e4fc36337e6c3df98810c1eb4 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 8 Jun 2026 14:19:02 -0600 Subject: [PATCH 13/15] fix(abac): re-throw reevaluateUsers failures so abac/users/sync surfaces errors --- ee/packages/abac/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ee/packages/abac/src/index.ts b/ee/packages/abac/src/index.ts index 2a4dbff17492c..047da168c79c3 100644 --- a/ee/packages/abac/src/index.ts +++ b/ee/packages/abac/src/index.ts @@ -978,6 +978,7 @@ export class AbacService extends ServiceClass implements IAbacService { } } catch (err) { logger.error({ msg: 'Failed to reevaluate users', err }); + throw err; } } } From f10526bb2e69ed0c6972e9c3c217f02d2293a001 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 9 Jun 2026 10:00:07 -0600 Subject: [PATCH 14/15] review --- ee/packages/abac/src/pdp/VirtruPDP.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/ee/packages/abac/src/pdp/VirtruPDP.ts b/ee/packages/abac/src/pdp/VirtruPDP.ts index 0e5272eafd40f..86d338e6640c0 100644 --- a/ee/packages/abac/src/pdp/VirtruPDP.ts +++ b/ee/packages/abac/src/pdp/VirtruPDP.ts @@ -357,23 +357,22 @@ export class VirtruPDP implements IPolicyDecisionPoint { return []; } - const abacRooms = await Rooms.findPrivateRoomsByIdsWithAbacAttributes(roomIds, { + const abacRoomCursor = Rooms.findPrivateRoomsByIdsWithAbacAttributes(roomIds, { projection: { _id: 1, abacAttributes: 1 }, - }).toArray(); + }); - const abacRoomById = Object.fromEntries(abacRooms.map((room) => [room._id, room])); + const abacRoomById = new Map(); + for await (const room of abacRoomCursor) { + abacRoomById.set(room._id, room); + } const entries = users .map((user) => { - const rooms = (user.__rooms ?? []).map((rid) => abacRoomById[rid]).filter(Boolean); + const rooms = (user.__rooms ?? []).map((rid) => abacRoomById.get(rid)).filter(isTruthy); return rooms.length ? { user, rooms } : null; }) .filter(isTruthy); - if (!entries.length) { - return []; - } - return this.evaluateUserRooms(entries); } From 5230be5228c39ffc0d599132dbd4419a12398052 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 9 Jun 2026 10:02:31 -0600 Subject: [PATCH 15/15] test --- ee/packages/abac/src/pdp/VirtruPDP.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ee/packages/abac/src/pdp/VirtruPDP.spec.ts b/ee/packages/abac/src/pdp/VirtruPDP.spec.ts index fb3ce52bd4c5a..d869fa685f8ee 100644 --- a/ee/packages/abac/src/pdp/VirtruPDP.spec.ts +++ b/ee/packages/abac/src/pdp/VirtruPDP.spec.ts @@ -481,7 +481,7 @@ describe('reevaluateUsers', () => { }); it('returns non-compliant {user, room} pairs for denied users', async () => { - roomsFindPrivateRoomsByIdsWithAbacAttributes.mockReturnValue(cursor([room])); + roomsFindPrivateRoomsByIdsWithAbacAttributes.mockReturnValue(asyncIterable([room])); const apiCall = jest.fn().mockResolvedValue(denyFor(['r1'])); const pdp = new VirtruPDP(mkClient({ apiCall })); @@ -492,7 +492,7 @@ describe('reevaluateUsers', () => { }); it('returns no pairs when the user is permitted', async () => { - roomsFindPrivateRoomsByIdsWithAbacAttributes.mockReturnValue(cursor([room])); + roomsFindPrivateRoomsByIdsWithAbacAttributes.mockReturnValue(asyncIterable([room])); const apiCall = jest.fn().mockResolvedValue(permitFor(['r1'])); const pdp = new VirtruPDP(mkClient({ apiCall }));