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
2 changes: 2 additions & 0 deletions apps/meteor/client/lib/utils/mapSubscriptionFromApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ export const mapSubscriptionFromApi = ({
_updatedAt,
oldRoomKeys,
suggestedOldRoomKeys,
abacLastTimeChecked,
...subscription
}: Serialized<ISubscription>): ISubscription => ({
...subscription,
ts: new Date(ts),
ls: new Date(ls),
lr: new Date(lr),
_updatedAt: new Date(_updatedAt),
...(abacLastTimeChecked && { abacLastTimeChecked: new Date(abacLastTimeChecked) }),
...(oldRoomKeys && { oldRoomKeys: oldRoomKeys.map(({ ts, ...key }) => ({ ...key, ts: new Date(ts) })) }),
...(suggestedOldRoomKeys && { suggestedOldRoomKeys: suggestedOldRoomKeys.map(({ ts, ...key }) => ({ ...key, ts: new Date(ts) })) }),
});
6 changes: 6 additions & 0 deletions apps/meteor/ee/server/settings/abac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ export function addSettings(): void {
section: 'ABAC',
i18nDescription: 'ABAC_Enabled_Description',
});
await this.add('Abac_Cache_Decision_Time_Seconds', 300, {
type: 'int',
public: true,
section: 'ABAC',
invalidValue: 0,
});
},
);
});
Expand Down
28 changes: 19 additions & 9 deletions apps/meteor/server/services/authorization/canAccessRoom.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Authorization } from '@rocket.chat/core-services';
import { Authorization, License, Abac } from '@rocket.chat/core-services';
import type { RoomAccessValidator } from '@rocket.chat/core-services';
import { TEAM_TYPE } from '@rocket.chat/core-typings';
import { TEAM_TYPE, AbacAccessOperation, AbacObjectType } from '@rocket.chat/core-typings';
import type { IUser, ITeam } from '@rocket.chat/core-typings';
import { Subscriptions, Rooms, Settings, TeamMember, Team } from '@rocket.chat/models';

Expand Down Expand Up @@ -57,15 +57,25 @@ const roomAccessValidators: RoomAccessValidator[] = [
return false;
}

if (!(await Subscriptions.countByRoomIdAndUserId(room._id, user._id))) {
return false;
}

if (await Authorization.hasPermission(user._id, 'view-joined-room')) {
return true;
const [canViewJoined, canViewT] = await Promise.all([
Authorization.hasPermission(user._id, 'view-joined-room'),
Authorization.hasPermission(user._id, `view-${room.t}-room`),
]);

// When there's no ABAC setting, license or values on the room, fallback to previous behavior
if (
!room?.abacAttributes?.length ||
!(await License.hasModule('abac')) ||
(!(await Settings.getValueById('ABAC_Enabled')) as boolean)
) {
if (!(await Subscriptions.countByRoomIdAndUserId(room._id, user._id))) {
return false;
}

return canViewJoined || canViewT;
}

return Authorization.hasPermission(user._id, `view-${room.t}-room`);
return (canViewJoined || canViewT) && Abac.canAccessObject(room, user, AbacAccessOperation.READ, AbacObjectType.ROOM);
},

async function _validateAccessToDiscussionsParentRoom(room, user): Promise<boolean> {
Expand Down
3 changes: 2 additions & 1 deletion apps/meteor/server/services/authorization/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,13 @@ export class Authorization extends ServiceClass implements IAuthorization {
}

async canAccessRoomId(rid: IRoom['_id'], uid: IUser['_id']): Promise<boolean> {
const room = await Rooms.findOneById<Pick<IRoom, '_id' | 't' | 'teamId' | 'prid'>>(rid, {
const room = await Rooms.findOneById<Pick<IRoom, '_id' | 't' | 'teamId' | 'prid' | 'abacAttributes'>>(rid, {
projection: {
_id: 1,
t: 1,
teamId: 1,
prid: 1,
abacAttributes: 1,
},
});

Expand Down
131 changes: 131 additions & 0 deletions apps/meteor/tests/end-to-end/api/abac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1483,4 +1483,135 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I
});
});
});

