From bc2f4c820a733c8ecffaa38022d52d6b0ff9bfd5 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 17 Nov 2025 11:58:31 -0600 Subject: [PATCH 1/5] user model func --- .../model-typings/src/models/IUsersModel.ts | 4 +++ packages/models/src/models/Users.ts | 36 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index 2d50bfd377cb9..065a947ee34b3 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -91,6 +91,10 @@ export interface IUsersModel extends IBaseModel { findOneByLDAPId(id: string, attribute?: string): Promise; findOneByAppId(appId: string, options?: FindOptions): Promise; + findUsersByIdentifiers( + params: { usernames?: string[]; ids?: string[]; emails?: string[]; ldapIds?: string[] }, + options?: FindOptions, + ): FindCursor; findLDAPUsers(options?: FindOptions): FindCursor; diff --git a/packages/models/src/models/Users.ts b/packages/models/src/models/Users.ts index f84411ff50953..1314c7e94c716 100644 --- a/packages/models/src/models/Users.ts +++ b/packages/models/src/models/Users.ts @@ -129,6 +129,42 @@ export class UsersRaw extends BaseRaw> implements IU ]; } + findUsersByIdentifiers( + { usernames, ids, emails, ldapIds }: { usernames?: string[]; ids?: string[]; emails?: string[]; ldapIds?: string[] }, + options: FindOptions = {}, + ): FindCursor | void { + usernames = (usernames || []).filter(Boolean); + ids = (ids || []).filter(Boolean); + emails = (emails || []).map((e) => String(e).trim()).filter(Boolean); + ldapIds = (ldapIds || []).filter(Boolean); + + if (!usernames.length && !ids.length && !emails.length && !ldapIds.length) { + return; + } + + const or: Filter[] = []; + + if (ids.length) { + or.push({ _id: { $in: ids } }); + } + if (usernames.length) { + or.push({ username: { $in: usernames } }); + } + if (emails.length) { + or.push({ 'emails.address': { $in: emails } }); + } + if (ldapIds.length) { + or.push({ 'services.ldap.id': { $in: ldapIds } }); + } + + const query: Filter = { + active: true, + $or: or, + }; + + return this.find(query, options); + } + /** * @param {string} uid * @param {IRole['_id'][]} roles list of role ids From 9ffa6124cff9a2609e4f0f4685d9276c591c3ab6 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 17 Nov 2025 12:55:27 -0600 Subject: [PATCH 2/5] force abac sync from endpoint --- apps/meteor/ee/server/api/abac/index.ts | 40 +++++++++++++++++-- apps/meteor/ee/server/api/abac/schemas.ts | 39 ++++++++++++++++++ apps/meteor/ee/server/lib/ldap/Manager.ts | 37 +++++++++++++++++ .../ee/server/local-services/ldap/service.ts | 6 +++ .../ee/server/sdk/types/ILDAPEEService.ts | 4 ++ 5 files changed, 122 insertions(+), 4 deletions(-) diff --git a/apps/meteor/ee/server/api/abac/index.ts b/apps/meteor/ee/server/api/abac/index.ts index 4c2ab21eb5290..ec6c6017bf91d 100644 --- a/apps/meteor/ee/server/api/abac/index.ts +++ b/apps/meteor/ee/server/api/abac/index.ts @@ -1,4 +1,5 @@ import { Abac } from '@rocket.chat/core-services'; +import { Users } from '@rocket.chat/models'; import { validateUnauthorizedErrorResponse } from '@rocket.chat/rest-typings/src/v1/Ajv'; import { @@ -12,6 +13,8 @@ import { POSTRoomAbacAttributesBodySchema, POSTSingleRoomAbacAttributeBodySchema, PUTRoomAbacAttributeValuesBodySchema, + POSTAbacUsersSyncBodySchema, + POSTAbacUsersSyncResponseSchema, GenericErrorSchema, GETAbacRoomsListQueryValidator, GETAbacRoomsResponseValidator, @@ -20,6 +23,7 @@ import { API } from '../../../../app/api/server'; import type { ExtractRoutesFromAPI } from '../../../../app/api/server/ApiClass'; import { getPaginationItems } from '../../../../app/api/server/helpers/getPaginationItems'; import { settings } from '../../../../app/settings/server'; +import { LDAPEE } from '../../sdk'; const abacEndpoints = API.v1 .post( @@ -175,7 +179,38 @@ const abacEndpoints = API.v1 ); }, ) - // create attribute + + .post( + 'abac/users/sync', + { + authRequired: true, + permissionsRequired: ['abac-management'], + license: ['abac', 'ldap-enterprise'], + body: POSTAbacUsersSyncBodySchema, + response: { + 200: GenericSuccessSchema, + 401: validateUnauthorizedErrorResponse, + 400: GenericErrorSchema, + 403: validateUnauthorizedErrorResponse, + }, + }, + async function action() { + if (!settings.get('ABAC_Enabled')) { + throw new Error('error-abac-not-enabled'); + } + + const { usernames, ids, emails, ldapIds } = this.bodyParams; + + const cursor = Users.findUsersByIdentifiers({ usernames, ids, emails, ldapIds }); + if (!cursor) { + throw new Error('error-invalid-params'); + } + + await LDAPEE.syncUsersAbacAttributes(cursor); + + return API.v1.success(); + }, + ) .post( 'abac/attributes', { @@ -239,7 +274,6 @@ const abacEndpoints = API.v1 }, async function action() { const { _id } = this.urlParams; - const result = await Abac.getAbacAttributeById(_id); return API.v1.success(result); }, @@ -259,7 +293,6 @@ const abacEndpoints = API.v1 }, async function action() { const { _id } = this.urlParams; - await Abac.deleteAbacAttributeById(_id); return API.v1.success(); }, @@ -279,7 +312,6 @@ const abacEndpoints = API.v1 }, async function action() { const { key } = this.urlParams; - const inUse = await Abac.isAbacAttributeInUseByKey(key); return API.v1.success({ inUse }); }, diff --git a/apps/meteor/ee/server/api/abac/schemas.ts b/apps/meteor/ee/server/api/abac/schemas.ts index 48f3be8151429..c32338b3e8c25 100644 --- a/apps/meteor/ee/server/api/abac/schemas.ts +++ b/apps/meteor/ee/server/api/abac/schemas.ts @@ -217,6 +217,45 @@ const GenericError = { }, }; +const PostAbacUsersSyncBody = { + type: 'object', + properties: { + usernames: { + type: 'array', + items: { type: 'string', minLength: 1 }, + minItems: 1, + uniqueItems: true, + }, + ids: { + type: 'array', + items: { type: 'string', minLength: 1 }, + minItems: 1, + uniqueItems: true, + }, + emails: { + type: 'array', + items: { type: 'string', minLength: 1 }, + minItems: 1, + uniqueItems: true, + }, + ldapIds: { + type: 'array', + items: { type: 'string', minLength: 1 }, + minItems: 1, + uniqueItems: true, + }, + }, + additionalProperties: false, + anyOf: [{ required: ['usernames'] }, { required: ['ids'] }, { required: ['emails'] }, { required: ['ldapIds'] }], +}; + +export const POSTAbacUsersSyncBodySchema = ajv.compile<{ + usernames?: string[]; + ids?: string[]; + emails?: string[]; + ldapIds?: string[]; +}>(PostAbacUsersSyncBody); + export const GenericErrorSchema = ajv.compile<{ success: boolean; message: string }>(GenericError); const GETAbacRoomsListQuerySchema = { diff --git a/apps/meteor/ee/server/lib/ldap/Manager.ts b/apps/meteor/ee/server/lib/ldap/Manager.ts index 07645249476e7..b505c71a80cb1 100644 --- a/apps/meteor/ee/server/lib/ldap/Manager.ts +++ b/apps/meteor/ee/server/lib/ldap/Manager.ts @@ -3,6 +3,7 @@ import type { ILDAPEntry, IUser, IRoom, IRole, IImportUser, IImportRecord } from import { License } from '@rocket.chat/license'; import { Users, Roles, Subscriptions as SubscriptionsRaw, Rooms } from '@rocket.chat/models'; import type ldapjs from 'ldapjs'; +import type { FindCursor } from 'mongodb'; import type { ImporterAfterImportCallback, @@ -127,6 +128,27 @@ export class LDAPEEManager extends LDAPManager { } } + public static async syncUsersAbacAttributes(users: FindCursor): Promise { + if (!settings.get('LDAP_Enable') || !License.hasModule('abac') || !settings.get('ABAC_Enabled')) { + return; + } + + try { + const ldap = new LDAPConnection(); + await ldap.connect(); + + try { + for await (const user of users) { + await this.syncUserAbacAttribute(ldap, user); + } + } finally { + ldap.disconnect(); + } + } catch (error) { + logger.error(error); + } + } + public static validateLDAPTeamsMappingChanges(json: string): void { if (!json) { return; @@ -733,6 +755,21 @@ export class LDAPEEManager extends LDAPManager { } } + private static async syncUserAbacAttribute(ldap: LDAPConnection, user: IUser): Promise { + const mapping = this.parseJson(settings.get('LDAP_ABAC_AttributeMap')); + if (!mapping) { + logger.error('LDAP to ABAC attribute mapping is not valid JSON'); + return; + } + + const ldapUser = await this.findLDAPUser(ldap, user); + if (!ldapUser) { + return; + } + + await Abac.addSubjectAttributes(user, ldapUser, mapping); + } + private static async findLDAPUser(ldap: LDAPConnection, user: IUser): Promise { if (user.services?.ldap?.id) { return ldap.findOneById(user.services.ldap.id, user.services.ldap.idAttribute); diff --git a/apps/meteor/ee/server/local-services/ldap/service.ts b/apps/meteor/ee/server/local-services/ldap/service.ts index cd08a7db34438..2493ed674be39 100644 --- a/apps/meteor/ee/server/local-services/ldap/service.ts +++ b/apps/meteor/ee/server/local-services/ldap/service.ts @@ -1,4 +1,6 @@ import { ServiceClassInternal } from '@rocket.chat/core-services'; +import type { IUser } from '@rocket.chat/core-typings'; +import type { FindCursor } from 'mongodb'; import { LDAPEEManager } from '../../lib/ldap/Manager'; import type { ILDAPEEService } from '../../sdk/types/ILDAPEEService'; @@ -21,4 +23,8 @@ export class LDAPEEService extends ServiceClassInternal implements ILDAPEEServic async syncAbacAttributes(): Promise { return LDAPEEManager.syncAbacAttributes(); } + + async syncUsersAbacAttributes(users: FindCursor): Promise { + return LDAPEEManager.syncUsersAbacAttributes(users); + } } diff --git a/apps/meteor/ee/server/sdk/types/ILDAPEEService.ts b/apps/meteor/ee/server/sdk/types/ILDAPEEService.ts index dad7c23f0a2f1..1a51c1cbb88b4 100644 --- a/apps/meteor/ee/server/sdk/types/ILDAPEEService.ts +++ b/apps/meteor/ee/server/sdk/types/ILDAPEEService.ts @@ -1,6 +1,10 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import type { FindCursor } from 'mongodb'; + export interface ILDAPEEService { sync(): Promise; syncAvatars(): Promise; syncLogout(): Promise; syncAbacAttributes(): Promise; + syncUsersAbacAttributes(users: FindCursor): Promise; } From 7c876377ee991bf90d31480d576e46c85bfba7fc Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 17 Nov 2025 13:12:23 -0600 Subject: [PATCH 3/5] fix ts --- packages/model-typings/src/models/IUsersModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index 065a947ee34b3..4ea74da3a31b4 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -94,7 +94,7 @@ export interface IUsersModel extends IBaseModel { findUsersByIdentifiers( params: { usernames?: string[]; ids?: string[]; emails?: string[]; ldapIds?: string[] }, options?: FindOptions, - ): FindCursor; + ): FindCursor | void; findLDAPUsers(options?: FindOptions): FindCursor; From 3a16ee88e4914be85c2f540fcf1b61aecaf64377 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 17 Nov 2025 13:48:37 -0600 Subject: [PATCH 4/5] missing removal --- apps/meteor/ee/server/api/abac/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/meteor/ee/server/api/abac/index.ts b/apps/meteor/ee/server/api/abac/index.ts index ec6c6017bf91d..e9359c1fbd9c6 100644 --- a/apps/meteor/ee/server/api/abac/index.ts +++ b/apps/meteor/ee/server/api/abac/index.ts @@ -14,7 +14,6 @@ import { POSTSingleRoomAbacAttributeBodySchema, PUTRoomAbacAttributeValuesBodySchema, POSTAbacUsersSyncBodySchema, - POSTAbacUsersSyncResponseSchema, GenericErrorSchema, GETAbacRoomsListQueryValidator, GETAbacRoomsResponseValidator, From 72caa83bf1aad248f6704ec810fcc127f25c3b76 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 17 Nov 2025 15:43:07 -0600 Subject: [PATCH 5/5] fix --- apps/meteor/ee/server/api/abac/index.ts | 7 +---- .../model-typings/src/models/IUsersModel.ts | 2 +- packages/models/src/models/Users.ts | 30 ++++++++----------- 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/apps/meteor/ee/server/api/abac/index.ts b/apps/meteor/ee/server/api/abac/index.ts index e9359c1fbd9c6..3fc89273e80ce 100644 --- a/apps/meteor/ee/server/api/abac/index.ts +++ b/apps/meteor/ee/server/api/abac/index.ts @@ -200,12 +200,7 @@ const abacEndpoints = API.v1 const { usernames, ids, emails, ldapIds } = this.bodyParams; - const cursor = Users.findUsersByIdentifiers({ usernames, ids, emails, ldapIds }); - if (!cursor) { - throw new Error('error-invalid-params'); - } - - await LDAPEE.syncUsersAbacAttributes(cursor); + await LDAPEE.syncUsersAbacAttributes(Users.findUsersByIdentifiers({ usernames, ids, emails, ldapIds })); return API.v1.success(); }, diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index 4ea74da3a31b4..065a947ee34b3 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -94,7 +94,7 @@ export interface IUsersModel extends IBaseModel { findUsersByIdentifiers( params: { usernames?: string[]; ids?: string[]; emails?: string[]; ldapIds?: string[] }, options?: FindOptions, - ): FindCursor | void; + ): FindCursor; findLDAPUsers(options?: FindOptions): FindCursor; diff --git a/packages/models/src/models/Users.ts b/packages/models/src/models/Users.ts index 1314c7e94c716..57ad050a78360 100644 --- a/packages/models/src/models/Users.ts +++ b/packages/models/src/models/Users.ts @@ -132,29 +132,25 @@ export class UsersRaw extends BaseRaw> implements IU findUsersByIdentifiers( { usernames, ids, emails, ldapIds }: { usernames?: string[]; ids?: string[]; emails?: string[]; ldapIds?: string[] }, options: FindOptions = {}, - ): FindCursor | void { - usernames = (usernames || []).filter(Boolean); - ids = (ids || []).filter(Boolean); - emails = (emails || []).map((e) => String(e).trim()).filter(Boolean); - ldapIds = (ldapIds || []).filter(Boolean); - - if (!usernames.length && !ids.length && !emails.length && !ldapIds.length) { - return; - } + ): FindCursor { + const normalizedIds = (ids ?? []).filter(Boolean); + const normalizedUsernames = (usernames ?? []).filter(Boolean); + const normalizedEmails = (emails ?? []).map((e) => String(e).trim()).filter(Boolean); + const normalizedLdapIds = (ldapIds ?? []).filter(Boolean); const or: Filter[] = []; - if (ids.length) { - or.push({ _id: { $in: ids } }); + if (normalizedIds.length) { + or.push({ _id: { $in: normalizedIds } }); } - if (usernames.length) { - or.push({ username: { $in: usernames } }); + if (normalizedUsernames.length) { + or.push({ username: { $in: normalizedUsernames } }); } - if (emails.length) { - or.push({ 'emails.address': { $in: emails } }); + if (normalizedEmails.length) { + or.push({ 'emails.address': { $in: normalizedEmails } }); } - if (ldapIds.length) { - or.push({ 'services.ldap.id': { $in: ldapIds } }); + if (normalizedLdapIds.length) { + or.push({ 'services.ldap.id': { $in: normalizedLdapIds } }); } const query: Filter = {