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-7971 - introduce room members module #5291

Merged
merged 33 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
786ce74
wip: added new roles and service to add user groups, tests broken.
EzzatOmar Oct 6, 2024
66b10bc
update authorization rule for room-members, connect to room member wi…
EzzatOmar Oct 15, 2024
91fe96e
Merge branch 'main' into BC-7971-room-members
EzzatOmar Oct 15, 2024
199aed9
fix broken do mapper tests
EzzatOmar Oct 15, 2024
6d085a3
remove docs
EzzatOmar Oct 15, 2024
ac210c1
Merge branch 'main' into BC-7971-room-members
EzzatOmar Oct 15, 2024
c2c60db
add group type = room
EzzatOmar Oct 16, 2024
d1c367c
- Add group type "room" to group module.
EzzatOmar Oct 16, 2024
48ae35f
Merge branch 'main' into BC-7971-room-members
EzzatOmar Oct 16, 2024
fb26f78
fix code coverage
EzzatOmar Oct 16, 2024
a04929d
Merge branch 'main' into BC-7971-room-members
EzzatOmar Oct 17, 2024
1ae2f1e
Merge branch 'main' into BC-7971-room-members
EzzatOmar Oct 21, 2024
72af51c
add group create service
EzzatOmar Oct 21, 2024
11dac30
add groupservice.addUserToGroup method
EzzatOmar Oct 21, 2024
eca91f3
update nest module, remove not needed imports
EzzatOmar Oct 21, 2024
bdb8a35
add entity and do for room member
EzzatOmar Oct 21, 2024
e8bc6a8
add mapper for room member
EzzatOmar Oct 21, 2024
f58d452
add room member repo
EzzatOmar Oct 21, 2024
d633c98
remove old mapper
EzzatOmar Oct 21, 2024
cc7d5b5
add room member rule
EzzatOmar Oct 21, 2024
97eb5a9
add authorization rule to room member
EzzatOmar Oct 21, 2024
ca1eaed
use new structure in room member service
EzzatOmar Oct 21, 2024
e3590ec
put authorization in room uc
EzzatOmar Oct 21, 2024
b6f00b9
reverse group repo exposure
EzzatOmar Oct 21, 2024
0d6143f
Merge branch 'main' into BC-7971-room-members
EzzatOmar Oct 21, 2024
4a9f7c4
fix tests
EzzatOmar Oct 22, 2024
c6fab6b
Merge branch 'main' into BC-7971-room-members
EzzatOmar Oct 22, 2024
da969dc
add missing tests and cleanup
EzzatOmar Oct 23, 2024
100f17d
Merge branch 'main' into BC-7971-room-members
EzzatOmar Oct 23, 2024
0b5dd82
reverse authz order to make sure 404 is returned
EzzatOmar Oct 23, 2024
7835361
Merge branch 'main' into BC-7971-room-members
EzzatOmar Oct 23, 2024
74f4f10
update migration backup
EzzatOmar Oct 23, 2024
23988d4
fix lint err
EzzatOmar Oct 23, 2024
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
29 changes: 29 additions & 0 deletions apps/server/src/migrations/mikro-orm/Migration202410041210124.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Migration } from '@mikro-orm/migrations-mongodb';

export class Migration202410041210124 extends Migration {
async up(): Promise<void> {
// Add ROOM_VIEWER role
await this.getCollection('roles').insertOne({
name: 'room_viewer',
permissions: ['ROOM_VIEW'],
});
console.info('Added ROOM_VIEWER role with ROOM_VIEW permission');

// Add ROOM_EDITOR role
await this.getCollection('roles').insertOne({
name: 'room_editor',
permissions: ['ROOM_VIEW', 'ROOM_EDIT'],
});
console.info('Added ROOM_EDITOR role with ROOM_VIEW and ROOM_EDIT permissions');
}

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

// Remove ROOM_EDITOR role
await this.getCollection('roles').deleteOne({ name: 'ROOM_EDITOR' });
console.info('Rollback: Removed ROOM_EDITOR role');
}
}
1 change: 1 addition & 0 deletions apps/server/src/modules/group/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { GroupModule } from './group.module';
export { GroupConfig } from './group.config';
export * from './domain';
export { GroupService } from './service';
export { GroupRepo } from './repo';
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { createMock } from '@golevelup/ts-jest';
import { Test, TestingModule } from '@nestjs/testing';
import { Role, User } from '@shared/domain/entity';
import { groupEntityFactory, roleFactory, setupEntities, userFactory } from '@shared/testing';
import { AuthorizationHelper, AuthorizationInjectionService } from '@src/modules/authorization';
import { RoomMemberEntity } from '@src/modules/room-member';
import { roomMemberEntityFactory } from '@src/modules/room-member/testing';
import { Permission, RoleName } from '@shared/domain/interface';
import { Action, AuthorizationContext } from '@src/modules/authorization/domain/type';
import { RoomMemberRule } from './room-member.rule';

