From f9cf3ef20936c3492b7001fcf50eea2c99833834 Mon Sep 17 00:00:00 2001 From: Takeshi Kimata <117462761+kimatata@users.noreply.github.com> Date: Thu, 6 Jun 2024 11:58:40 +0900 Subject: [PATCH] feat: inplement auth for cases --- backend/middleware/auth.js | 153 +------------------- backend/middleware/verifyEditable.js | 205 +++++++++++++++++++++++++++ backend/middleware/verifyVisible.js | 2 +- backend/routes/cases/delete.js | 5 +- backend/routes/cases/edit.js | 5 +- backend/routes/cases/new.js | 5 +- backend/routes/folders/delete.js | 5 +- backend/routes/folders/edit.js | 5 +- backend/routes/folders/new.js | 5 +- backend/routes/members/delete.js | 5 +- backend/routes/members/edit.js | 5 +- backend/routes/members/new.js | 5 +- backend/routes/projects/delete.js | 3 +- backend/routes/projects/edit.js | 3 +- 14 files changed, 238 insertions(+), 173 deletions(-) create mode 100644 backend/middleware/verifyEditable.js diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index c370b79..04b5e8d 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -1,11 +1,7 @@ const jwt = require('jsonwebtoken'); -const { roles, memberRoles, defaultDangerKey } = require('../routes/users/authSettings'); +const { roles, defaultDangerKey } = require('../routes/users/authSettings'); const { DataTypes } = require('sequelize'); const defineUser = require('../models/users'); -const defineMember = require('../models/members'); -const defineProject = require('../models/projects'); -const defineFolder = require('../models/folders'); -const defineCase = require('../models/cases'); function authMiddleware(sequelize) { /** @@ -51,156 +47,9 @@ function authMiddleware(sequelize) { next(); } - /** - * Verify user has project - * (have to be called after verifySignedIn() middleware) - */ - async function verifyProjectOwner(req, res, next) { - const Project = defineProject(sequelize, DataTypes); - - const projectId = req.params.projectId; - if (!projectId) { - return res.status(400).json({ error: 'projectId is required' }); - } - - const project = await Project.findByPk(projectId); - if (!project) { - return res.status(404).send('Project not found'); - } - - if (project.userId !== req.userId) { - return res.status(403).json({ error: 'Forbidden' }); - } - - next(); - } - - /** - * Verify user has permission of project management - * (User must be the owner or manager of the project) - * (have to be called after verifySignedIn() middleware) - */ - async function verifyProjectManager(req, res, next) { - const Project = defineProject(sequelize, DataTypes); - - const projectId = req.params.projectId || req.query.projectId; - if (!projectId) { - return res.status(400).json({ error: 'projectId is required' }); - } - - const project = await Project.findByPk(projectId); - if (!project) { - return res.status(404).send('Project not found'); - } - - if (project.userId === req.userId) { - next(); - return; - } - - // check the user is manager of the project - const Member = defineMember(sequelize, DataTypes); - const member = await Member.findOne({ - where: { - userId: req.userId, - projectId: projectId, - }, - }); - if (member) { - const managerRoleIndex = memberRoles.findIndex((entry) => entry.uid === 'manager'); - if (member.role === managerRoleIndex) { - next(); - return; - } - } - - return res.status(403).json({ error: 'Forbidden' }); - } - - /** - * Verify user has permission of project development - * (User must be the owner or manager or developer of the project) - * (have to be called after verifySignedIn() middleware) - */ - async function verifyProjectDeveloper(req, res, next) { - const Project = defineProject(sequelize, DataTypes); - const Folder = defineFolder(sequelize, DataTypes); - const Case = defineCase(sequelize, DataTypes); - const Member = defineMember(sequelize, DataTypes); - Project.hasMany(Folder, { foreignKey: 'projectId' }); - Folder.hasMany(Case, { foreignKey: 'folderId' }); - Project.hasMany(Member, { foreignKey: 'projectId' }); - - let projectId = req.params.projectId || req.query.projectId; - const folderId = req.params.folderId || req.query.folderId; - const caseId = req.params.caseId || req.query.caseId; - if (!projectId && !folderId && !caseId) { - return res.status(400).json({ error: 'projectId, folderId or caseId is required' }); - } - - if (!projectId) { - if (folderId) { - // find project id from folderId - const folder = await Folder.findByPk(folderId); - if (folder && folder.projectId) { - projectId = folder.projectId; - } else { - return res.status(404).send('failed to find project from folderId'); - } - } - } else if (caseId) { - // find project id from caseId - const testCase = await Case.findByPk(caseId, { - include: { - model: Folder, - include: Project, - }, - }); - if (testCase && testCase.Folder && testCase.Folder.Project) { - projectId = testCase.Folder.Project.id; - } else { - return res.status(404).send('Failed to find project from caseId'); - } - } - - const project = await Project.findOne({ - where: { id: projectId }, - include: [ - { - model: Member, - where: { userId: req.userId }, - required: false, - }, - ], - }); - if (!project) { - return res.status(404).send('Project not found'); - } - - if (project.userId === req.userId) { - next(); - return; - } - - const member = project.Members && project.Members[0]; - if (member) { - const managerRoleIndex = memberRoles.findIndex((entry) => entry.uid === 'manager'); - const developerRoleIndex = memberRoles.findIndex((entry) => entry.uid === 'developer'); - if (member.role === managerRoleIndex || member.role === developerRoleIndex) { - next(); - return; - } - } - - return res.status(403).json({ error: 'Forbidden' }); - } - return { verifySignedIn, verifyAdmin, - verifyProjectOwner, - verifyProjectManager, - verifyProjectDeveloper, }; } diff --git a/backend/middleware/verifyEditable.js b/backend/middleware/verifyEditable.js new file mode 100644 index 0000000..bc7d578 --- /dev/null +++ b/backend/middleware/verifyEditable.js @@ -0,0 +1,205 @@ +const { memberRoles } = require('../routes/users/authSettings'); +const { DataTypes } = require('sequelize'); +const defineMember = require('../models/members'); +const defineProject = require('../models/projects'); +const defineFolder = require('../models/folders'); +const defineCase = require('../models/cases'); + +function verifyEditableMiddleware(sequelize) { + /** + * Verify user has project + * (have to be called after verifySignedIn() middleware) + */ + async function verifyProjectOwner(req, res, next) { + const Project = defineProject(sequelize, DataTypes); + + const projectId = req.params.projectId; + if (!projectId) { + return res.status(400).json({ error: 'projectId is required' }); + } + + const project = await Project.findByPk(projectId); + if (!project) { + return res.status(404).send('Project not found'); + } + + if (project.userId !== req.userId) { + return res.status(403).json({ error: 'Forbidden' }); + } + + next(); + } + + /** + * Verify user is manager of the project by projectId + * (have to be called after verifySignedIn() middleware) + */ + async function verifyProjectManagerFromProjectId(req, res, next) { + const Project = defineProject(sequelize, DataTypes); + + const projectId = req.params.projectId || req.query.projectId; + if (!projectId) { + return res.status(400).json({ error: 'projectId is required' }); + } + + const project = await Project.findByPk(projectId); + if (!project) { + return res.status(404).send('Project not found'); + } + + if (project.userId === req.userId) { + next(); + return; + } + + // check the user is manager of the project + const Member = defineMember(sequelize, DataTypes); + const member = await Member.findOne({ + where: { + userId: req.userId, + projectId: projectId, + }, + }); + if (member) { + const managerRoleIndex = memberRoles.findIndex((entry) => entry.uid === 'manager'); + if (member.role === managerRoleIndex) { + next(); + return; + } + } + + return res.status(403).json({ error: 'Forbidden' }); + } + + /** + * Verify user is developer of the project by projectId + * (have to be called after verifySignedIn() middleware) + */ + async function verifyProjectDeveloperFromProjectId(req, res, next) { + const projectId = req.params.projectId || req.query.projectId; + if (!projectId) { + return res.status(400).json({ error: 'projectId is required' }); + } + + const isDeveloper = await isDeveloper(projectId, req.userId); + if (isDeveloper) { + next(); + return; + } + + return res.status(403).json({ error: 'Forbidden' }); + } + + /** + * Verify user is developer of the project by folderId + * (have to be called after verifySignedIn() middleware) + */ + async function verifyProjectDeveloperFromFolderId(req, res, next) { + const Folder = defineFolder(sequelize, DataTypes); + + const folderId = req.params.folderId || req.query.folderId; + if (!folderId) { + return res.status(400).json({ error: 'folderId is required' }); + } + + // find project id from folderId + const folder = await Folder.findByPk(folderId); + const projectId = folder && folder.id; + if (!projectId) { + return res.status(404).send('failed to find projectId'); + } + + const isDeveloper = await isDeveloper(projectId, req.userId); + if (isDeveloper) { + next(); + return; + } + + return res.status(403).json({ error: 'Forbidden' }); + } + + /** + * Verify user is developer of the project by caseId + * (have to be called after verifySignedIn() middleware) + */ + async function verifyProjectDeveloperFromCaseId(req, res, next) { + const Project = defineProject(sequelize, DataTypes); + const Folder = defineFolder(sequelize, DataTypes); + const Case = defineCase(sequelize, DataTypes); + Project.hasMany(Folder, { foreignKey: 'projectId' }); + Folder.hasMany(Case, { foreignKey: 'folderId' }); + Folder.belongsTo(Project, { foreignKey: 'projectId' }); + Case.belongsTo(Folder, { foreignKey: 'folderId' }); + + const caseId = req.params.caseId || req.query.caseId; + if (!caseId) { + return res.status(400).json({ error: 'caseId is required' }); + } + + // find project id from caseId + const testCase = await Case.findByPk(caseId, { + include: { + model: Folder, + include: Project, + }, + }); + + const projectId = testCase && testCase.Folder && testCase.Folder.Project && testCase.Folder.Project.id; + if (!projectId) { + return res.status(404).send('failed to find projectId'); + } + + const isDeveloper = await isDeveloper(projectId, req.userId); + if (isDeveloper) { + next(); + return; + } + + return res.status(403).json({ error: 'Forbidden' }); + } + + async function isDeveloper(projectId, userId) { + const Project = defineProject(sequelize, DataTypes); + const Member = defineMember(sequelize, DataTypes); + + const project = await Project.findOne({ + where: { id: projectId }, + include: [ + { + model: Member, + where: { userId: userId }, + required: false, + }, + ], + }); + if (!project) { + return false; + } + + // owner has developer or higher authority + if (project.userId === req.userId) { + return true; + } + + const member = project.Members && project.Members[0]; + if (member) { + const managerRoleIndex = memberRoles.findIndex((entry) => entry.uid === 'manager'); + const developerRoleIndex = memberRoles.findIndex((entry) => entry.uid === 'developer'); + if (member.role === managerRoleIndex || member.role === developerRoleIndex) { + return true; + } + } + + return false; + } + + return { + verifyProjectOwner, + verifyProjectManagerFromProjectId, + verifyProjectDeveloperFromProjectId, + verifyProjectDeveloperFromFolderId, + verifyProjectDeveloperFromCaseId, + }; +} + +module.exports = verifyEditableMiddleware; diff --git a/backend/middleware/verifyVisible.js b/backend/middleware/verifyVisible.js index f1324aa..f138053 100644 --- a/backend/middleware/verifyVisible.js +++ b/backend/middleware/verifyVisible.js @@ -103,7 +103,7 @@ function verifyVisibleMiddleware(sequelize) { ], }); if (!project) { - return res.status(404).send('Project not found'); + return false; } // if project is public, everyone can see diff --git a/backend/routes/cases/delete.js b/backend/routes/cases/delete.js index 0a20de6..8f6a4f5 100644 --- a/backend/routes/cases/delete.js +++ b/backend/routes/cases/delete.js @@ -4,10 +4,11 @@ const defineCase = require('../../models/cases'); const { DataTypes } = require('sequelize'); module.exports = function (sequelize) { - const { verifySignedIn, verifyProjectDeveloper } = require('../../middleware/auth')(sequelize); + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectDeveloperFromProjectId } = require('../../middleware/verifyEditable')(sequelize); const Case = defineCase(sequelize, DataTypes); - router.post('/bulkdelete', verifySignedIn, verifyProjectDeveloper, async (req, res) => { + router.post('/bulkdelete', verifySignedIn, verifyProjectDeveloperFromProjectId, async (req, res) => { const { caseIds } = req.body; if (!caseIds || !Array.isArray(caseIds)) { return res.status(400).send('Invalid caseIds array'); diff --git a/backend/routes/cases/edit.js b/backend/routes/cases/edit.js index 520cc74..6c6847c 100644 --- a/backend/routes/cases/edit.js +++ b/backend/routes/cases/edit.js @@ -5,11 +5,12 @@ const defineStep = require('../../models/steps'); const { DataTypes } = require('sequelize'); module.exports = function (sequelize) { - const { verifySignedIn, verifyProjectDeveloper } = require('../../middleware/auth')(sequelize); + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectDeveloperFromCaseId } = require('../../middleware/verifyEditable')(sequelize); const Case = defineCase(sequelize, DataTypes); const Step = defineStep(sequelize, DataTypes); - router.put('/:caseId', verifySignedIn, verifyProjectDeveloper, async (req, res) => { + router.put('/:caseId', verifySignedIn, verifyProjectDeveloperFromCaseId, async (req, res) => { const caseId = req.params.caseId; const updateCase = req.body; try { diff --git a/backend/routes/cases/new.js b/backend/routes/cases/new.js index d9239c6..936b0c4 100644 --- a/backend/routes/cases/new.js +++ b/backend/routes/cases/new.js @@ -14,10 +14,11 @@ function isEmpty(value) { } module.exports = function (sequelize) { - const { verifySignedIn, verifyProjectDeveloper } = require('../../middleware/auth')(sequelize); + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectDeveloperFromFolderId } = require('../../middleware/verifyEditable')(sequelize); const Case = defineCase(sequelize, DataTypes); - router.post('/', verifySignedIn, verifyProjectDeveloper, async (req, res) => { + router.post('/', verifySignedIn, verifyProjectDeveloperFromFolderId, async (req, res) => { const folderId = req.query.folderId; try { diff --git a/backend/routes/folders/delete.js b/backend/routes/folders/delete.js index bce0b46..0085313 100644 --- a/backend/routes/folders/delete.js +++ b/backend/routes/folders/delete.js @@ -4,10 +4,11 @@ const defineFolder = require('../../models/folders'); const { DataTypes } = require('sequelize'); module.exports = function (sequelize) { - const { verifySignedIn, verifyProjectDeveloper } = require('../../middleware/auth')(sequelize); + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectDeveloperFromFolderId } = require('../../middleware/verifyEditable')(sequelize); const Folder = defineFolder(sequelize, DataTypes); - router.delete('/:folderId', verifySignedIn, verifyProjectDeveloper, async (req, res) => { + router.delete('/:folderId', verifySignedIn, verifyProjectDeveloperFromFolderId, async (req, res) => { const folderId = req.params.folderId; try { const folder = await Folder.findByPk(folderId); diff --git a/backend/routes/folders/edit.js b/backend/routes/folders/edit.js index 6de9ff8..8c2be86 100644 --- a/backend/routes/folders/edit.js +++ b/backend/routes/folders/edit.js @@ -4,10 +4,11 @@ const defineFolder = require('../../models/folders'); const { DataTypes } = require('sequelize'); module.exports = function (sequelize) { - const { verifySignedIn, verifyProjectDeveloper } = require('../../middleware/auth')(sequelize); + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectDeveloperFromFolderId } = require('../../middleware/verifyEditable')(sequelize); const Folder = defineFolder(sequelize, DataTypes); - router.put('/:folderId', verifySignedIn, verifyProjectDeveloper, async (req, res) => { + router.put('/:folderId', verifySignedIn, verifyProjectDeveloperFromFolderId, async (req, res) => { const folderId = req.params.folderId; const { name, detail, projectId, parentFolderId } = req.body; try { diff --git a/backend/routes/folders/new.js b/backend/routes/folders/new.js index 0fd6e32..e068000 100644 --- a/backend/routes/folders/new.js +++ b/backend/routes/folders/new.js @@ -4,10 +4,11 @@ const defineFolder = require('../../models/folders'); const { DataTypes } = require('sequelize'); module.exports = function (sequelize) { - const { verifySignedIn, verifyProjectDeveloper } = require('../../middleware/auth')(sequelize); + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectDeveloperFromProjectId } = require('../../middleware/verifyEditable')(sequelize); const Folder = defineFolder(sequelize, DataTypes); - router.post('/', verifySignedIn, verifyProjectDeveloper, async (req, res) => { + router.post('/', verifySignedIn, verifyProjectDeveloperFromProjectId, async (req, res) => { try { const projectId = req.query.projectId; const { name, detail, parentFolderId } = req.body; diff --git a/backend/routes/members/delete.js b/backend/routes/members/delete.js index 854e2cf..ec0208f 100644 --- a/backend/routes/members/delete.js +++ b/backend/routes/members/delete.js @@ -4,10 +4,11 @@ const defineMember = require('../../models/members'); const { DataTypes } = require('sequelize'); module.exports = function (sequelize) { - const { verifySignedIn, verifyProjectManager } = require('../../middleware/auth')(sequelize); + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectManagerFromProjectId } = require('../../middleware/verifyEditable')(sequelize); const Member = defineMember(sequelize, DataTypes); - router.delete('/', verifySignedIn, verifyProjectManager, async (req, res) => { + router.delete('/', verifySignedIn, verifyProjectManagerFromProjectId, async (req, res) => { const userId = req.query.userId; const projectId = req.query.projectId; diff --git a/backend/routes/members/edit.js b/backend/routes/members/edit.js index 23a4c13..c975270 100644 --- a/backend/routes/members/edit.js +++ b/backend/routes/members/edit.js @@ -4,10 +4,11 @@ const defineMember = require('../../models/members'); const { DataTypes } = require('sequelize'); module.exports = function (sequelize) { - const { verifySignedIn, verifyProjectManager } = require('../../middleware/auth')(sequelize); + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectManagerFromProjectId } = require('../../middleware/verifyEditable')(sequelize); const Member = defineMember(sequelize, DataTypes); - router.put('/', verifySignedIn, verifyProjectManager, async (req, res) => { + router.put('/', verifySignedIn, verifyProjectManagerFromProjectId, async (req, res) => { const userId = req.query.userId; const projectId = req.query.projectId; const role = req.query.role; diff --git a/backend/routes/members/new.js b/backend/routes/members/new.js index b517d51..ade8aab 100644 --- a/backend/routes/members/new.js +++ b/backend/routes/members/new.js @@ -5,10 +5,11 @@ const { DataTypes } = require('sequelize'); const { memberRoles } = require('../../routes/users/authSettings'); module.exports = function (sequelize) { - const { verifySignedIn, verifyProjectManager } = require('../../middleware/auth')(sequelize); + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectManagerFromProjectId } = require('../../middleware/verifyEditable')(sequelize); const Member = defineMember(sequelize, DataTypes); - router.post('/', verifySignedIn, verifyProjectManager, async (req, res) => { + router.post('/', verifySignedIn, verifyProjectManagerFromProjectId, async (req, res) => { const userId = req.query.userId; const projectId = req.query.projectId; diff --git a/backend/routes/projects/delete.js b/backend/routes/projects/delete.js index 14df553..47dadc6 100644 --- a/backend/routes/projects/delete.js +++ b/backend/routes/projects/delete.js @@ -6,7 +6,8 @@ const defineRun = require('../../models/runs'); const { DataTypes } = require('sequelize'); module.exports = function (sequelize) { - const { verifySignedIn, verifyProjectOwner } = require('../../middleware/auth')(sequelize); + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectOwner } = require('../../middleware/verifyEditable')(sequelize); const Project = defineProject(sequelize, DataTypes); const Folder = defineFolder(sequelize, DataTypes); const Run = defineRun(sequelize, DataTypes); diff --git a/backend/routes/projects/edit.js b/backend/routes/projects/edit.js index 561365d..a90c65b 100644 --- a/backend/routes/projects/edit.js +++ b/backend/routes/projects/edit.js @@ -4,7 +4,8 @@ const defineProject = require('../../models/projects'); const { DataTypes } = require('sequelize'); module.exports = function (sequelize) { - const { verifySignedIn, verifyProjectOwner } = require('../../middleware/auth')(sequelize); + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectOwner } = require('../../middleware/verifyEditable')(sequelize); const Project = defineProject(sequelize, DataTypes); router.put('/:projectId', verifySignedIn, verifyProjectOwner, async (req, res) => {