-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
update authorization rule for room-members, connect to room member wi…
…th room module
- Loading branch information
Showing
26 changed files
with
1,079 additions
and
220 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 0 additions & 11 deletions
11
apps/server/src/modules/room-member/api/room-member.controller.ts
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
223 changes: 223 additions & 0 deletions
223
apps/server/src/modules/room-member/authorization/room-member.rule.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); | ||
}); |
35 changes: 35 additions & 0 deletions
35
apps/server/src/modules/room-member/authorization/room-member.rule.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
Oops, something went wrong.