Skip to content

Commit

Permalink
feat: rbac in database (#215)
Browse files Browse the repository at this point in the history
This merge commit integrates the RBAC feature, adding entities, migration scripts, seeder, service functions, endpoints, and various fixes and improvements. It also includes extensive test coverage for roles and permissions, and several refactorings and code cleanups.
  • Loading branch information
Yoronex authored Jul 24, 2024
1 parent e7217ac commit 1443ed9
Show file tree
Hide file tree
Showing 72 changed files with 3,687 additions and 1,342 deletions.
10 changes: 5 additions & 5 deletions src/controller/authentication-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -558,12 +558,12 @@ export default class AuthenticationController extends BaseController {

try {
const user = await User.findOne({ where: { id: body.userId } });
const contents = await AuthenticationService.makeJsonWebToken(
{ tokenHandler: this.tokenHandler, roleManager: this.roleManager }, user, false,
const response = await AuthenticationService.getSaltedToken(
user,
{ tokenHandler: this.tokenHandler, roleManager: this.roleManager },
false,
body.nonce,
);
const token = await this.tokenHandler.signToken(contents, body.nonce);
const response = AuthenticationService
.asAuthenticationResponse(contents.user, contents.roles, contents.organs, token);
res.json(response);
} catch (error) {
this.logger.error('Could not create token:', error);
Expand Down
2 changes: 1 addition & 1 deletion src/controller/payout-request-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { PayoutRequestState } from '../entity/transactions/payout-request-status
import PayoutRequestRequest from './request/payout-request-request';
import User from '../entity/user/user';
import BalanceService from '../service/balance-service';
import FileService from "../service/file-service";
import FileService from '../service/file-service';

export default class PayoutRequestController extends BaseController {
private logger: Logger = log4js.getLogger('PayoutRequestController');
Expand Down
311 changes: 303 additions & 8 deletions src/controller/rbac-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

import { Request, Response } from 'express';
import { Response } from 'express';
import log4js, { Logger } from 'log4js';
import BaseController, { BaseControllerOptions } from './base-controller';
import Policy from './policy';
import RBACService from '../service/rbac-service';
import { RequestWithToken } from '../middleware/token-middleware';
import { CreatePermissionParams, UpdateRoleRequest } from './request/rbac-request';
import { verifyCreatePermissionRequest, verifyUpdateRoleRequest } from './request/validators/rbac-request-spec';
import { isFail } from '../helpers/specification-validation';
import Permission from '../entity/rbac/permission';

export default class RbacController extends BaseController {
private logger: Logger = log4js.getLogger('RbacController');
Expand All @@ -42,35 +47,325 @@ export default class RbacController extends BaseController {
'/roles': {
GET: {
policy: async () => true,
handler: this.returnAllRoles.bind(this),
handler: this.getAllRoles.bind(this),
},
POST: {
policy: async (req) => this.roleManager.can(req.token.roles, 'create', 'all', 'Role', ['*']),
handler: this.createRole.bind(this),
body: { modelName: 'UpdateRoleRequest' },
},
},
'/roles/:id(\\d+)': {
GET: {
policy: async () => true,
handler: this.getSingleRole.bind(this),
},
PATCH: {
policy: async (req) => this.roleManager.can(req.token.roles, 'update', 'all', 'Role', ['*']),
handler: this.updateRole.bind(this),
body: { modelName: 'UpdateRoleRequest' },
},
DELETE: {
policy: async (req) => this.roleManager.can(req.token.roles, 'delete', 'all', 'Role', ['*']),
handler: this.deleteRole.bind(this),
},
},
'/roles/:id(\\d+)/permissions': {
POST: {
policy: async (req) => this.roleManager.can(req.token.roles, 'create', 'all', 'Permission', ['*']),
handler: this.addPermissions.bind(this),
body: { modelName: 'CreatePermissionsRequest' },
},
},
'/roles/:id(\\d+)/permissions/:entity/:action/:relation': {
DELETE: {
policy: async (req) => this.roleManager.can(req.token.roles, 'delete', 'all', 'Permission', ['*']),
handler: this.deletePermission.bind(this),
},
},
};
}

/**
* GET /rbac/roles
* @summary Returns all existing roles
* @summary Get all existing roles
* @operationId getAllRoles
* @tags rbac - Operations of rbac controller
* @security JWT
* @return {Array.<RoleResponse>} 200 - All existing roles
* @return {string} 500 - Internal server error
*/
public async returnAllRoles(req: Request, res: Response): Promise<void> {
const { body } = req;
this.logger.trace('Get all roles', body);
public async getAllRoles(req: RequestWithToken, res: Response): Promise<void> {
this.logger.trace('Get all roles by user', req.token.user);

// handle request
try {
const roles = this.roleManager.getRegisteredRoles();
const [roles] = await RBACService.getRoles();

// Map every role to response
const responses = RBACService.asRoleResponse(roles);
const responses = roles.map((r) => RBACService.asRoleResponse(r));
res.json(responses);
} catch (error) {
this.logger.error('Could not return all roles:', error);
res.status(500).json('Internal server error.');
}
}

/**
* GET /rbac/roles/{id}
* @summary Get a single existing role with its permissions
* @operationId getSingleRole
* @tags rbac - Operations of the rbac controller
* @param {integer} id.path.required - The ID of the role that should be returned
* @security JWT
* @return {RoleWithPermissionsResponse} 200 - Role with its permissions
* @return {string} 404 - Role not found error
*/
public async getSingleRole(req: RequestWithToken, res: Response): Promise<void> {
const { id } = req.params;
this.logger.trace('Get single role', id, 'by user', req.token.user);

try {
const roleId = Number(id);
const [[role]] = await RBACService.getRoles({ roleId, returnPermissions: true });
if (!role) {
res.status(404).json('Role not found.');
return;
}

res.json(RBACService.asRoleResponse(role));
} catch (error) {
this.logger.error('Could not get single role:', error);
res.status(500).json('Internal server error.');
}
}

/**
* POST /rbac/roles
* @summary Create a new role
* @operationId createRole
* @tags rbac - Operations of the rbac controller
* @param {UpdateRoleRequest} request.body.required - The role which should be created
* @security JWT
* @return {RoleResponse} 200 - The created role
* @return {string} 400 - Validation error
* @return {string} 500 - Internal server error
*/
public async createRole(req: RequestWithToken, res: Response): Promise<void> {
const { body } = req;
this.logger.trace('Create new role by', req.token.user);

try {
const request = { ...body } as UpdateRoleRequest;

const validation = await verifyUpdateRoleRequest(request);
if (isFail(validation)) {
res.status(400).json(validation.fail.value);
return;
}

const role = await RBACService.createRole(request);
const response = RBACService.asRoleResponse(role);
res.json(response);
} catch (error) {
this.logger.error('Could not create role:', error);
res.status(500).json('Internal server error.');
}
}

/**
* PATCH /rbac/roles/{id}
* @summary Update an existing role
* @operationId updateRole
* @tags rbac - Operations of the rbac controller
* @param {integer} id.path.required - The ID of the role which should be updated
* @param {UpdateRoleRequest} request.body.required - The role which should be updated
* @security JWT
* @return {RoleResponse} 200 - The created role
* @return {string} 400 - Validation error
* @return {string} 404 - Role not found error
* @return {string} 500 - Internal server error
*/
public async updateRole(req: RequestWithToken, res: Response): Promise<void> {
const { id } = req.params;
const { body } = req;
this.logger.trace('Update role', id, 'by', req.token.user);

try {
const roleId = Number(id);
const request = { ...body } as UpdateRoleRequest;

const validation = await verifyUpdateRoleRequest(request);
if (isFail(validation)) {
res.status(400).json(validation.fail.value);
return;
}

let [[role]] = await RBACService.getRoles({ roleId }, { take: 1 });
if (!role) {
res.status(404).json('Role not found.');
return;
}
if (role.systemDefault) {
res.status(400).json('Cannot update system default role.');
return;
}

role = await RBACService.updateRole(roleId, request);
const response = RBACService.asRoleResponse(role);
res.json(response);
} catch (error) {
this.logger.error('Could not update role:', error);
res.status(500).json('Internal server error.');
}
}

/**
* DELETE /rbac/roles/{id}
* @summary Delete an existing role
* @operationId deleteRole
* @tags rbac - Operations of the rbac controller
* @param {integer} id.path.required - The ID of the role which should be deleted
* @security JWT
* @return {string} 204 - Success
* @return {string} 404 - Role not found error
* @return {string} 500 - Internal server error
*/
public async deleteRole(req: RequestWithToken, res: Response): Promise<void> {
const { id } = req.params;
this.logger.trace('Delete role', id, 'by', req.token.user);

try {
const roleId = Number(id);

let [[role]] = await RBACService.getRoles({ roleId }, { take: 1 });
if (!role) {
res.status(404).json('Role not found.');
return;
}
if (role.systemDefault) {
res.status(400).json('Cannot delete system default role.');
return;
}

await RBACService.removeRole(roleId);
res.status(204).json();
} catch (error) {
this.logger.error('Could not delete role:', error);
res.status(500).json('Internal server error.');
}
}

/**
* POST /rbac/roles/{id}/permissions
* @summary Add new permissions to an existing role
* @operationId addPermissions
* @tags rbac - Operations of the rbac controller
* @param {integer} id.path.required - The ID of the role which should get the new permissions
* @param {Array.<CreatePermissionParams>} request.body.required - The permissions that need to be added
* @return {Array.<PermissionResponse>} 200 - The created permissions
* @return {string} 400 - Validation error
* @return {string} 404 - Role not found error
* @return {string} 500 - Internal server error
*/
public async addPermissions(req: RequestWithToken, res: Response): Promise<void> {
const { id } = req.params;
const { body } = req;
this.logger.trace('Add permissions to role', id, 'by', req.token.user);

try {
const roleId = Number(id);

let [[role]] = await RBACService.getRoles({ roleId, returnPermissions: true }, { take: 1 });
if (!role) {
res.status(404).json('Role not found.');
return;
}
if (role.systemDefault) {
res.status(400).json('Cannot add permission to system default role.');
return;
}

if (!Array.isArray(body)) {
res.status(404).json('Body should be an array.');
return;
}

const params: CreatePermissionParams[] = [...body];
const validations = await Promise.all(params.map((p) => verifyCreatePermissionRequest(p)));
for (let validation of validations) {
if (isFail(validation)) {
res.status(404).json(validation.fail.value);
return;
}
}

// Check for duplicates in the request body / existing permissions
const invalidPermissions = params.filter((p, index) => {
const existingMatch = RBACService.findPermission(role.permissions, p);
if (existingMatch) return true;
const bodyWithoutCurrentPermission = [...params];
bodyWithoutCurrentPermission.splice(index, 1);
const bodyMatch = RBACService.findPermission(bodyWithoutCurrentPermission, p);
return !!bodyMatch;
});
if (invalidPermissions.length > 0) {
res.status(400).json(`Follow permissions are duplicates. They either already exist as permissions, or are duplicate in the request body: ${JSON.stringify(invalidPermissions)}`);
return;
}

const permissions = await RBACService.addPermissions(roleId, params);
const response = RBACService.asPermissionResponse(permissions);
res.json(response);
return;
} catch (error) {
this.logger.error('Could not add permissions:', error);
res.status(500).json('Internal server error.');
}
}

/**
* DELETE /rbac/roles/{id}/permissions/{entity}/{action}/{relation}
* @summary Delete a permission from an existing role
* @operationId deletePermission
* @tags rbac - Operations of the rbac controller
* @param {integer} id.path.required - The ID of the role
* @param {integer} entity.path.required - The entity of the permission
* @param {integer} action.path.required - The action of the permission
* @param {integer} relation.path.required - The relation of the permission
* @return {string} 204 - Success
* @return {string} 404 - Role not found error
* @return {string} 404 - Permission not found error
* @return {string} 500 - Internal server error
*/
public async deletePermission(req: RequestWithToken, res: Response): Promise<void> {
const { id, action, entity, relation } = req.params;
this.logger.trace('Delete permission', action, relation, entity, 'from role', id, 'by', req.token.user);

try {
const roleId = Number(id);

let [[role]] = await RBACService.getRoles({ roleId }, { take: 1 });
if (!role) {
res.status(404).json('Role not found.');
return;
}
if (role.systemDefault) {
res.status(400).json('Cannot delete permission from system default role.');
return;
}

const permission = await Permission.findOne({ where: { roleId, entity, action, relation } });
if (!permission) {
res.status(404).json('Permission not found.');
return;
}

await RBACService.removePermission(roleId, { entity, action, relation });
res.status(204).json();
} catch (error) {
this.logger.error('Could not delete permission:', error);
res.status(500).json('Internal server error.');
}
}
}
Loading

0 comments on commit 1443ed9

Please sign in to comment.