Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BC-8141 - room membership rule guest role #5386

Merged
merged 11 commits into from
Dec 10, 2024
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
Loading