Skip to content

Commit

Permalink
BC-8141 - room membership rule guest role (#5386)
Browse files Browse the repository at this point in the history
* extend roomMembershipRule to check for user's school access (primary or secondary)
  • Loading branch information
hoeppner-dataport authored Dec 10, 2024
1 parent 4da6d99 commit 062af47
Show file tree
Hide file tree
Showing 22 changed files with 258 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ export class AuthorizationContextBuilder {
return context;
}

static write(requiredPermissions: Permission[]): AuthorizationContext {
public static write(requiredPermissions: Permission[]): AuthorizationContext {
const context = this.build(requiredPermissions, Action.write);

return context;
}

static read(requiredPermissions: Permission[]): AuthorizationContext {
public static read(requiredPermissions: Permission[]): AuthorizationContext {
const context = this.build(requiredPermissions, Action.read);

return context;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
cleanupCollections,
groupEntityFactory,
roleFactory,
schoolEntityFactory,
TestApiClient,
UserAndAccountTestFactory,
} from '@shared/testing';
Expand Down Expand Up @@ -49,12 +50,17 @@ describe(`board copy with room relation (api)`, () => {
name: RoleName.ROOMEDITOR,
permissions: [Permission.ROOM_EDIT],
});
const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher();
const school = schoolEntityFactory.buildWithId();
const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school });
const userGroup = groupEntityFactory.buildWithId({
type: GroupEntityTypes.ROOM,
users: [{ role, user: teacherUser }],
});
const roomMembership = roomMembershipEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id });
const roomMembership = roomMembershipEntityFactory.build({
roomId: room.id,
userGroupId: userGroup.id,
schoolId: teacherUser.school.id,
});
const columnBoardNode = columnBoardEntityFactory.build({
...columnBoardProps,
context: { id: room.id, type: BoardExternalReferenceType.Room },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { Permission } from '@shared/domain/interface';
import { RoleName } from '@shared/domain/interface/rolename.enum';
import { cleanupCollections, groupEntityFactory, roleFactory, TestApiClient, userFactory } from '@shared/testing';
import {
cleanupCollections,
groupEntityFactory,
roleFactory,
schoolEntityFactory,
TestApiClient,
userFactory,
} from '@shared/testing';
import { accountFactory } from '@src/modules/account/testing';
import { GroupEntityTypes } from '@src/modules/group/entity';
import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing';
Expand Down Expand Up @@ -42,7 +49,8 @@ describe(`create board in room (api)`, () => {
describe('When request is valid', () => {
describe('When user is allowed to edit the room', () => {
const setup = async () => {
const user = userFactory.buildWithId();
const school = schoolEntityFactory.buildWithId();
const user = userFactory.buildWithId({ school });
const account = accountFactory.withUser(user).build();

const role = roleFactory.buildWithId({ name: RoleName.ROOMEDITOR, permissions: [Permission.ROOM_EDIT] });
Expand All @@ -52,9 +60,13 @@ describe(`create board in room (api)`, () => {
users: [{ user, role }],
});

const room = roomEntityFactory.buildWithId();
const room = roomEntityFactory.buildWithId({ schoolId: user.school.id });

const roomMembership = roomMembershipEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id });
const roomMembership = roomMembershipEntityFactory.build({
roomId: room.id,
userGroupId: userGroup.id,
schoolId: user.school.id,
});

await em.persistAndFlush([account, user, role, userGroup, room, roomMembership]);
em.clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ describe(BoardContextService.name, () => {
id: 'foo',
roomId: columnBoard.context.id,
members: [{ userId: user.id, roles: [role] }],
schoolId: user.school.id,
});

const result = await service.getUsersWithBoardRoles(columnBoard);
Expand Down Expand Up @@ -271,6 +272,7 @@ describe(BoardContextService.name, () => {
id: 'foo',
roomId: columnBoard.context.id,
members: [{ userId: user.id, roles: [role] }],
schoolId: user.school.id,
});

const result = await service.getUsersWithBoardRoles(columnBoard);
Expand Down Expand Up @@ -306,6 +308,7 @@ describe(BoardContextService.name, () => {
id: 'foo',
roomId: columnBoard.context.id,
members: [{ userId: user.id, roles: [role] }],
schoolId: user.school.id,
});

const result = await service.getUsersWithBoardRoles(columnBoard);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Permission } from '@shared/domain/interface';
import { roleDtoFactory, setupEntities, userFactory } from '@shared/testing';
import { Action, AuthorizationHelper, AuthorizationInjectionService } from '@modules/authorization';
import { roomFactory } from '@modules/room/testing';
import { Test, TestingModule } from '@nestjs/testing';
import { Permission, RoleName } from '@shared/domain/interface';
import { roleDtoFactory, roleFactory, schoolEntityFactory, setupEntities, userFactory } from '@shared/testing';
import { RoomMembershipAuthorizable } from '../do/room-membership-authorizable.do';
import { RoomMembershipRule } from './room-membership.rule';

Expand Down Expand Up @@ -30,7 +31,7 @@ describe(RoomMembershipRule.name, () => {
describe('when entity is applicable', () => {
const setup = () => {
const user = userFactory.buildWithId();
const roomMembershipAuthorizable = new RoomMembershipAuthorizable('', []);
const roomMembershipAuthorizable = new RoomMembershipAuthorizable('', [], user.school.id);

return { user, roomMembershipAuthorizable };
};
Expand Down Expand Up @@ -60,66 +61,135 @@ describe(RoomMembershipRule.name, () => {
});

describe('hasPermission', () => {
describe('when user is viewer member of room', () => {
const setup = () => {
const user = userFactory.buildWithId();
const roleDto = roleDtoFactory.build({ permissions: [Permission.ROOM_VIEW] });
const roomMembershipAuthorizable = new RoomMembershipAuthorizable('', [{ roles: [roleDto], userId: user.id }]);
describe("when user's primary school is room's school", () => {
describe('when user is not member of the room', () => {
const setup = () => {
const user = userFactory.buildWithId();
const roomMembershipAuthorizable = new RoomMembershipAuthorizable('', [], user.school.id);

return { user, roomMembershipAuthorizable };
};
return { user, roomMembershipAuthorizable };
};

it('should return "true" for read action', () => {
const { user, roomMembershipAuthorizable } = setup();
it('should return "false" for read action', () => {
const { user, roomMembershipAuthorizable } = setup();

const res = service.hasPermission(user, roomMembershipAuthorizable, {
action: Action.read,
requiredPermissions: [],
});
const res = service.hasPermission(user, roomMembershipAuthorizable, {
action: Action.read,
requiredPermissions: [],
});

expect(res).toBe(true);
expect(res).toBe(false);
});
});

it('should return "false" for write action', () => {
const { user, roomMembershipAuthorizable } = setup();
describe('when user has view permission for room', () => {
const setup = () => {
const user = userFactory.buildWithId();
const roleDto = roleDtoFactory.build({ permissions: [Permission.ROOM_VIEW] });
const roomMembershipAuthorizable = new RoomMembershipAuthorizable(
'',
[{ roles: [roleDto], userId: user.id }],
user.school.id
);

return { user, roomMembershipAuthorizable };
};

it('should return "true" for read action', () => {
const { user, roomMembershipAuthorizable } = setup();

const res = service.hasPermission(user, roomMembershipAuthorizable, {
action: Action.write,
requiredPermissions: [],
const res = service.hasPermission(user, roomMembershipAuthorizable, {
action: Action.read,
requiredPermissions: [],
});

expect(res).toBe(true);
});

expect(res).toBe(false);
it('should return "false" for write action', () => {
const { user, roomMembershipAuthorizable } = setup();

const res = service.hasPermission(user, roomMembershipAuthorizable, {
action: Action.write,
requiredPermissions: [],
});

expect(res).toBe(false);
});
});
});

describe('when user is not member of room', () => {
const setup = () => {
const user = userFactory.buildWithId();
const roomMembershipAuthorizable = new RoomMembershipAuthorizable('', []);
describe('when user is not member of room', () => {
const setup = () => {
const user = userFactory.buildWithId();
const roomMembershipAuthorizable = new RoomMembershipAuthorizable('', [], user.school.id);

return { user, roomMembershipAuthorizable };
};
return { user, roomMembershipAuthorizable };
};

it('should return "false" for read action', () => {
const { user, roomMembershipAuthorizable } = setup();
it('should return "false" for read action', () => {
const { user, roomMembershipAuthorizable } = setup();

const res = service.hasPermission(user, roomMembershipAuthorizable, {
action: Action.read,
requiredPermissions: [],
const res = service.hasPermission(user, roomMembershipAuthorizable, {
action: Action.read,
requiredPermissions: [],
});

expect(res).toBe(false);
});

expect(res).toBe(false);
});
it('should return "false" for write action', () => {
const { user, roomMembershipAuthorizable } = setup();

it('should return "false" for write action', () => {
const { user, roomMembershipAuthorizable } = setup();
const res = service.hasPermission(user, roomMembershipAuthorizable, {
action: Action.write,
requiredPermissions: [],
});

expect(res).toBe(false);
});
});
});

const res = service.hasPermission(user, roomMembershipAuthorizable, {
action: Action.write,
requiredPermissions: [],
describe("when user is guest at room's school", () => {
describe('when user has view permission for room', () => {
const setup = () => {
const otherSchool = schoolEntityFactory.buildWithId();
const guestTeacherRole = roleFactory.buildWithId({ name: RoleName.GUESTTEACHER });
const user = userFactory.buildWithId({
secondarySchools: [{ school: otherSchool, role: guestTeacherRole }],
});
const room = roomFactory.build({ schoolId: otherSchool.id });
const roleDto = roleDtoFactory.build({ permissions: [Permission.ROOM_VIEW] });
const roomMembershipAuthorizable = new RoomMembershipAuthorizable(
room.id,
[{ roles: [roleDto], userId: user.id }],
otherSchool.id
);

return { user, roomMembershipAuthorizable };
};

it('should return "true" for read action', () => {
const { user, roomMembershipAuthorizable } = setup();

const res = service.hasPermission(user, roomMembershipAuthorizable, {
action: Action.read,
requiredPermissions: [],
});

expect(res).toBe(true);
});

expect(res).toBe(false);
it('should return "false" for write action', () => {
const { user, roomMembershipAuthorizable } = setup();

const res = service.hasPermission(user, roomMembershipAuthorizable, {
action: Action.write,
requiredPermissions: [],
});

expect(res).toBe(false);
});
});
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Action, AuthorizationContext, AuthorizationInjectionService, Rule } from '@modules/authorization';
import { Injectable } from '@nestjs/common';
import { User } from '@shared/domain/entity';
import { Permission } from '@shared/domain/interface';
import { AuthorizationInjectionService, Action, AuthorizationContext, Rule } from '@modules/authorization';
import { RoomMembershipAuthorizable } from '../do/room-membership-authorizable.do';

@Injectable()
Expand All @@ -17,6 +17,14 @@ export class RoomMembershipRule implements Rule<RoomMembershipAuthorizable> {
}

public hasPermission(user: User, object: RoomMembershipAuthorizable, context: AuthorizationContext): boolean {
const primarySchoolId = user.school.id;
const secondarySchools = user.secondarySchools ?? [];
const secondarySchoolIds = secondarySchools.map(({ school }) => school.id);

if (![primarySchoolId, ...secondarySchoolIds].includes(object.schoolId)) {
return false;
}

const { action } = context;
const permissionsThisUserHas = object.members
.filter((member) => member.userId === user.id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ export class RoomMembershipAuthorizable implements AuthorizableObject {

public readonly roomId: EntityId;

public readonly schoolId: EntityId;

public readonly members: UserWithRoomRoles[];

public constructor(roomId: EntityId, members: UserWithRoomRoles[]) {
constructor(roomId: EntityId, members: UserWithRoomRoles[], schoolId: EntityId) {
this.members = members;
this.roomId = roomId;
this.schoolId = schoolId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ describe('RoomMembershipService', () => {
it('should return empty RoomMembershipAuthorizable when roomMembership not exists', async () => {
const roomId = 'nonexistent';
roomMembershipRepo.findByRoomId.mockResolvedValue(null);
roomService.getSingleRoom.mockResolvedValue(roomFactory.build({ id: roomId }));

const result = await service.getRoomMembershipAuthorizable(roomId);

Expand Down
Loading

0 comments on commit 062af47

Please sign in to comment.