diff --git a/apps/meteor/ee/server/api/abac/index.ts b/apps/meteor/ee/server/api/abac/index.ts index 4c2ab21eb5290..3fc89273e80ce 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,7 @@ import { POSTRoomAbacAttributesBodySchema, POSTSingleRoomAbacAttributeBodySchema, PUTRoomAbacAttributeValuesBodySchema, + POSTAbacUsersSyncBodySchema, GenericErrorSchema, GETAbacRoomsListQueryValidator, GETAbacRoomsResponseValidator, @@ -20,6 +22,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 +178,33 @@ 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; + + await LDAPEE.syncUsersAbacAttributes(Users.findUsersByIdentifiers({ usernames, ids, emails, ldapIds })); + + return API.v1.success(); + }, + ) .post( 'abac/attributes', { @@ -239,7 +268,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 +287,6 @@ const abacEndpoints = API.v1 }, async function action() { const { _id } = this.urlParams; - await Abac.deleteAbacAttributeById(_id); return API.v1.success(); }, @@ -279,7 +306,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; } 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..57ad050a78360 100644 --- a/packages/models/src/models/Users.ts +++ b/packages/models/src/models/Users.ts @@ -129,6 +129,38 @@ export class UsersRaw extends BaseRaw> implements IU ]; } + findUsersByIdentifiers( + { usernames, ids, emails, ldapIds }: { usernames?: string[]; ids?: string[]; emails?: string[]; ldapIds?: string[] }, + options: FindOptions = {}, + ): 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 (normalizedIds.length) { + or.push({ _id: { $in: normalizedIds } }); + } + if (normalizedUsernames.length) { + or.push({ username: { $in: normalizedUsernames } }); + } + if (normalizedEmails.length) { + or.push({ 'emails.address': { $in: normalizedEmails } }); + } + if (normalizedLdapIds.length) { + or.push({ 'services.ldap.id': { $in: normalizedLdapIds } }); + } + + const query: Filter = { + active: true, + $or: or, + }; + + return this.find(query, options); + } + /** * @param {string} uid * @param {IRole['_id'][]} roles list of role ids