• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

GEWIS / sudosos-backend / 25753937432

12 May 2026 09:17AM UTC coverage: 88.117% (-1.0%) from 89.089%
25753937432

push

github

web-flow
chore(deps): fix missing dependencies for running docs:dev (#911)

3925 of 4574 branches covered (85.81%)

Branch coverage included in aggregate %.

20093 of 22683 relevant lines covered (88.58%)

1125.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.4
/src/controller/rbac-controller.ts
1
/**
1✔
2
 *  SudoSOS back-end API service.
3
 *  Copyright (C) 2026 Study association GEWIS
4
 *
5
 *  This program is free software: you can redistribute it and/or modify
6
 *  it under the terms of the GNU Affero General Public License as published
7
 *  by the Free Software Foundation, either version 3 of the License, or
8
 *  (at your option) any later version.
9
 *
10
 *  This program is distributed in the hope that it will be useful,
11
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 *  GNU Affero General Public License for more details.
14
 *
15
 *  You should have received a copy of the GNU Affero General Public License
16
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
17
 *
18
 *  @license
19
 */
1✔
20

21
/**
1✔
22
 * This is the module page of rbac-controller.
23
 *
24
 * @module rbac
25
 */
1✔
26

27
import { Response } from 'express';
28
import log4js, { Logger } from 'log4js';
29
import BaseController, { BaseControllerOptions } from './base-controller';
30
import Policy from './policy';
31
import RBACService from '../service/rbac-service';
32
import { RequestWithToken } from '../middleware/token-middleware';
33
import { CreatePermissionParams, UpdateRoleRequest } from './request/rbac-request';
34
import { createPermissionsRequestSpecFactory, updateRoleRequestSpecFactory } from './request/validators/rbac-request-spec';
35
import { globalAsyncValidatorRegistry } from '../middleware/async-validator-registry';
36
import Permission from '../entity/rbac/permission';
37
import { parseRequestPagination, toResponse } from '../helpers/pagination';
38
import { asUserResponse } from '../service/user-service';
39

40
export default class RbacController extends BaseController {
1✔
41
  private logger: Logger = log4js.getLogger('RbacController');
1✔
42

43
  /**
1✔
44
   * Creates a new rbac controller instance.
45
   * @param options - The options passed to the base controller.
46
   */
1✔
47
  public constructor(options: BaseControllerOptions) {
1✔
48
    super(options);
2✔
49
    this.configureLogger(this.logger);
2✔
50
    globalAsyncValidatorRegistry.register('UpdateRoleRequest', updateRoleRequestSpecFactory);
2✔
51
    globalAsyncValidatorRegistry.register('CreatePermissionsRequest', createPermissionsRequestSpecFactory);
2✔
52
  }
2✔
53

54
  /**
1✔
55
   * @inheritdoc
56
   */
1✔
57
  public getPolicy(): Policy {
1✔
58
    return {
2✔
59
      '/roles': {
2✔
60
        GET: {
2✔
61
          policy: async () => true,
2✔
62
          handler: this.getAllRoles.bind(this),
2✔
63
        },
2✔
64
        POST: {
2✔
65
          policy: async (req) => this.roleManager.can(req.token.roles, 'create', 'all', 'Role', ['*']),
2✔
66
          handler: this.createRole.bind(this),
2✔
67
          body: { modelName: 'UpdateRoleRequest' },
2✔
68
        },
2✔
69
      },
2✔
70
      '/roles/:id(\\d+)': {
2✔
71
        GET: {
2✔
72
          policy: async () => true,
2✔
73
          handler: this.getSingleRole.bind(this),
2✔
74
        },
2✔
75
        PATCH: {
2✔
76
          policy: async (req) => this.roleManager.can(req.token.roles, 'update', 'all', 'Role', ['*']),
2✔
77
          handler: this.updateRole.bind(this),
2✔
78
          body: { modelName: 'UpdateRoleRequest' },
2✔
79
        },
2✔
80
        DELETE: {
2✔
81
          policy: async (req) => this.roleManager.can(req.token.roles, 'delete', 'all', 'Role', ['*']),
2✔
82
          handler: this.deleteRole.bind(this),
2✔
83
        },
2✔
84
      },
2✔
85
      '/roles/:id(\\d+)/users': {
2✔
86
        GET: {
2✔
87
          policy: async (req) => this.roleManager.can(
2✔
88
            req.token.roles, 'get', 'all', 'User', ['*'],
2✔
89
          ),
90
          handler: this.getRoleUsers.bind(this),
2✔
91
        },
2✔
92
      },
2✔
93
      '/roles/:id(\\d+)/permissions': {
2✔
94
        POST: {
2✔
95
          policy: async (req) => this.roleManager.can(req.token.roles, 'create', 'all', 'Permission', ['*']),
2✔
96
          handler: this.addPermissions.bind(this),
2✔
97
          body: { modelName: 'CreatePermissionsRequest' },
2✔
98
        },
2✔
99
      },
2✔
100
      '/roles/:id(\\d+)/permissions/:entity/:action/:relation': {
2✔
101
        DELETE: {
2✔
102
          policy: async (req) => this.roleManager.can(req.token.roles, 'delete', 'all', 'Permission', ['*']),
2✔
103
          handler: this.deletePermission.bind(this),
2✔
104
        },
2✔
105
      },
2✔
106
    };
2✔
107
  }
2✔
108

109
  /**
1✔
110
   * GET /rbac/roles
111
   * @summary Get all existing roles
112
   * @operationId getAllRoles
113
   * @tags rbac - Operations of rbac controller
114
   * @security JWT
115
   * @return {Array.<RoleResponse>} 200 - All existing roles
116
   * @return {string} 500 - Internal server error
117
   */
1✔
118
  public async getAllRoles(req: RequestWithToken, res: Response): Promise<void> {
1✔
119
    this.logger.trace('Get all roles by user', req.token.user);
3✔
120

121
    // handle request
3✔
122
    try {
3✔
123
      const [roles] = await RBACService.getRoles();
3✔
124

125
      // Map every role to response
3✔
126
      const responses = roles.map((r) => RBACService.asRoleResponse(r));
3✔
127
      res.json(responses);
3✔
128
    } catch (error) {
3!
129
      this.logger.error('Could not return all roles:', error);
×
130
      res.status(500).json('Internal server error.');
×
131
    }
×
132
  }
3✔
133

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

148
    try {
3✔
149
      const roleId = Number(id);
3✔
150
      const [[role]] = await RBACService.getRoles({ roleId, returnPermissions: true });
3✔
151
      if (!role) {
3✔
152
        res.status(404).json('Role not found.');
1✔
153
        return;
1✔
154
      }
1✔
155

156
      res.json(RBACService.asRoleResponse(role));
2✔
157
    } catch (error) {
3!
158
      this.logger.error('Could not get single role:', error);
×
159
      res.status(500).json('Internal server error.');
×
160
    }
×
161
  }
3✔
162

163
  /**
1✔
164
   * GET /rbac/roles/{id}/users
165
   * @summary Get all users linked to a specific role
166
   * @operationId getRoleUsers
167
   * @tags rbac - Operations of the rbac controller
168
   * @param {integer} id.path.required - The ID of the role that the users are linked to
169
   * @security JWT
170
   * @return {PaginatedUserResponse} 200 - A list of all users linked to this role
171
   * @return {string} 404 - Role not found error
172
   */
1✔
173
  public async getRoleUsers(req: RequestWithToken, res: Response): Promise<void> {
1✔
174
    const { id } = req.params;
2✔
175
    this.logger.trace('Get all users of role', id, 'by user', req.token.user);
2✔
176

177
    try {
2✔
178
      const roleId = Number(id);
2✔
179

180
      const [[role]] = await RBACService.getRoles({ roleId });
2✔
181
      if (!role) {
2✔
182
        res.status(404).json('Role not found.');
1✔
183
        return;
1✔
184
      }
1✔
185

186
      const pagination = parseRequestPagination(req);
1✔
187
      const [users, count] = await RBACService.getRoleUsers(roleId, pagination);
1✔
188
      const records = users.map((u) => asUserResponse(u, true));
1✔
189
      res.json(toResponse(records, count, pagination));
1✔
190
    } catch (error) {
2!
191
      this.logger.error('Could not get users of role:', error);
×
192
      res.status(500).json('Internal server error.');
×
193
    }
×
194
  }
2✔
195

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

211
    try {
1✔
212
      const request = { ...body } as UpdateRoleRequest;
1✔
213

214
      const role = await RBACService.createRole(request);
1✔
215
      const response = RBACService.asRoleResponse(role);
1✔
216
      res.json(response);
1✔
217
    } catch (error) {
1!
218
      this.logger.error('Could not create role:', error);
×
219
      res.status(500).json('Internal server error.');
×
220
    }
×
221
  }
