diff --git a/apps/meteor/app/api/server/v1/teams.ts b/apps/meteor/app/api/server/v1/teams.ts index ee5ec7e273f66..d19520547c86b 100644 --- a/apps/meteor/app/api/server/v1/teams.ts +++ b/apps/meteor/app/api/server/v1/teams.ts @@ -20,6 +20,7 @@ import { eraseRoom } from '../../../../server/lib/eraseRoom'; import { canAccessRoomAsync } from '../../../authorization/server'; import { hasPermissionAsync, hasAtLeastOnePermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { removeUserFromRoom } from '../../../lib/server/functions/removeUserFromRoom'; +import { settings } from '../../../settings/server'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; @@ -234,6 +235,13 @@ API.v1.addRoute( } const canUpdateAny = !!(await hasPermissionAsync(this.userId, 'view-all-team-channels', team.roomId)); + if (settings.get('ABAC_Enabled') && isDefault) { + const room = await Rooms.findOneByIdAndType(roomId, 'p', { projection: { abacAttributes: 1 } }); + if (room?.abacAttributes?.length) { + return API.v1.failure('error-room-is-abac-managed'); + } + } + const room = await Team.updateRoom(this.userId, roomId, isDefault, canUpdateAny); return API.v1.success({ room }); diff --git a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts index 7cbdca852cd6d..c2b6b44e99144 100644 --- a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts +++ b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts @@ -11,6 +11,7 @@ import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { setRoomAvatar } from '../../../lib/server/functions/setRoomAvatar'; import { notifyOnRoomChangedById } from '../../../lib/server/lib/notifyListener'; +import { settings } from '../../../settings/server'; import { saveReactWhenReadOnly } from '../functions/saveReactWhenReadOnly'; import { saveRoomAnnouncement } from '../functions/saveRoomAnnouncement'; import { saveRoomCustomFields } from '../functions/saveRoomCustomFields'; @@ -62,13 +63,19 @@ const hasRetentionPolicy = (room: IRoom & { retention?: any }): room is IRoomWit 'retention' in room && room.retention !== undefined; const validators: RoomSettingsValidators = { - async default({ userId }) { + async default({ userId, room, value }) { if (!(await hasPermissionAsync(userId, 'view-room-administration'))) { throw new Meteor.Error('error-action-not-allowed', 'Viewing room administration is not allowed', { method: 'saveRoomSettings', action: 'Viewing_room_administration', }); } + if (settings.get('ABAC_Enabled') && value && room?.abacAttributes?.length) { + throw new Meteor.Error('error-action-not-allowed', 'Setting an ABAC managed room as default is not allowed', { + method: 'saveRoomSettings', + action: 'Viewing_room_administration', + }); + } }, async featured({ userId }) { if (!(await hasPermissionAsync(userId, 'view-room-administration'))) { diff --git a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts index f5317a7b0e3b3..c1f6e2821e35b 100644 --- a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts +++ b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts @@ -5,6 +5,7 @@ import { Subscriptions } from '@rocket.chat/models'; import { getDefaultChannels } from './getDefaultChannels'; import { callbacks } from '../../../../lib/callbacks'; import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; +import { settings } from '../../../settings/server'; import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; import { notifyOnSubscriptionChangedById } from '../lib/notifyListener'; @@ -13,6 +14,10 @@ export const addUserToDefaultChannels = async function (user: IUser, silenced?: const defaultRooms = await getDefaultChannels(); for await (const room of defaultRooms) { + if (settings.get('ABAC_Enabled') && room?.abacAttributes?.length) { + continue; + } + if (!(await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { projection: { _id: 1 } }))) { const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(user); diff --git a/apps/meteor/tests/end-to-end/api/abac.ts b/apps/meteor/tests/end-to-end/api/abac.ts index 0b506c6875649..5a7e4480b1dde 100644 --- a/apps/meteor/tests/end-to-end/api/abac.ts +++ b/apps/meteor/tests/end-to-end/api/abac.ts @@ -6,6 +6,7 @@ import { before, after, describe, it } from 'mocha'; import { getCredentials, request, credentials } from '../../data/api-data'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; import { createRoom, deleteRoom } from '../../data/rooms.helper'; +import { deleteTeam } from '../../data/teams.helper'; import { password } from '../../data/user'; import { createUser, deleteUser, login } from '../../data/users.helper'; import { IS_EE } from '../../e2e/config/constants'; @@ -396,6 +397,152 @@ import { IS_EE } from '../../e2e/config/constants'; }); }); + describe('Default and Team Default Room Restrictions', () => { + let privateDefaultRoomId: string; + let teamId: string; + let teamPrivateRoomId: string; + let teamDefaultRoomId: string; + const localAbacKey = `default_team_test_${Date.now()}`; + let mainRoomIdSaveSettings: string; + const teamName = `abac-team-${Date.now()}`; + const teamNameMainRoom = `abac-team-main-save-settings-${Date.now()}`; + + before('create team main room for rooms.saveRoomSettings default restriction test', async () => { + const createTeamMain = await request + .post(`${v1}/teams.create`) + .set(credentials) + .send({ name: teamNameMainRoom, type: 1 }) + .expect(200); + + mainRoomIdSaveSettings = createTeamMain.body.team?.roomId; + + await request.post(`${v1}/rooms.saveRoomSettings`).set(credentials).send({ rid: mainRoomIdSaveSettings, default: true }).expect(200); + }); + + before('create local ABAC attribute definition for tests', async () => { + await request + .post(`${v1}/abac/attributes`) + .set(credentials) + .send({ key: localAbacKey, values: ['red', 'green'] }) + .expect(200); + }); + + before('create private room and try to set it as default', async () => { + const res = await createRoom({ + type: 'p', + name: `abac-default-room-${Date.now()}`, + }); + privateDefaultRoomId = res.body.group._id; + + await request.post(`${v1}/rooms.saveRoomSettings`).set(credentials).send({ rid: privateDefaultRoomId, default: true }).expect(200); + }); + + before('create private team, private room inside it and set as team default', async () => { + const createTeamRes = await request.post(`${v1}/teams.create`).set(credentials).send({ name: teamName, type: 0 }).expect(200); + teamId = createTeamRes.body.team._id; + + const roomRes = await createRoom({ + type: 'p', + name: `abac-team-room-${Date.now()}`, + extraData: { teamId }, + }); + teamPrivateRoomId = roomRes.body.group._id; + + const setDefaultRes = await request + .post(`${v1}/teams.updateRoom`) + .set(credentials) + .send({ teamId, roomId: teamPrivateRoomId, isDefault: true }) + .expect(200); + + if (setDefaultRes.body?.room?.teamDefault) { + teamDefaultRoomId = teamPrivateRoomId; + } + }); + + it('should fail adding ABAC attribute to private default room', async () => { + await request + .post(`${v1}/abac/room/${privateDefaultRoomId}/attributes/${localAbacKey}`) + .set(credentials) + .send({ values: ['red'] }) + .expect(400) + .expect((res) => { + expect(res.body.success).to.be.false; + expect(res.body.error).to.include('error-cannot-convert-default-room-to-abac'); + }); + }); + + it('should fail adding ABAC attribute to team default private room', async () => { + await request + .post(`${v1}/abac/room/${teamDefaultRoomId}/attributes/${localAbacKey}`) + .set(credentials) + .send({ values: ['red'] }) + .expect(400) + .expect((res) => { + expect(res.body.success).to.be.false; + expect(res.body.error).to.include('error-cannot-convert-default-room-to-abac'); + }); + }); + + it('should allow adding ABAC attribute after removing default flag from private room', async () => { + await request.post(`${v1}/rooms.saveRoomSettings`).set(credentials).send({ rid: privateDefaultRoomId, default: false }).expect(200); + + await request + .post(`${v1}/abac/room/${privateDefaultRoomId}/attributes/${localAbacKey}`) + .set(credentials) + .send({ values: ['red'] }) + .expect(200) + .expect((res) => { + expect(res.body.success).to.be.true; + }); + }); + + it('should allow adding ABAC attribute after removing team default flag', async () => { + await request + .post(`${v1}/teams.updateRoom`) + .set(credentials) + .send({ teamId, roomId: teamDefaultRoomId, isDefault: false }) + .expect(200); + + await request + .post(`${v1}/abac/room/${teamDefaultRoomId}/attributes/${localAbacKey}`) + .set(credentials) + .send({ values: ['green'] }) + .expect(200) + .expect((res) => { + expect(res.body.success).to.be.true; + }); + }); + + it('should enforce restriction on team main room when default using rooms.saveRoomSettings', async () => { + await request + .post(`${v1}/abac/room/${mainRoomIdSaveSettings}/attributes/${localAbacKey}`) + .set(credentials) + .send({ values: ['red'] }) + .expect(400) + .expect((res) => { + expect(res.body.success).to.be.false; + expect(res.body.error).to.include('error-cannot-convert-default-room-to-abac'); + }); + + await request.post(`${v1}/rooms.saveRoomSettings`).set(credentials).send({ rid: mainRoomIdSaveSettings, default: false }).expect(200); + + await request + .post(`${v1}/abac/room/${mainRoomIdSaveSettings}/attributes/${localAbacKey}`) + .set(credentials) + .send({ values: ['red'] }) + .expect(200) + .expect((res) => { + expect(res.body.success).to.be.true; + }); + }); + + after(async () => { + await deleteRoom({ type: 'p', roomId: privateDefaultRoomId }); + await deleteTeam(credentials, teamName); + await deleteTeam(credentials, teamNameMainRoom); + }); + }); + describe('Usage & Deletion', () => { it('POST add room usage for attribute (re-add after clearing) and expect delete while in use to fail', async () => { await request @@ -447,8 +594,82 @@ import { IS_EE } from '../../e2e/config/constants'; }); }); + describe('ABAC Managed Room Default Conversion Restrictions', () => { + const conversionAttrKey = `conversion_test_${Date.now()}`; + const teamName = `abac-conversion-team-${Date.now()}`; + let abacRoomId: string; + let teamIdForConversion: string; + let teamRoomId: string; + + before('create attribute definition and ABAC-managed private room', async () => { + await request + .post(`${v1}/abac/attributes`) + .set(credentials) + .send({ key: conversionAttrKey, values: ['alpha', 'beta'] }) + .expect(200); + + const roomRes = await createRoom({ + type: 'p', + name: `abac-conversion-room-${Date.now()}`, + }); + abacRoomId = roomRes.body.group._id; + + await request + .post(`${v1}/abac/room/${abacRoomId}/attributes/${conversionAttrKey}`) + .set(credentials) + .send({ values: ['alpha'] }) + .expect(200); + }); + + before('create team, private room inside, add ABAC attribute', async () => { + // Public team + const teamRes = await request.post(`${v1}/teams.create`).set(credentials).send({ name: teamName, type: 0 }).expect(200); + teamIdForConversion = teamRes.body.team._id; + + const teamRoomRes = await createRoom({ + type: 'p', + name: `abac-team-conversion-room-${Date.now()}`, + extraData: { teamId: teamIdForConversion }, + }); + teamRoomId = teamRoomRes.body.group._id; + + await request + .post(`${v1}/abac/room/${teamRoomId}/attributes/${conversionAttrKey}`) + .set(credentials) + .send({ values: ['beta'] }) + .expect(200); + }); + + after(async () => { + await Promise.all([deleteTeam(credentials, teamName), deleteRoom({ type: 'p', roomId: abacRoomId })]); + }); + + it('should fail converting ABAC-managed private room into default room', async () => { + await request + .post(`${v1}/rooms.saveRoomSettings`) + .set(credentials) + .send({ rid: abacRoomId, default: true }) + .expect(400) + .expect((res) => { + expect(res.body.success).to.be.false; + expect(res.body.error).to.include('Setting an ABAC managed room as default is not allowed [error-action-not-allowed]'); + }); + }); + + it('should fail converting ABAC-managed team room into team default room', async () => { + await request + .post(`${v1}/teams.updateRoom`) + .set(credentials) + .send({ teamId: teamIdForConversion, roomId: teamRoomId, isDefault: true }) + .expect(400) + .expect((res) => { + expect(res.body.success).to.be.false; + expect(res.body.error).to.include('error-room-is-abac-managed'); + }); + }); + }); + describe('Extended Validations & Edge Cases', () => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars let secondAttributeId: string; const firstKey = `${initialKey}_first`; const secondKey = `${initialKey}_second`; diff --git a/ee/packages/abac/src/index.spec.ts b/ee/packages/abac/src/index.spec.ts index f16a1c83bf222..ef397b80d0628 100644 --- a/ee/packages/abac/src/index.spec.ts +++ b/ee/packages/abac/src/index.spec.ts @@ -389,6 +389,18 @@ describe('AbacService (unit)', () => { expect(mockSetAbacAttributesById).not.toHaveBeenCalled(); }); + it('throws error-cannot-convert-default-room-to-abac when room is default', async () => { + mockFindOneByIdAndType.mockResolvedValueOnce({ _id: 'r1', abacAttributes: [], default: true }); + await expect(service.setRoomAbacAttributes('r1', { dept: ['eng'] })).rejects.toThrow('error-cannot-convert-default-room-to-abac'); + expect(mockSetAbacAttributesById).not.toHaveBeenCalled(); + }); + + it('throws error-cannot-convert-default-room-to-abac when room is teamDefault', async () => { + mockFindOneByIdAndType.mockResolvedValueOnce({ _id: 'r1', abacAttributes: [], teamDefault: true }); + await expect(service.setRoomAbacAttributes('r1', { dept: ['eng'] })).rejects.toThrow('error-cannot-convert-default-room-to-abac'); + expect(mockSetAbacAttributesById).not.toHaveBeenCalled(); + }); + it('throws error-invalid-attribute-key for invalid key format', async () => { mockFindOneByIdAndType.mockResolvedValueOnce({ _id: 'r1', abacAttributes: [] }); await expect(service.setRoomAbacAttributes('r1', { 'bad key': ['v'] } as any)).rejects.toThrow('error-invalid-attribute-key'); @@ -515,6 +527,20 @@ describe('AbacService (unit)', () => { await expect(service.updateRoomAbacAttributeValues('missing', 'dept', ['eng'])).rejects.toThrow('error-room-not-found'); }); + it('throws error-cannot-convert-default-room-to-abac when room is default', async () => { + mockFindOneByIdAndType.mockResolvedValueOnce({ _id: 'r1', abacAttributes: [], default: true }); + await expect(service.updateRoomAbacAttributeValues('r1', 'dept', ['eng'])).rejects.toThrow( + 'error-cannot-convert-default-room-to-abac', + ); + }); + + it('throws error-cannot-convert-default-room-to-abac when room is teamDefault', async () => { + mockFindOneByIdAndType.mockResolvedValueOnce({ _id: 'r1', abacAttributes: [], teamDefault: true }); + await expect(service.updateRoomAbacAttributeValues('r1', 'dept', ['eng'])).rejects.toThrow( + 'error-cannot-convert-default-room-to-abac', + ); + }); + it('throws error-invalid-attribute-values if adding new key exceeds max attributes', async () => { const existing = Array.from({ length: 10 }, (_, i) => ({ key: `k${i}`, values: ['x'] })); mockFindOneByIdAndType.mockResolvedValueOnce({ _id: 'r1', abacAttributes: existing }); @@ -572,6 +598,18 @@ describe('AbacService (unit)', () => { expect(mockRemoveAbacAttributeByRoomIdAndKey).not.toHaveBeenCalled(); }); + it('throws error-cannot-convert-default-room-to-abac when room is default', async () => { + mockFindOneByIdAndType.mockResolvedValueOnce({ _id: 'r1', abacAttributes: [], default: true }); + await expect((service as any).removeRoomAbacAttribute('r1', 'dept')).rejects.toThrow('error-cannot-convert-default-room-to-abac'); + expect(mockRemoveAbacAttributeByRoomIdAndKey).not.toHaveBeenCalled(); + }); + + it('throws error-cannot-convert-default-room-to-abac when room is teamDefault', async () => { + mockFindOneByIdAndType.mockResolvedValueOnce({ _id: 'r1', abacAttributes: [], teamDefault: true }); + await expect((service as any).removeRoomAbacAttribute('r1', 'dept')).rejects.toThrow('error-cannot-convert-default-room-to-abac'); + expect(mockRemoveAbacAttributeByRoomIdAndKey).not.toHaveBeenCalled(); + }); + it('returns early (no update, no hook) when attribute key not present', async () => { mockFindOneByIdAndType.mockResolvedValueOnce({ _id: 'r1', abacAttributes: [{ key: 'other', values: ['x'] }] }); await (service as any).removeRoomAbacAttribute('r1', 'dept'); @@ -614,6 +652,20 @@ describe('AbacService (unit)', () => { await expect((service as any).replaceRoomAbacAttributeByKey('missing', 'dept', ['eng'])).rejects.toThrow('error-room-not-found'); }); + it('throws error-cannot-convert-default-room-to-abac when room is default', async () => { + mockFindOneByIdAndType.mockResolvedValueOnce({ _id: 'r1', abacAttributes: [], default: true }); + await expect((service as any).replaceRoomAbacAttributeByKey('r1', 'dept', ['eng'])).rejects.toThrow( + 'error-cannot-convert-default-room-to-abac', + ); + }); + + it('throws error-cannot-convert-default-room-to-abac when room is teamDefault', async () => { + mockFindOneByIdAndType.mockResolvedValueOnce({ _id: 'r1', abacAttributes: [], teamDefault: true }); + await expect((service as any).replaceRoomAbacAttributeByKey('r1', 'dept', ['eng'])).rejects.toThrow( + 'error-cannot-convert-default-room-to-abac', + ); + }); + it('throws error-invalid-attribute-values if adding new key exceeds max attributes', async () => { const existing = Array.from({ length: 10 }, (_, i) => ({ key: `k${i}`, values: ['x'] })); mockFindOneByIdAndType.mockResolvedValueOnce({ _id: 'r1', abacAttributes: existing }); @@ -684,6 +736,20 @@ describe('AbacService (unit)', () => { expect(mockInsertAbacAttributeIfNotExistsById).not.toHaveBeenCalled(); }); + it('throws error-cannot-convert-default-room-to-abac when room is default', async () => { + mockAbacFind.mockReturnValueOnce({ toArray: async () => [{ key: 'dept', values: ['eng'] }] }); + mockFindOneByIdAndType.mockResolvedValueOnce({ _id: 'r1', abacAttributes: [], default: true }); + await expect(service.addRoomAbacAttributeByKey('r1', 'dept', ['eng'])).rejects.toThrow('error-cannot-convert-default-room-to-abac'); + expect(mockInsertAbacAttributeIfNotExistsById).not.toHaveBeenCalled(); + }); + + it('throws error-cannot-convert-default-room-to-abac when room is teamDefault', async () => { + mockAbacFind.mockReturnValueOnce({ toArray: async () => [{ key: 'dept', values: ['eng'] }] }); + mockFindOneByIdAndType.mockResolvedValueOnce({ _id: 'r1', abacAttributes: [], teamDefault: true }); + await expect(service.addRoomAbacAttributeByKey('r1', 'dept', ['eng'])).rejects.toThrow('error-cannot-convert-default-room-to-abac'); + expect(mockInsertAbacAttributeIfNotExistsById).not.toHaveBeenCalled(); + }); + it('throws error-attribute-definition-not-found when attribute definition missing', async () => { // No definitions returned mockAbacFind.mockReturnValueOnce({ toArray: async () => [] }); diff --git a/ee/packages/abac/src/index.ts b/ee/packages/abac/src/index.ts index 3106bdc713cec..7677f243bf3df 100644 --- a/ee/packages/abac/src/index.ts +++ b/ee/packages/abac/src/index.ts @@ -170,12 +170,19 @@ export class AbacService extends ServiceClass implements IAbacService { } async setRoomAbacAttributes(rid: string, attributes: Record): Promise { - const room = await Rooms.findOneByIdAndType>(rid, 'p', { - projection: { abacAttributes: 1, t: 1, teamMain: 1 }, - }); + const room = await Rooms.findOneByIdAndType>( + rid, + 'p', + { + projection: { abacAttributes: 1, t: 1, teamMain: 1, teamDefault: 1, default: 1 }, + }, + ); if (!room) { throw new Error('error-room-not-found'); } + if (room.default || room.teamDefault) { + throw new Error('error-cannot-convert-default-room-to-abac'); + } const normalized = this.validateAndNormalizeAttributes(attributes); @@ -260,12 +267,21 @@ export class AbacService extends ServiceClass implements IAbacService { } async updateRoomAbacAttributeValues(rid: string, key: string, values: string[]): Promise { - const room = await Rooms.findOneByIdAndType>(rid, 'p', { - projection: { abacAttributes: 1, t: 1, teamMain: 1 }, - }); + const room = await Rooms.findOneByIdAndType>( + rid, + 'p', + { + projection: { abacAttributes: 1, t: 1, teamMain: 1, teamDefault: 1, default: 1 }, + }, + ); if (!room) { throw new Error('error-room-not-found'); } + + if (room.default || room.teamDefault) { + throw new Error('error-cannot-convert-default-room-to-abac'); + } + const previous: IAbacAttributeDefinition[] = room.abacAttributes || []; const existingIndex = previous.findIndex((a) => a.key === key); @@ -304,11 +320,17 @@ export class AbacService extends ServiceClass implements IAbacService { } async removeRoomAbacAttribute(rid: string, key: string): Promise { - const room = await Rooms.findOneByIdAndType>(rid, 'p', { projection: { abacAttributes: 1 } }); + const room = await Rooms.findOneByIdAndType>(rid, 'p', { + projection: { abacAttributes: 1, default: 1, teamDefault: 1 }, + }); if (!room) { throw new Error('error-room-not-found'); } + if (room.default || room.teamDefault) { + throw new Error('error-cannot-convert-default-room-to-abac'); + } + const previous: IAbacAttributeDefinition[] = room.abacAttributes || []; const exists = previous.some((a) => a.key === key); if (!exists) { @@ -326,13 +348,21 @@ export class AbacService extends ServiceClass implements IAbacService { async addRoomAbacAttributeByKey(rid: string, key: string, values: string[]): Promise { await this.ensureAttributeDefinitionsExist([{ key, values }]); - const room = await Rooms.findOneByIdAndType>(rid, 'p', { - projection: { abacAttributes: 1, t: 1, teamMain: 1 }, - }); + const room = await Rooms.findOneByIdAndType>( + rid, + 'p', + { + projection: { abacAttributes: 1, t: 1, teamMain: 1, teamDefault: 1, default: 1 }, + }, + ); if (!room) { throw new Error('error-room-not-found'); } + if (room.default || room.teamDefault) { + throw new Error('error-cannot-convert-default-room-to-abac'); + } + const previous: IAbacAttributeDefinition[] = room.abacAttributes || []; if (previous.some((a) => a.key === key)) { throw new Error('error-duplicate-attribute-key'); @@ -351,13 +381,21 @@ export class AbacService extends ServiceClass implements IAbacService { async replaceRoomAbacAttributeByKey(rid: string, key: string, values: string[]): Promise { await this.ensureAttributeDefinitionsExist([{ key, values }]); - const room = await Rooms.findOneByIdAndType>(rid, 'p', { - projection: { abacAttributes: 1, t: 1, teamMain: 1 }, - }); + const room = await Rooms.findOneByIdAndType>( + rid, + 'p', + { + projection: { abacAttributes: 1, t: 1, teamMain: 1, teamDefault: 1, default: 1 }, + }, + ); if (!room) { throw new Error('error-room-not-found'); } + if (room.default || room.teamDefault) { + throw new Error('error-cannot-convert-default-room-to-abac'); + } + const exists = room?.abacAttributes?.some((a) => a.key === key); if (exists) {