describe('RoomMemberRule', () => {
let module: TestingModule;
let rule: RoomMemberRule;
let injectionService: AuthorizationInjectionService;

beforeAll(async () => {
await setupEntities();

module = await Test.createTestingModule({
providers: [
RoomMemberRule,
{
provide: AuthorizationHelper,
useValue: createMock<AuthorizationHelper>(),
},
AuthorizationInjectionService,
],
}).compile();

rule = module.get(RoomMemberRule);
injectionService = await module.get(AuthorizationInjectionService);
});

afterAll(async () => {
await module.close();
});

beforeEach(() => {
jest.clearAllMocks();
});

describe('injection', () => {
it('should inject itself into authorisation module', () => {
expect(injectionService.getAuthorizationRules()).toContain(rule);
});
});

describe('isApplicable', () => {
describe('when the entity is applicable', () => {
const setup = () => {
const user: User = userFactory.buildWithId();
const roomMember: RoomMemberEntity = roomMemberEntityFactory.build({});

return {
user,
roomMember,
};
};

it('should return true', () => {
const { user, roomMember } = setup();

const result = rule.isApplicable(user, roomMember);

expect(result).toEqual(true);
});
});

describe('when the entity is not applicable', () => {
const setup = () => {
const user: User = userFactory.buildWithId();

return {
user,
};
};

it('should return false', () => {
const { user } = setup();

const result = rule.isApplicable(user, user);

expect(result).toEqual(false);
});
});
});

describe('hasPermission', () => {
describe('when the user has no permission', () => {
const setup = () => {
const role: Role = roleFactory.buildWithId();
const user: User = userFactory.buildWithId();
const userGroupEntity = groupEntityFactory.buildWithId({
users: [{ role, user }],
organization: undefined,
externalSource: undefined,
});

const roomMember = roomMemberEntityFactory.build({ userGroup: userGroupEntity });

return {
user,
roomMember,
};
};

it('should not allow read', () => {
const { roomMember, user } = setup();
const context: AuthorizationContext = {
action: Action.read,
requiredPermissions: [],
};
const result = rule.hasPermission(user, roomMember, context);

expect(result).toEqual(false);
});

it('should not allow write', () => {
const { roomMember, user } = setup();
const context: AuthorizationContext = {
action: Action.write,
requiredPermissions: [],
};
const result = rule.hasPermission(user, roomMember, context);

expect(result).toEqual(false);
});
});

describe('when user has ROOM_EDITOR role', () => {
const setup = () => {
const role = roleFactory.buildWithId({
name: RoleName.ROOM_EDITOR,
permissions: [Permission.ROOM_EDIT, Permission.ROOM_VIEW],
});
const user = userFactory.buildWithId();
const userGroupEntity = groupEntityFactory.buildWithId({
users: [{ role, user }],
organization: undefined,
externalSource: undefined,
});

const roomMember = roomMemberEntityFactory.build({ userGroup: userGroupEntity });

return { user, roomMember };
};

it('should allow read', () => {
const { user, roomMember } = setup();
const context = { action: Action.read, requiredPermissions: [] };
const result = rule.hasPermission(user, roomMember, context);
expect(result).toEqual(true);
});

it('should allow write', () => {
const { user, roomMember } = setup();
const context = { action: Action.write, requiredPermissions: [] };
const result = rule.hasPermission(user, roomMember, context);
expect(result).toEqual(true);
});
});

describe('when user has ROOM_VIEWER role', () => {
const setup = () => {
const role = roleFactory.buildWithId({ name: RoleName.ROOM_VIEWER, permissions: [Permission.ROOM_VIEW] });
const user = userFactory.buildWithId();
const userGroupEntity = groupEntityFactory.buildWithId({
users: [{ role, user }],
organization: undefined,
externalSource: undefined,
});

const roomMember = roomMemberEntityFactory.build({ userGroup: userGroupEntity });

return { user, roomMember };
};

it('should allow read', () => {
const { user, roomMember } = setup();
const context = { action: Action.read, requiredPermissions: [] };
const result = rule.hasPermission(user, roomMember, context);
expect(result).toEqual(true);
});

it('should not allow write', () => {
const { user, roomMember } = setup();
const context = { action: Action.write, requiredPermissions: [] };
const result = rule.hasPermission(user, roomMember, context);
expect(result).toEqual(false);
});
});

describe('when user is not room member', () => {
const setup = () => {
const user = userFactory.buildWithId();
const userGroupEntity = groupEntityFactory.buildWithId({
users: [],
organization: undefined,
externalSource: undefined,
});

const roomMember = roomMemberEntityFactory.build({ userGroup: userGroupEntity });

return { user, roomMember };
};

it('should not allow read', () => {
const { user, roomMember } = setup();
const context = { action: Action.read, requiredPermissions: [] };
const result = rule.hasPermission(user, roomMember, context);
expect(result).toEqual(false);
});

it('should not allow write', () => {
const { user, roomMember } = setup();
const context = { action: Action.write, requiredPermissions: [] };
const result = rule.hasPermission(user, roomMember, context);
expect(result).toEqual(false);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Injectable } from '@nestjs/common';
import { User } from '@shared/domain/entity';
import { Permission } from '@shared/domain/interface';
import { RoomMemberEntity } from '@src/modules/room-member';
import { AuthorizationHelper } from '@src/modules/authorization/domain/service/authorization.helper';
import { Action, AuthorizationContext, Rule } from '@src/modules/authorization/domain/type';
import { AuthorizationInjectionService } from '@src/modules/authorization/domain/service/authorization-injection.service';

@Injectable()
export class RoomMemberRule implements Rule<RoomMemberEntity> {
constructor(
// TODO: check if we can remove this
private readonly authorizationHelper: AuthorizationHelper,
authorisationInjectionService: AuthorizationInjectionService
) {
authorisationInjectionService.injectAuthorizationRule(this);
}

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

return isMatched;
}

public hasPermission(user: User, object: RoomMemberEntity, context: AuthorizationContext): boolean {
const { action } = context;
const userPermissionsForThisRoom = object.userGroup.users
.filter((group) => group.user.id === user.id)
.flatMap((group) => group.role.permissions);
if (action === Action.read) {
return userPermissionsForThisRoom.includes(Permission.ROOM_VIEW);
}
return userPermissionsForThisRoom.includes(Permission.ROOM_EDIT);
}
}
39 changes: 39 additions & 0 deletions apps/server/src/modules/room-member/do/room-member.do.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { ObjectId } from '@mikro-orm/mongodb';
import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object';
import { EntityId } from '@shared/domain/types';
import { GroupEntity } from '@src/modules/group/entity/group.entity';

export interface RoomMemberProps extends AuthorizableObject {
id: EntityId;
roomId: ObjectId;
userGroup: GroupEntity;
createdAt: Date;
updatedAt: Date;
}

export type RoomMemberCreateProps = Pick<RoomMemberProps, 'roomId'>;
export type RoomMemberUpdateProps = RoomMemberCreateProps;

export class RoomMember extends DomainObject<RoomMemberProps> {
Metauriel marked this conversation as resolved.
Show resolved Hide resolved
public constructor(props: RoomMemberProps) {
super(props);
}

public getProps(): RoomMemberProps {
// Note: Propagated hotfix. Will be resolved with mikro-orm update. Look at the comment in board-node.do.ts.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { domainObject, ...copyProps } = this.props;

return copyProps;
}

public get roomId(): ObjectId {
return this.props.roomId;
}

public get userGroup(): GroupEntity {
return this.props.userGroup;
}
}
6 changes: 6 additions & 0 deletions apps/server/src/modules/room-member/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { RoomMemberEntity } from './repo/entity';
import { RoomMemberRepo } from './repo/room-member.repo';
import { RoomMemberService } from './service/room-member.service';

export * from './room-member.module';
export { RoomMemberEntity, RoomMemberRepo, RoomMemberService };
1 change: 1 addition & 0 deletions apps/server/src/modules/room-member/repo/entity/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './room-member.entity';
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Entity, Index, OneToOne, Property } from '@mikro-orm/core';
import { ObjectId } from '@mikro-orm/mongodb';
import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity';
import { GroupEntity } from '@src/modules/group/entity/group.entity';
import { RoomMember, RoomMemberProps } from '../../do/room-member.do';

@Entity({ tableName: 'room-members' })
export class RoomMemberEntity extends BaseEntityWithTimestamps implements RoomMemberProps {
@Property()
@Index()
roomId!: ObjectId;

@OneToOne(() => GroupEntity, { owner: true, orphanRemoval: true })
userGroup!: GroupEntity;

@Property({ persist: false })
domainObject: RoomMember | undefined;
}
Loading
Loading