1✔
222

223
  /**
1✔
224
   * PATCH /rbac/roles/{id}
225
   * @summary Update an existing role
226
   * @operationId updateRole
227
   * @tags rbac - Operations of the rbac controller
228
   * @param {integer} id.path.required - The ID of the role which should be updated
229
   * @param {UpdateRoleRequest} request.body.required - The role which should be updated
230
   * @security JWT
231
   * @return {RoleResponse} 200 - The created role
232
   * @return {string} 400 - Validation error
233
   * @return {string} 404 - Role not found error
234
   * @return {string} 500 - Internal server error
235
   */
1✔
236
  public async updateRole(req: RequestWithToken, res: Response): Promise<void> {
1✔
237
    const { id } = req.params;
3✔
238
    const { body } = req;
3✔
239
    this.logger.trace('Update role', id, 'by', req.token.user);
3✔
240

241
    try {
3✔
242
      const roleId = Number(id);
3✔
243
      const request = { ...body } as UpdateRoleRequest;
3✔
244

245
      let [[role]] = await RBACService.getRoles({ roleId }, { take: 1 });
3✔
246
      if (!role) {
3✔
247
        res.status(404).json('Role not found.');
1✔
248
        return;
1✔
249
      }
1✔
250
      if (role.systemDefault) {
3✔
251
        res.status(400).json('Cannot update system default role.');
1✔
252
        return;
1✔
253
      }
1✔
254

255
      role = await RBACService.updateRole(roleId, request);
1✔
256
      const response = RBACService.asRoleResponse(role);
1✔
257
      res.json(response);
1✔
258
    } catch (error) {
3!
259
      this.logger.error('Could not update role:', error);
×
260
      res.status(500).json('Internal server error.');
×
261
    }
×
262
  }
