From 0a82574eb46c5586b8291e2f66e104b7bdaa99d4 Mon Sep 17 00:00:00 2001 From: fserres Date: Mon, 30 Sep 2024 11:24:12 -0400 Subject: [PATCH] Add permissions --- server/config/roles.json | 27 +++++++++++++++++++++++++++ server/constants/errorCodes.js | 5 +++++ server/controllers/users.js | 9 +++++---- server/middleware/jwtToken.js | 4 ++-- server/middleware/rbac.js | 23 +++++++++++++++++++++++ server/models/permissions.js | 13 +++++++++++++ server/models/role.js | 30 ++++++++++++++++++++++++++++++ server/models/users.js | 3 ++- server/routers/folders.js | 16 ++++++++-------- server/routers/images.js | 3 ++- server/routers/quiz.js | 21 +++++++++++---------- server/routers/users.js | 3 ++- server/socket/socket.js | 5 +++++ 13 files changed, 135 insertions(+), 27 deletions(-) create mode 100644 server/config/roles.json create mode 100644 server/middleware/rbac.js create mode 100644 server/models/permissions.js create mode 100644 server/models/role.js diff --git a/server/config/roles.json b/server/config/roles.json new file mode 100644 index 0000000..3195e49 --- /dev/null +++ b/server/config/roles.json @@ -0,0 +1,27 @@ +{ + "roles": [ + { + "name": "admin", + "permissions": [ + "crud_quiz", + "crud_folders", + "crud_images", + "crud_user" + ] + }, + { + "name": "teacher", + "permissions": [ + "crud_quiz", + "crud_folders", + "crud_images" + ] + }, + { + "name": "student", + "permissions": [ + "participate_quiz" + ] + } + ] +} \ No newline at end of file diff --git a/server/constants/errorCodes.js b/server/constants/errorCodes.js index d7ca180..c9db790 100644 --- a/server/constants/errorCodes.js +++ b/server/constants/errorCodes.js @@ -7,6 +7,11 @@ exports.UNAUTHORIZED_INVALID_TOKEN = { code: 401 } +exports.UNAUTHORIZED_PERMISSION_MISSING = { + message: 'Accès refusé. Vous n\'avez pas les permissions nécessaires.', + code: 403 +} + exports.MISSING_REQUIRED_PARAMETER = { message: 'Paramètre requis manquant.', code: 400 diff --git a/server/controllers/users.js b/server/controllers/users.js index 4494f1d..e7578d7 100644 --- a/server/controllers/users.js +++ b/server/controllers/users.js @@ -9,13 +9,13 @@ class UsersController { async register(req, res, next) { try { - const { email, password } = req.body; + const { email, password, role } = req.body; if (!email || !password) { throw new AppError(MISSING_REQUIRED_PARAMETER); } - await model.register(email, password); + await model.register(email, password, role); emailer.registerConfirmation(email) @@ -43,11 +43,12 @@ class UsersController { throw new AppError(LOGIN_CREDENTIALS_ERROR); } - const token = jwt.create(user.email, user._id); + const token = jwt.create(user.email, user._id, user.role); return res.status(200).json({ token: token, - id: user.email + id: user.email, + role: user.role }); } diff --git a/server/middleware/jwtToken.js b/server/middleware/jwtToken.js index 292e591..b886281 100644 --- a/server/middleware/jwtToken.js +++ b/server/middleware/jwtToken.js @@ -7,8 +7,8 @@ dotenv.config(); class Token { - create(email, userId) { - return jwt.sign({ email, userId }, process.env.JWT_SECRET); + create(email, userId, role) { + return jwt.sign({ email, userId, role }, process.env.JWT_SECRET); } authenticate(req, res, next) { diff --git a/server/middleware/rbac.js b/server/middleware/rbac.js new file mode 100644 index 0000000..2a32d86 --- /dev/null +++ b/server/middleware/rbac.js @@ -0,0 +1,23 @@ +const Permissions = require('../models/permissions'); +const AppError = require("./AppError"); +const { UNAUTHORIZED_PERMISSION_MISSING} = require("../constants/errorCodes"); +const jwt = require("jsonwebtoken"); + +class Rbac { + checkPermission = (...permissions) => { + return (req, res, next) => { + const userRole = req.user ? req.user.role : 'anonymous'; + const userPermissions = Permissions.getPermissionsByRoleName(userRole); + + for (let permission of permissions) { + if (!userPermissions.includes(permission)) { + return next(new AppError(UNAUTHORIZED_PERMISSION_MISSING)); + } + } + + return next(); + }; + }; +} + +module.exports = new Rbac; \ No newline at end of file diff --git a/server/models/permissions.js b/server/models/permissions.js new file mode 100644 index 0000000..b9e9f6d --- /dev/null +++ b/server/models/permissions.js @@ -0,0 +1,13 @@ +const Role = require('../models/role'); + +class Permissions { + constructor() { + this.permissions = []; + } + + getPermissionsByRoleName(roleName) { + const role = Role.getRoleByName(roleName); + return role ? role.permissions : []; + } +} +module.exports = new Permissions; \ No newline at end of file diff --git a/server/models/role.js b/server/models/role.js new file mode 100644 index 0000000..b8cfc0a --- /dev/null +++ b/server/models/role.js @@ -0,0 +1,30 @@ +const roles = require('../config/roles.json'); + +class Role { + constructor() { + this.roles = roles.roles; + } + + getRoleByName(name) { + return this.convertCrud(this.roles.find((role) => role.name === name)); + } + + getRoles() { + return this.roles; + } + + convertCrud(role) { + for (let permission of role.permissions) { + if (permission.startsWith("crud")) { + let resourceName = permission.split("_"); + role.permissions.push("create_" + resourceName[1]); + role.permissions.push("read_" + resourceName[1]); + role.permissions.push("update_" + resourceName[1]); + role.permissions.push("delete_" + resourceName[1]); + } + } + return role; + } +} + +module.exports = new Role; \ No newline at end of file diff --git a/server/models/users.js b/server/models/users.js index caef0a1..13284b7 100644 --- a/server/models/users.js +++ b/server/models/users.js @@ -19,7 +19,7 @@ class Users { return await bcrypt.compare(password, hash) } - async register(email, password) { + async register(email, password, role) { await db.connect() const conn = db.getConnection(); @@ -34,6 +34,7 @@ class Users { const newUser = { email: email, password: await this.hashPassword(password), + role: role, created_at: new Date() }; diff --git a/server/routers/folders.js b/server/routers/folders.js index e64c18a..6dcd141 100644 --- a/server/routers/folders.js +++ b/server/routers/folders.js @@ -3,16 +3,16 @@ const router = express.Router(); const jwt = require('../middleware/jwtToken.js'); const foldersController = require('../controllers/folders.js') +const rbac = require("../middleware/rbac"); -router.post("/create", jwt.authenticate, foldersController.create); -router.get("/getUserFolders", jwt.authenticate, foldersController.getUserFolders); -router.get("/getFolderContent/:folderId", jwt.authenticate, foldersController.getFolderContent); -router.delete("/delete/:folderId", jwt.authenticate, foldersController.delete); -router.put("/rename", jwt.authenticate, foldersController.rename); +router.post("/create", jwt.authenticate, rbac.checkPermission('create_folders'), foldersController.create); +router.get("/getUserFolders", jwt.authenticate, rbac.checkPermission('read_folders'), foldersController.getUserFolders); +router.get("/getFolderContent/:folderId", jwt.authenticate, rbac.checkPermission('read_folders'), foldersController.getFolderContent); +router.delete("/delete/:folderId", jwt.authenticate, rbac.checkPermission('delete_folders'), foldersController.delete); +router.put("/rename", jwt.authenticate, rbac.checkPermission('update_folders'), foldersController.rename); -//router.post("/duplicate", jwt.authenticate, foldersController.duplicate); -router.post("/duplicate", jwt.authenticate, foldersController.duplicate); +router.post("/duplicate", jwt.authenticate, rbac.checkPermission('create_folders', 'read_folders'), foldersController.duplicate); -router.post("/copy/:folderId", jwt.authenticate, foldersController.copy); +router.post("/copy/:folderId", jwt.authenticate, rbac.checkPermission('create_folders', 'read_folders'), foldersController.copy); module.exports = router; \ No newline at end of file diff --git a/server/routers/images.js b/server/routers/images.js index d9b63b0..a695128 100644 --- a/server/routers/images.js +++ b/server/routers/images.js @@ -6,10 +6,11 @@ const imagesController = require('../controllers/images.js') // For getting the image out of the form data const multer = require('multer'); +const rbac = require("../middleware/rbac"); const storage = multer.memoryStorage(); const upload = multer({ storage: storage }); -router.post("/upload", jwt.authenticate, upload.single('image'), imagesController.upload); +router.post("/upload", jwt.authenticate, rbac.checkPermission('create_images'), upload.single('image'), imagesController.upload); router.get("/get/:id", imagesController.get); module.exports = router; \ No newline at end of file diff --git a/server/routers/quiz.js b/server/routers/quiz.js index c0f7ea2..5faa660 100644 --- a/server/routers/quiz.js +++ b/server/routers/quiz.js @@ -3,17 +3,18 @@ const router = express.Router(); const jwt = require('../middleware/jwtToken.js'); const quizController = require('../controllers/quiz.js') +const rbac = require('../middleware/rbac.js'); -router.post("/create", jwt.authenticate, quizController.create); -router.get("/get/:quizId", jwt.authenticate, quizController.get); -router.delete("/delete/:quizId", jwt.authenticate, quizController.delete); -router.put("/update", jwt.authenticate, quizController.update); -router.put("/move", jwt.authenticate, quizController.move); +router.post("/create", jwt.authenticate, rbac.checkPermission('create_quiz'), quizController.create); +router.get("/get/:quizId", jwt.authenticate, rbac.checkPermission('participate_quiz'), quizController.get); +router.delete("/delete/:quizId", jwt.authenticate, rbac.checkPermission('delete_quiz'), quizController.delete); +router.put("/update", jwt.authenticate, rbac.checkPermission('update_quiz'), quizController.update); +router.put("/move", jwt.authenticate, rbac.checkPermission('update_quiz'), quizController.move); -router.post("/duplicate", jwt.authenticate, quizController.duplicate); -router.post("/copy/:quizId", jwt.authenticate, quizController.copy); -router.put("/Share", jwt.authenticate, quizController.Share); -router.get("/getShare/:quizId", jwt.authenticate, quizController.getShare); -router.post("/receiveShare", jwt.authenticate, quizController.receiveShare); +router.post("/duplicate", jwt.authenticate, rbac.checkPermission('create_quiz', 'read_quiz'), quizController.duplicate); +router.post("/copy/:quizId", jwt.authenticate, rbac.checkPermission('create_quiz', 'read_quiz'), quizController.copy); +router.put("/Share", jwt.authenticate, rbac.checkPermission('create_quiz', 'read_quiz'), quizController.Share); +router.get("/getShare/:quizId", jwt.authenticate, rbac.checkPermission('create_quiz', 'read_quiz'), quizController.getShare); +router.post("/receiveShare", jwt.authenticate, rbac.checkPermission('create_quiz', 'read_quiz'), quizController.receiveShare); module.exports = router; \ No newline at end of file diff --git a/server/routers/users.js b/server/routers/users.js index 4e43ca5..6ffb39f 100644 --- a/server/routers/users.js +++ b/server/routers/users.js @@ -3,11 +3,12 @@ const router = express.Router(); const jwt = require('../middleware/jwtToken.js'); const usersController = require('../controllers/users.js') +const rbac = require("../middleware/rbac"); router.post("/register", usersController.register); router.post("/login", usersController.login); router.post("/reset-password", usersController.resetPassword); router.post("/change-password", jwt.authenticate, usersController.changePassword); -router.post("/delete-user", jwt.authenticate, usersController.delete); +router.post("/delete-user", jwt.authenticate, rbac.checkPermission('delete_user'), usersController.delete); module.exports = router; \ No newline at end of file diff --git a/server/socket/socket.js b/server/socket/socket.js index 0d63ab2..90bb7fb 100644 --- a/server/socket/socket.js +++ b/server/socket/socket.js @@ -5,6 +5,7 @@ const setupWebsocket = (io) => { let totalConnections = 0; io.on("connection", (socket) => { + // Get jwt and roles if (totalConnections >= MAX_TOTAL_CONNECTIONS) { console.log("Connection limit reached. Disconnecting client."); socket.emit( @@ -24,6 +25,10 @@ const setupWebsocket = (io) => { ); socket.on("create-room", (sentRoomName) => { + // If roles authorize else send error message + // else { + // socket.emit('access_denied', 'You do not have permission to perform this action'); + // } if (sentRoomName) { const roomName = sentRoomName.toUpperCase(); if (!io.sockets.adapter.rooms.get(roomName)) {