describe('Room access (after subscribed)', () => {
let cacheRoom: IRoom;
const cacheAttrKey = `access_cache_attr_${Date.now()}`;
let cacheUser: IUser;
let cacheUserCreds: Credentials;
const ttlSeconds = 5;

before(async function () {
this.timeout(10000);

await request
.post(`${v1}/abac/attributes`)
.set(credentials)
.send({ key: cacheAttrKey, values: ['on'] })
.expect(200);

cacheRoom = (await createRoom({ type: 'p', name: `abac-cache-room-${Date.now()}` })).body.group;

cacheUser = await createUser();
cacheUserCreds = await login(cacheUser.username, password);
await addAbacAttributesToUserDirectly(cacheUser._id, [{ key: cacheAttrKey, values: ['on'] }]);
await addAbacAttributesToUserDirectly(credentials['X-User-Id'], [{ key: cacheAttrKey, values: ['on'] }]);

await request
.post(`${v1}/abac/rooms/${cacheRoom._id}/attributes/${cacheAttrKey}`)
.set(credentials)
.send({ values: ['on'] })
.expect(200);

await request
.post(`${v1}/groups.invite`)
.set(credentials)
.send({ roomId: cacheRoom._id, usernames: [cacheUser.username] })
.expect(200);

await updateSetting('Abac_Cache_Decision_Time_Seconds', ttlSeconds);

await request
.post(`${v1}/chat.sendMessage`)
.set(credentials)
.send({ message: { rid: cacheRoom._id, msg: 'Seed message for cache access test' } })
.expect(200);
});

after(async () => {
await deleteRoom({ type: 'p', roomId: cacheRoom._id });
await deleteUser(cacheUser);
await updateSetting('Abac_Cache_Decision_Time_Seconds', 300);
});

it('ACCESS: user can retrieve messages after subscription (initial compliant)', async () => {
await request
.get(`/api/v1/groups.history`)
.set(cacheUserCreds)
.query({ roomId: cacheRoom._id })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('messages').that.is.an('array');
});
});

it('ACCESS: user retains access within cache after losing attributes', async () => {
await addAbacAttributesToUserDirectly(cacheUser._id, []);

await request
.get(`/api/v1/groups.history`)
.set(cacheUserCreds)
.query({ roomId: cacheRoom._id })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
});

it('ACCESS: user loses access after cache expiry', async () => {
await updateSetting('Abac_Cache_Decision_Time_Seconds', 0);

await request
.get(`${v1}/groups.history`)
.set(cacheUserCreds)
.query({ roomId: cacheRoom._id })
.expect(403)
.expect((res) => {
expect(res.body).to.have.property('success', false);
});
});

it('ACCESS: user is removed from the room when the access check fails', async () => {
const roomInfoRes = await request
.get(`${v1}/rooms.membersOrderedByRole`)
.set(credentials)
.query({ roomId: cacheRoom._id })
.expect(200);
const { members } = roomInfoRes.body;

expect(members.find((m: IUser) => m.username === cacheUser.username)).to.be.undefined;
});

it('ACCESS: user can be re invited to the room and access history', async () => {
await addAbacAttributesToUserDirectly(cacheUser._id, [{ key: cacheAttrKey, values: ['on'] }]);
await request
.post(`${v1}/groups.invite`)
.set(credentials)
.send({ roomId: cacheRoom._id, usernames: [cacheUser.username] })
.expect(200);

await request
.get(`/api/v1/groups.history`)
.set(cacheUserCreds)
.query({ roomId: cacheRoom._id })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
});

it('ACCESS: keeps access once room attributes are removed', async () => {
await request.delete(`${v1}/abac/rooms/${cacheRoom._id}/attributes/${cacheAttrKey}`).set(credentials).expect(200);

await request
.get(`${v1}/groups.history`)
.set(cacheUserCreds)
.query({ roomId: cacheRoom._id })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
});
});
});
Loading
Loading