3✔
263

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

279
    try {
3✔
280
      const roleId = Number(id);
3✔
281

282
      let [[role]] = await RBACService.getRoles({ roleId }, { take: 1 });
3✔
283
      if (!role) {
3✔
284
        res.status(404).json('Role not found.');
1✔
285
        return;
1✔
286
      }
1✔
287
      if (role.systemDefault) {
3✔
288
        res.status(400).json('Cannot delete system default role.');
1✔
289
        return;
1✔
290
      }
1✔
291

292
      await RBACService.removeRole(roleId);
1✔
293
      res.status(204).json();
1✔
294
    } catch (error) {
3!
295
      this.logger.error('Could not delete role:', error);
×
296
      res.status(500).json('Internal server error.');
×
297
    }
×
298
  }
3✔
299

300
  /**
1✔
301
   * POST /rbac/roles/{id}/permissions
302
   * @summary Add new permissions to an existing role
303
   * @operationId addPermissions
304
   * @security JWT
305
   * @tags rbac - Operations of the rbac controller
306
   * @param {integer} id.path.required - The ID of the role which should get the new permissions
307
   * @param {Array.<CreatePermissionParams>} request.body.required - The permissions that need to be added
308
   * @return {Array.<PermissionResponse>} 200 - The created permissions
309
   * @return {string} 400 - Validation error
310
   * @return {string} 404 - Role not found error
311
   * @return {string} 500 - Internal server error
312
   */
