Skip to content

Commit

Permalink
update authorization rule for room-members, connect to room member wi…
Browse files Browse the repository at this point in the history
…th room module
  • Loading branch information
EzzatOmar committed Oct 15, 2024
1 parent 786ce74 commit 66b10bc
Show file tree
Hide file tree
Showing 26 changed files with 1,079 additions and 220 deletions.
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';
11 changes: 0 additions & 11 deletions apps/server/src/modules/room-member/api/room-member.controller.ts

This file was deleted.

9 changes: 0 additions & 9 deletions apps/server/src/modules/room-member/api/room-member.uc.ts

This file was deleted.

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);
}
}
93 changes: 93 additions & 0 deletions apps/server/src/modules/room-member/doc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Room Member Module

The Room Member module manages the association between users and rooms, handling permissions and roles within rooms. This module is designed to be injected into the Room module for managing user access and roles within rooms.

## Data Structure

### RoomMemberEntity

The core entity of this module is the `RoomMemberEntity`, which represents a user's membership in a room.

```typescript
@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;
}
```

Let's break down the structure:

1. `@Entity({ tableName: 'room-members' })`: This decorator marks the class as an entity and specifies the table name in the database.

2. `extends BaseEntityWithTimestamps`: The entity inherits from a base class that likely includes common fields like `id`, `createdAt`, and `updatedAt`.

3. `implements RoomMemberProps`: The entity implements an interface that defines its properties.

4. `roomId: ObjectId`: This property represents the ID of the room to which this member belongs. It's indexed for faster queries.

5. `userGroup: GroupEntity`: This is a one-to-one relationship with the `GroupEntity`. The `owner: true` option means this entity owns the relationship, and `orphanRemoval: true` means that if this entity is deleted, the associated `GroupEntity` will also be deleted.

6. `domainObject: RoomMember | undefined`: This is a non-persisted property that can hold a reference to the domain object representation of this entity.

### GroupEntity

The `userGroup` property uses the `GroupEntity` from the Group module. This structure allows for multiple users to be associated with a room through a single group.

```typescript
class GroupEntity {
id: EntityId;
name: string;
users: GroupUserEmbeddable[];
// other properties...
}
```

### GroupUserEmbeddable

Each user in the group is represented by a `GroupUserEmbeddable`:

```typescript
class GroupUserEmbeddable {
user: User;
role: Role;
}
```

This structure allows for flexible assignment of roles to users within the context of a room.

## Key Points

1. The `RoomMemberEntity` doesn't directly store user IDs or roles. Instead, it uses a `GroupEntity` to manage this information.
2. This design allows for easy management of multiple users and roles for a single room.
3. The `roomId` is stored directly on the `RoomMemberEntity` for efficient querying of members for a specific room.
4. The `domainObject` property facilitates the separation between the database entity and the domain object, following domain-driven design principles.

This structure provides a flexible and scalable way to manage room memberships, allowing for complex permission and role scenarios within rooms.

## Service

The `RoomMemberService` is a service for the `RoomMember` entity. It provides methods for creating, updating, and deleting `RoomMember` entities.

```typescript
class RoomMemberService {
constructor(private readonly roomMembersRepo: RoomMemberRepo) {}
}
```

## Usage

The `RoomMemberService` is designed to be injected into the `Room` module for managing user access and roles within rooms.

## API

There is no API for now. Member specific writes/reads can be implemented by adding an API to the RoomMember module.
Like adding/removing users to a room.

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 };
Loading

0 comments on commit 66b10bc

Please sign in to comment.