refactor: moved bouncer actions to policies

This commit is contained in:
Sergey Yarkov 2022-06-08 23:08:09 +03:00
parent 06e8acd0b2
commit b6217a4007
8 changed files with 106 additions and 122 deletions

View File

@ -93,7 +93,7 @@ export default class UsersController extends BaseController {
*/ */
public async delete(ctx: HttpContextContract) { public async delete(ctx: HttpContextContract) {
const result = await this.userService.deleteUser(ctx.params.id); const result = await this.userService.deleteUser(ctx.params.id, ctx);
if (!result.success && result.error) { if (!result.success && result.error) {
throw new Exception(result.message, result.status, result.error.code); throw new Exception(result.message, result.status, result.error.code);

View File

@ -0,0 +1,35 @@
import { BasePolicy } from '@ioc:Adonis/Addons/Bouncer';
/**
* Helpers
*/
import RoleHelper from 'App/Helpers/RoleHelper';
/**
* Models
*/
import Lesson from 'App/Models/Lesson';
import User from 'App/Models/User';
/**
* Datatypes
*/
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
export default class LessonPolicy extends BasePolicy {
public async view(user: User, lesson: Lesson) {
/**
* Load required data to check permissions
*/
await user.load(loader => loader.load('roles').load('courses'));
await lesson.load('course');
const isAdminOrTeacher = RoleHelper.userContainRoles(user.roles, [RoleEnum.ADMIN, RoleEnum.TEACHER]);
if (isAdminOrTeacher) {
return true;
}
return !!user.courses.find(course => course.id === lesson.course.id);
}
}

View File

@ -0,0 +1,36 @@
import { BasePolicy } from '@ioc:Adonis/Addons/Bouncer';
/**
* Models
*/
import User from 'App/Models/User';
/**
* Helpers
*/
import RoleHelper from 'App/Helpers/RoleHelper';
/**
* Datatypes
*/
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
export default class RolePolicy extends BasePolicy {
public async manage(user: User, roles: Array<RoleEnum>) {
await user.load('roles');
const isAdmin = RoleHelper.userContainRoles(user.roles, [RoleEnum.ADMIN]);
const isTeacher = RoleHelper.userContainRoles(user.roles, [RoleEnum.TEACHER]);
if (isAdmin) {
return true;
}
/**
* User with role `TEACHER` can manage only user with `STUDENT` role
*/
if (!(roles.includes(RoleEnum.TEACHER) || roles.includes(RoleEnum.ADMIN)) && isTeacher) {
return true;
}
}
}

View File

@ -234,9 +234,9 @@ export default class CourseService {
/** /**
* Find course teacher * Find course teacher
*/ */
const teacher = await this.userRepository.getById(data.teacher_id); const user = await this.userRepository.getById(data.teacher_id);
if (!teacher) { if (!user) {
return { return {
success: false, success: false,
status: HttpStatusEnum.NOT_FOUND, status: HttpStatusEnum.NOT_FOUND,
@ -248,13 +248,13 @@ export default class CourseService {
}; };
} }
const isTeacher = RoleHelper.userHasRoles(teacher.roles, [RoleEnum.TEACHER]); const isHasPermissions = RoleHelper.userContainRoles(user.roles, [RoleEnum.ADMIN, RoleEnum.TEACHER]);
if (!isTeacher) { if (!isHasPermissions) {
return { return {
success: false, success: false,
status: HttpStatusEnum.BAD_REQUEST, status: HttpStatusEnum.BAD_REQUEST,
message: `User with id "${teacher.id}" not a teacher.`, message: `User with id "${user.id}" does not have sufficient permissions.`,
data: {}, data: {},
error: { error: {
code: 'E_BAD_REQUEST', code: 'E_BAD_REQUEST',
@ -359,6 +359,10 @@ export default class CourseService {
}; };
} }
// if (data.teacher_id) {
// }
return { return {
success: true, success: true,
status: HttpStatusEnum.OK, status: HttpStatusEnum.OK,

View File

@ -78,20 +78,7 @@ export default class LessonService {
}; };
} }
/** await ctx.bouncer.with('LessonPolicy').authorize('view', lesson);
* Allow user to view lesson content
*/
if (await ctx.bouncer.denies('viewLessonContent', lesson)) {
return {
success: false,
status: HttpStatusEnum.FORBIDDEN,
message: 'The user is not a student of this course.',
data: {},
error: {
code: 'E_FORBIDDEN',
},
};
}
/** /**
* Load lesson with materials * Load lesson with materials
@ -109,7 +96,8 @@ export default class LessonService {
/** /**
* Fetch lesson material by file name * Fetch lesson material by file name
* *
* @param fileName Name of file * @param ctx Http context
* @param name Filename
* @returns Response * @returns Response
*/ */
public async fetchMaterialFile(ctx: HttpContextContract, name: string): Promise<IResponse<LessonMaterial>> { public async fetchMaterialFile(ctx: HttpContextContract, name: string): Promise<IResponse<LessonMaterial>> {
@ -131,18 +119,7 @@ export default class LessonService {
* Allow user to view lesson material * Allow user to view lesson material
*/ */
await material.load('lesson'); await material.load('lesson');
await ctx.bouncer.with('LessonPolicy').authorize('view', material.lesson);
if (await ctx.bouncer.denies('viewLessonContent', material.lesson)) {
return {
success: false,
status: HttpStatusEnum.FORBIDDEN,
message: 'You are not able to view this file.',
data: {},
error: {
code: 'E_FORBIDDEN',
},
};
}
return { return {
success: true, success: true,
@ -168,18 +145,7 @@ export default class LessonService {
} }
await video.load('lesson'); await video.load('lesson');
await ctx.bouncer.with('LessonPolicy').authorize('view', video.lesson);
if (await ctx.bouncer.denies('viewLessonContent', video.lesson)) {
return {
success: false,
status: HttpStatusEnum.FORBIDDEN,
message: 'You are not able to view this file.',
data: {},
error: {
code: 'E_FORBIDDEN',
},
};
}
return { return {
success: true, success: true,
@ -211,17 +177,7 @@ export default class LessonService {
}; };
} }
if (await ctx.bouncer.denies('viewLessonContent', lesson)) { await ctx.bouncer.with('LessonPolicy').authorize('view', lesson);
return {
success: false,
status: HttpStatusEnum.FORBIDDEN,
message: 'The user is not a student of this course.',
data: {},
error: {
code: 'E_FORBIDDEN',
},
};
}
const progress = await this.lessonProgressRepository.get(user.id, lesson.id); const progress = await this.lessonProgressRepository.get(user.id, lesson.id);
@ -354,20 +310,7 @@ export default class LessonService {
}; };
} }
/** await ctx.bouncer.with('LessonPolicy').authorize('view', lesson);
* Allow user to view lesson content
*/
if (await ctx.bouncer.denies('viewLessonContent', lesson)) {
return {
success: false,
status: HttpStatusEnum.FORBIDDEN,
message: 'The user is not a student of this course.',
data: {},
error: {
code: 'E_FORBIDDEN',
},
};
}
/** /**
* Load lesson with materials * Load lesson with materials

View File

@ -27,6 +27,7 @@ import UserRepository from 'App/Repositories/UserRepository';
*/ */
import CreateUserValidator from 'App/Validators/User/CreateUserValidator'; import CreateUserValidator from 'App/Validators/User/CreateUserValidator';
import UpdateUserValidator from 'App/Validators/User/UpdateUserValidator'; import UpdateUserValidator from 'App/Validators/User/UpdateUserValidator';
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
@inject() @inject()
export default class UserService { export default class UserService {
@ -120,20 +121,7 @@ export default class UserService {
}; };
} }
/** await ctx.bouncer.with('RolePolicy').authorize('manage', [role.slug as RoleEnum]);
* Check user permissions
*/
if (await ctx.bouncer.denies('manageUserRole', role)) {
return {
success: false,
status: HttpStatusEnum.FORBIDDEN,
message: 'You dont have permissions to permorm that action',
data: {},
error: {
code: 'E_FORBIDDEN',
},
};
}
/** /**
* Create new user * Create new user
@ -166,7 +154,7 @@ export default class UserService {
data: UpdateUserValidator['schema']['props'], data: UpdateUserValidator['schema']['props'],
ctx: HttpContextContract ctx: HttpContextContract
): Promise<IResponse> { ): Promise<IResponse> {
const user = await this.userRepository.update(id, data); const user = await this.userRepository.getById(id);
if (!user) { if (!user) {
return { return {
@ -180,6 +168,9 @@ export default class UserService {
}; };
} }
await ctx.bouncer.with('RolePolicy').authorize('manage', user.roles.map(r => r.slug) as [RoleEnum]);
await this.userRepository.update(id, data);
/** /**
* Update user role * Update user role
*/ */
@ -187,18 +178,7 @@ export default class UserService {
const role = await this.roleRepository.getBySlug(data.role); const role = await this.roleRepository.getBySlug(data.role);
if (role) { if (role) {
if (await ctx.bouncer.denies('manageUserRole', role)) { await ctx.bouncer.with('RolePolicy').authorize('manage', [data.role]);
return {
success: false,
status: HttpStatusEnum.FORBIDDEN,
message: 'You dont have permissions to permorm that action',
data: {},
error: {
code: 'E_FORBIDDEN',
},
};
}
await this.userRepository.updateRoles(user, [role]); await this.userRepository.updateRoles(user, [role]);
} }
} }
@ -217,8 +197,8 @@ export default class UserService {
* @param id User id * @param id User id
* @returns Response * @returns Response
*/ */
public async deleteUser(id: string | number): Promise<IResponse> { public async deleteUser(id: string | number, ctx: HttpContextContract): Promise<IResponse> {
const user = await this.userRepository.delete(id); const user = await this.userRepository.getById(id);
if (!user) { if (!user) {
return { return {
@ -232,6 +212,9 @@ export default class UserService {
}; };
} }
await ctx.bouncer.with('RolePolicy').authorize('manage', user.roles.map(r => r.slug) as [RoleEnum]);
await this.userRepository.delete(id);
return { return {
success: true, success: true,
status: HttpStatusEnum.OK, status: HttpStatusEnum.OK,

View File

@ -6,11 +6,6 @@
*/ */
import Bouncer from '@ioc:Adonis/Addons/Bouncer'; import Bouncer from '@ioc:Adonis/Addons/Bouncer';
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
import RoleHelper from 'App/Helpers/RoleHelper';
import Lesson from 'App/Models/Lesson';
import Role from 'App/Models/Role';
import User from 'App/Models/User';
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -34,22 +29,7 @@ import User from 'App/Models/User';
| NOTE: Always export the "actions" const from this file | NOTE: Always export the "actions" const from this file
|**************************************************************** |****************************************************************
*/ */
export const { actions } = Bouncer.define('manageUserRole', async (user: User, role: Role) => { export const { actions } = Bouncer;
await user.load('roles');
return !(
(role.slug === RoleEnum.ADMIN || role.slug === RoleEnum.TEACHER) &&
!RoleHelper.userContainRoles(user.roles, [RoleEnum.ADMIN])
);
}).define('viewLessonContent', async (user: User, lesson: Lesson) => {
await user.load(loader => loader.load('roles').load('courses'));
await lesson.load('course');
if (!RoleHelper.userContainRoles(user.roles, [RoleEnum.ADMIN, RoleEnum.TEACHER])) {
return !!user.courses.find(course => course.id === lesson.course.id);
}
return true;
});
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -74,4 +54,7 @@ export const { actions } = Bouncer.define('manageUserRole', async (user: User, r
| NOTE: Always export the "policies" const from this file | NOTE: Always export the "policies" const from this file
|**************************************************************** |****************************************************************
*/ */
export const { policies } = Bouncer.registerPolicies({}); export const { policies } = Bouncer.registerPolicies({
LessonPolicy: () => import('App/Policies/LessonPolicy'),
RolePolicy: () => import('App/Policies/RolePolicy'),
});

View File

@ -7,7 +7,7 @@ Route.group(() => {
Route.patch('/:id', 'Api/v1/UsersController.update').middleware('role:admin,teacher').as('users.update'); Route.patch('/:id', 'Api/v1/UsersController.update').middleware('role:admin,teacher').as('users.update');
Route.delete('/:id', 'Api/v1/UsersController.delete').middleware('role:admin,teacher').as('users.delete'); Route.delete('/:id', 'Api/v1/UsersController.delete').middleware('role:admin,teacher').as('users.delete');
Route.post('/:id/attach-roles', 'Api/v1/UsersController.attachRoles') Route.post('/:id/attach-roles', 'Api/v1/UsersController.attachRoles')
.middleware('role:admin,teacher') .middleware('role:admin')
.as('users.attach-role'); .as('users.attach-role');
Route.delete('/:id/detach-roles', 'Api/v1/UsersController.detachRoles') Route.delete('/:id/detach-roles', 'Api/v1/UsersController.detachRoles')
.middleware('role:admin') .middleware('role:admin')