1✔
313
  public async addPermissions(req: RequestWithToken, res: Response): Promise<void> {
1✔
314
    const { id } = req.params;
6✔
315
    const { body } = req;
6✔
316
    this.logger.trace('Add permissions to role', id, 'by', req.token.user);
6✔
317

318
    try {
6✔
319
      const roleId = Number(id);
6✔
320

321
      let [[role]] = await RBACService.getRoles({ roleId, returnPermissions: true }, { take: 1 });
6✔
322
      if (!role) {
6✔
323
        res.status(404).json('Role not found.');
1✔
324
        return;
1✔
325
      }
1✔
326
      if (role.systemDefault) {
6✔
327
        res.status(400).json('Cannot add permission to system default role.');
1✔
328
        return;
1✔
329
      }
1✔
330

331
      const params: CreatePermissionParams[] = [...body];
4✔
332

333
      // Check for duplicates in the request body / existing permissions
4✔
334
      const invalidPermissions = params.filter((p, index) => {
4✔
335
        const existingMatch = RBACService.findPermission(role.permissions, p);
11✔
336
        if (existingMatch) return true;
11✔
337
        const bodyWithoutCurrentPermission = [...params];
8✔
338
        bodyWithoutCurrentPermission.splice(index, 1);
8✔
339
        const bodyMatch = RBACService.findPermission(bodyWithoutCurrentPermission, p);
8✔
340
        return !!bodyMatch;
8✔
341
      });
8✔
342
      if (invalidPermissions.length > 0) {
6✔
343
        res.status(400).json(`Follow permissions are duplicates. They either already exist as permissions, or are duplicate in the request body: ${JSON.stringify(invalidPermissions)}`);
2✔
344
        return;
2✔
345
      }
2✔
346

347
      const permissions = await RBACService.addPermissions(roleId, params);
2✔
348
      const response = RBACService.asPermissionResponse(permissions);
2✔
349
      res.json(response);
2✔
350
      return;
2✔
351
    } catch (error) {
6!
352
      this.logger.error('Could not add permissions:', error);
×
353
      res.status(500).json('Internal server error.');
×
354
    }
×
355
  }
6✔
356

357
  /**
1✔
358
   * DELETE /rbac/roles/{id}/permissions/{entity}/{action}/{relation}
359
   * @summary Delete a permission from an existing role
360
   * @operationId deletePermission
361
   * @security JWT
362
   * @tags rbac - Operations of the rbac controller
363
   * @param {integer} id.path.required - The ID of the role
364
   * @param {string} entity.path.required - The entity of the permission
365
   * @param {string} action.path.required - The action of the permission
366
   * @param {string} relation.path.required - The relation of the permission
367
   * @return {string} 204 - Success
368
   * @return {string} 404 - Role not found error
369
   * @return {string} 404 - Permission not found error
370
   * @return {string} 500 - Internal server error
371
   */
1✔
372
  public async deletePermission(req: RequestWithToken, res: Response): Promise<void> {
1✔
373
    const { id, action, entity, relation } = req.params;
4✔
374
    this.logger.trace('Delete permission', action, relation, entity, 'from role', id, 'by', req.token.user);
4✔
375

376
    try {
4✔
377
      const roleId = Number(id);
4✔
378

379
      let [[role]] = await RBACService.getRoles({ roleId }, { take: 1 });
4✔
380
      if (!role) {
4✔
381
        res.status(404).json('Role not found.');
1✔
382
        return;
1✔
383
      }
1✔
384
      if (role.systemDefault) {
4✔
385
        res.status(400).json('Cannot delete permission from system default role.');
1✔
386
        return;
1✔
387
      }
1✔
388

389
      const permission = await Permission.findOne({ where: { roleId, entity, action, relation } });
2✔
390
      if (!permission) {
4✔
391
        res.status(404).json('Permission not found.');
1✔
392
        return;
1✔
393
      }
1✔
394

395
      await RBACService.removePermission(roleId, { entity, action, relation });
1✔
396
      res.status(204).json();
1✔
397
    } catch (error) {
4!
398
      this.logger.error('Could not delete permission:', error);
×
399
      res.status(500).json('Internal server error.');
×
400
    }
×
401
  }
4✔
402
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc