Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions apps/meteor/ee/server/api/abac/index.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -12,6 +13,7 @@ import {
POSTRoomAbacAttributesBodySchema,
POSTSingleRoomAbacAttributeBodySchema,
PUTRoomAbacAttributeValuesBodySchema,
POSTAbacUsersSyncBodySchema,
GenericErrorSchema,
GETAbacRoomsListQueryValidator,
GETAbacRoomsResponseValidator,
Expand All @@ -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(
Expand Down Expand Up @@ -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',
{
Expand Down Expand Up @@ -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);
},
Expand All @@ -259,7 +287,6 @@ const abacEndpoints = API.v1
},
async function action() {
const { _id } = this.urlParams;

await Abac.deleteAbacAttributeById(_id);
return API.v1.success();
},
Expand All @@ -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 });
},
Expand Down
39 changes: 39 additions & 0 deletions apps/meteor/ee/server/api/abac/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
37 changes: 37 additions & 0 deletions apps/meteor/ee/server/lib/ldap/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -127,6 +128,27 @@ export class LDAPEEManager extends LDAPManager {
}
}

public static async syncUsersAbacAttributes(users: FindCursor<IUser>): Promise<void> {
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;
Expand Down Expand Up @@ -733,6 +755,21 @@ export class LDAPEEManager extends LDAPManager {
}
}

private static async syncUserAbacAttribute(ldap: LDAPConnection, user: IUser): Promise<void> {
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<ILDAPEntry | undefined> {
if (user.services?.ldap?.id) {
return ldap.findOneById(user.services.ldap.id, user.services.ldap.idAttribute);
Expand Down
6 changes: 6 additions & 0 deletions apps/meteor/ee/server/local-services/ldap/service.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -21,4 +23,8 @@ export class LDAPEEService extends ServiceClassInternal implements ILDAPEEServic
async syncAbacAttributes(): Promise<void> {
return LDAPEEManager.syncAbacAttributes();
}

async syncUsersAbacAttributes(users: FindCursor<IUser>): Promise<void> {
return LDAPEEManager.syncUsersAbacAttributes(users);
}
}
4 changes: 4 additions & 0 deletions apps/meteor/ee/server/sdk/types/ILDAPEEService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { IUser } from '@rocket.chat/core-typings';
import type { FindCursor } from 'mongodb';

export interface ILDAPEEService {
sync(): Promise<void>;
syncAvatars(): Promise<void>;
syncLogout(): Promise<void>;
syncAbacAttributes(): Promise<void>;
syncUsersAbacAttributes(users: FindCursor<IUser>): Promise<void>;
}
4 changes: 4 additions & 0 deletions packages/model-typings/src/models/IUsersModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ export interface IUsersModel extends IBaseModel<IUser> {
findOneByLDAPId<T extends Document = IUser>(id: string, attribute?: string): Promise<T | null>;

findOneByAppId<T extends Document = IUser>(appId: string, options?: FindOptions<IUser>): Promise<T | null>;
findUsersByIdentifiers(
params: { usernames?: string[]; ids?: string[]; emails?: string[]; ldapIds?: string[] },
options?: FindOptions<IUser>,
): FindCursor<IUser>;

findLDAPUsers<T extends Document = IUser>(options?: FindOptions<IUser>): FindCursor<T>;

Expand Down
32 changes: 32 additions & 0 deletions packages/models/src/models/Users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,38 @@ export class UsersRaw extends BaseRaw<IUser, DefaultFields<IUser>> implements IU
];
}

findUsersByIdentifiers(
{ usernames, ids, emails, ldapIds }: { usernames?: string[]; ids?: string[]; emails?: string[]; ldapIds?: string[] },
options: FindOptions<IUser> = {},
): FindCursor<IUser> {
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<IUser>[] = [];

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<IUser> = {
active: true,
$or: or,
};

return this.find(query, options);
}

/**
* @param {string} uid
* @param {IRole['_id'][]} roles list of role ids
Expand Down
Loading