Skip to content

Commit

Permalink
BC-8519 - add room owner role (#5393)
Browse files Browse the repository at this point in the history
* add roomadmin and roomowner roles

* remove room_delete permission from roomeditor

* creating room memberships is now an operation seperate from adding users.
** creating a room memberships adds a user as owner
** adding a user fails if no membership exists yet

* fix room membership rule to use room role to check permissions

* ensure that room owners can not be removed from a room

---------

Co-authored-by: Thomas Feldtkeller <[email protected]>
  • Loading branch information
hoeppner-dataport and Metauriel authored Dec 13, 2024
1 parent 9737e90 commit 0df7b67
Show file tree
Hide file tree
Showing 18 changed files with 429 additions and 172 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class Migration20241113100535 extends Migration {
);

if (teacherRoleUpdate.modifiedCount > 0) {
console.info('Rollback: Permission ROOM_CREATE added to role teacher.');
console.info('Rollback: Permission ROOM_CREATE removed from role teacher.');
}

const roomEditorRoleUpdate = await this.getCollection('roles').updateOne(
Expand All @@ -61,7 +61,7 @@ export class Migration20241113100535 extends Migration {
);

if (roomEditorRoleUpdate.modifiedCount > 0) {
console.info('Rollback: Permission ROOM_DELETE added to role roomeditor.');
console.info('Rollback: Permission ROOM_DELETE removed from role roomeditor.');
}
}
}
40 changes: 40 additions & 0 deletions apps/server/src/migrations/mikro-orm/Migration20241209165812.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Migration } from '@mikro-orm/migrations-mongodb';

export class Migration20241209165812 extends Migration {
async up(): Promise<void> {
// Add ROOM_OWNER role
await this.getCollection('roles').insertOne({
name: 'roomowner',
permissions: [
'ROOM_VIEW',
'ROOM_EDIT',
'ROOM_DELETE',
'ROOM_MEMBERS_ADD',
'ROOM_MEMBERS_REMOVE',
'ROOM_CHANGE_OWNER',
],
});
console.info(
'Added ROOM_OWNER role with ROOM_VIEW, -_EDIT, _DELETE, -_MEMBERS_ADD, -_MEMBERS_REMOVE AND -_CHANGE_OWNER permission'
);

// Add ROOM_ADMIN role
await this.getCollection('roles').insertOne({
name: 'roomadmin',
permissions: ['ROOM_VIEW', 'ROOM_EDIT', 'ROOM_MEMBERS_ADD', 'ROOM_MEMBERS_REMOVE'],
});
console.info(
'Added ROOM_ADMIN role with ROOM_VIEW, ROOM_EDIT, ROOM_MEMBERS_ADD AND ROOM_MEMBERS_REMOVE permissions'
);
}

async down(): Promise<void> {
// Remove ROOM_OWNER role
await this.getCollection('roles').deleteOne({ name: 'roomowner' });
console.info('Rollback: Removed ROOM_OWNER role');

// Remove ROOM_ADMIN role
await this.getCollection('roles').deleteOne({ name: 'roomadmin' });
console.info('Rollback: Removed ROOM_ADMIN role');
}
}
35 changes: 35 additions & 0 deletions apps/server/src/migrations/mikro-orm/Migration20241210152600.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Migration } from '@mikro-orm/migrations-mongodb';

export class Migration20241210152600 extends Migration {
async up(): Promise<void> {
const roomEditorRoleUpdate = await this.getCollection('roles').updateOne(
{ name: 'roomeditor' },
{
$set: {
permissions: ['ROOM_VIEW', 'ROOM_EDIT'],
},
}
);

if (roomEditorRoleUpdate.modifiedCount > 0) {
console.info('Permission ROOM_DELETE removed from role roomeditor.');
}
}

async down(): Promise<void> {
const roomEditorRoleUpdate = await this.getCollection('roles').updateOne(
{ name: 'roomeditor' },
{
$set: {
permissions: ['ROOM_VIEW', 'ROOM_EDIT', 'ROOM_DELETE'],
},
}
);

if (roomEditorRoleUpdate.modifiedCount > 0) {
console.info(
'Rollback: Permissions ROOM_DELETE added to and ROOM_MEMBERS_ADD and ROOM_MEMBERS_REMOVE removed from role roomeditor.'
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,17 @@ describe(RoomMembershipRule.name, () => {

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

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

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

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

describe('when user is not member of room', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ export class RoomMembershipRule implements Rule<RoomMembershipAuthorizable> {
this.authorisationInjectionService.injectAuthorizationRule(this);
}

public isApplicable(user: User, object: unknown): boolean {
public isApplicable(_: User, object: unknown): boolean {
const isMatched = object instanceof RoomMembershipAuthorizable;

return isMatched;
}

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 (!this.hasAccessToSchool(user, object.schoolId)) {
return false;
}

if (![primarySchoolId, ...secondarySchoolIds].includes(object.schoolId)) {
if (!this.hasRequiredRoomPermissions(user, object, context.requiredPermissions)) {
return false;
}

Expand All @@ -36,4 +36,30 @@ export class RoomMembershipRule implements Rule<RoomMembershipAuthorizable> {
}
return permissionsThisUserHas.includes(Permission.ROOM_EDIT);
}

private hasAccessToSchool(user: User, schoolId: string): boolean {
const primarySchoolId = user.school.id;
const secondarySchools = user.secondarySchools ?? [];
const secondarySchoolIds = secondarySchools.map(({ school }) => school.id);

return [primarySchoolId, ...secondarySchoolIds].includes(schoolId);
}

private hasRequiredRoomPermissions(
user: User,
object: RoomMembershipAuthorizable,
requiredPermissions: string[]
): boolean {
const roomPermissionsOfUser = this.resolveRoomPermissions(user, object);
const missingPermissions = requiredPermissions.filter((permission) => !roomPermissionsOfUser.includes(permission));
return missingPermissions.length === 0;
}

private resolveRoomPermissions(user: User, object: RoomMembershipAuthorizable): string[] {
const member = object.members.find((m) => m.userId === user.id);
if (!member) {
return [];
}
return member.roles.flatMap((role) => role.permissions ?? []);
}
}
Loading

0 comments on commit 0df7b67

Please sign in to comment.