From 4da4d20d9c9fb64156de41d8ef95b7dfc0c1e253 Mon Sep 17 00:00:00 2001 From: Sergey Peshkov Date: Mon, 30 Mar 2020 02:02:56 +0300 Subject: [PATCH] feat(permission): list permission members --- lib/permissions-manager.js | 36 ++++++ lib/server.js | 1 + middlewares/permissions.js | 41 +++++- test/api/permissions-members.test.js | 181 +++++++++++++++++++++++++++ 4 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 test/api/permissions-members.test.js diff --git a/lib/permissions-manager.js b/lib/permissions-manager.js index 2a62d120..9fee9d9f 100644 --- a/lib/permissions-manager.js +++ b/lib/permissions-manager.js @@ -66,6 +66,42 @@ class PermissionsManager { } } + getIndirectChildCircles(circleId) { + const directChildCirclesIds = this.circles + .filter((circle) => circle.parent_circle_id === circleId) + .map((circle) => circle.id); + + if (!directChildCirclesIds.length) { + return []; + } + + for (const id of directChildCirclesIds) { + directChildCirclesIds.push(...this.getIndirectChildCircles(id)); + } + + return directChildCirclesIds; + } + + async fetchPermissionCircles(permission) { + // 1) get all CirclePermission for this permission + const circlePermissions = await CirclePermission.findAll({ + where: { permission_id: permission.id } + }); + + // 2) for all the circlePermissions, get their circle + // and their child circles recursively and push it into map + const circlesIds = circlePermissions + .map((circlePermission) => this.getIndirectChildCircles(circlePermission.circle_id)) + .concat(circlePermissions.map((circlePermission) => circlePermission.circle_id)) + .flat(); + + const circles = await Circle.findAll({ + where: { id: { [Sequelize.Op.in]: circlesIds } } + }); + + return circles; + } + async fetchCurrentUserPermissions() { // for superadmin, just assign all the permissions. if (this.user.superadmin) { diff --git a/lib/server.js b/lib/server.js index 562d9568..6fc35a00 100644 --- a/lib/server.js +++ b/lib/server.js @@ -120,6 +120,7 @@ CirclesRouter.delete('/', circles.deleteCircle); // Everything related to a specific permission. Auth only. PermissionsRouter.use(middlewares.maybeAuthorize, middlewares.ensureAuthorized, fetch.fetchPermission); PermissionsRouter.get('/', permissions.getPermission); +PermissionsRouter.get('/members', permissions.getPermissionMembers); PermissionsRouter.put('/', permissions.updatePermission); PermissionsRouter.delete('/', permissions.deletePermission); diff --git a/middlewares/permissions.js b/middlewares/permissions.js index 7ef8f879..f9d8d807 100644 --- a/middlewares/permissions.js +++ b/middlewares/permissions.js @@ -1,7 +1,8 @@ -const { Permission } = require('../models'); +const { Permission, CircleMembership, Circle, User } = require('../models'); const helpers = require('../lib/helpers'); const errors = require('../lib/errors'); const constants = require('../lib/constants'); +const { Sequelize } = require('../lib/sequelize'); exports.listAllPermissions = async (req, res) => { const result = await Permission.findAndCountAll({ @@ -59,3 +60,41 @@ exports.deletePermission = async (req, res) => { message: 'Permission is deleted.' }); }; + +exports.getPermissionMembers = async (req, res) => { + if (!req.permissions.hasPermission('global:view:member')) { + return errors.makeForbiddenError(res, 'Permission global:view:member is required, but not present.'); + } + + const permissionCircles = await req.permissions.fetchPermissionCircles(req.currentPermission); + + // oh boy. + // 2 cases: when a person is member of one of the circles + // that has this permission directly or indirectly, + // and superadmins. + const result = await User.findAndCountAll({ + where: { + [Sequelize.Op.or]: [ + { + '$circle_memberships.circle_id$': { + [Sequelize.Op.in]: permissionCircles.map((circle) => circle.id) + } + }, + { superadmin: true } + ], + ...helpers.filterBy(req.query.query, constants.FIELDS_TO_QUERY.MEMBER) + }, + ...helpers.getPagination(req.query), + order: helpers.getSorting(req.query), + include: [ + CircleMembership, + { model: Circle, as: 'circles' } + ] + }); + + return res.json({ + success: true, + data: result.rows, + meta: { count: result.count } + }); +}; diff --git a/test/api/permissions-members.test.js b/test/api/permissions-members.test.js new file mode 100644 index 00000000..7c5d0f7b --- /dev/null +++ b/test/api/permissions-members.test.js @@ -0,0 +1,181 @@ +const { startServer, stopServer } = require('../../lib/server.js'); +const { request } = require('../scripts/helpers'); +const generator = require('../scripts/generator'); + +describe('Permission members', () => { + beforeAll(async () => { + await startServer(); + }); + + afterAll(async () => { + await stopServer(); + }); + + afterEach(async () => { + await generator.clearAll(); + }); + + test('should return 404 if the permission is not found', async () => { + const user = await generator.createUser(); + const token = await generator.createAccessToken({}, user); + + const res = await request({ + uri: '/permissions/1337/members', + method: 'GET', + headers: { 'X-Auth-Token': token.value } + }); + + expect(res.statusCode).toEqual(404); + expect(res.body.success).toEqual(false); + expect(res.body).not.toHaveProperty('data'); + expect(res.body).toHaveProperty('message'); + }); + + test('should return 400 if id is not a number', async () => { + const user = await generator.createUser(); + const token = await generator.createAccessToken({}, user); + + const res = await request({ + uri: '/permissions/xxx/members', + method: 'GET', + headers: { 'X-Auth-Token': token.value } + }); + + expect(res.statusCode).toEqual(400); + expect(res.body.success).toEqual(false); + expect(res.body).toHaveProperty('message'); + expect(res.body).not.toHaveProperty('data'); + }); + + test('should fail if no permission', async () => { + const user = await generator.createUser(); + const token = await generator.createAccessToken({}, user); + + const permission = await generator.createPermission(); + + const res = await request({ + uri: '/permissions/' + permission.id + '/members', + method: 'GET', + headers: { 'X-Auth-Token': token.value } + }); + + expect(res.statusCode).toEqual(403); + expect(res.body.success).toEqual(false); + expect(res.body).toHaveProperty('message'); + expect(res.body).not.toHaveProperty('data'); + }); + + test('should list superadmins', async () => { + const user = await generator.createUser({ superadmin: true }); + const token = await generator.createAccessToken({}, user); + + await generator.createPermission({ scope: 'global', action: 'view', object: 'member' }); + + const permission = await generator.createPermission(); + + const res = await request({ + uri: '/permissions/' + permission.id + '/members', + method: 'GET', + headers: { 'X-Auth-Token': token.value } + }); + + expect(res.statusCode).toEqual(200); + expect(res.body.success).toEqual(true); + expect(res.body).toHaveProperty('data'); + expect(res.body).not.toHaveProperty('errors'); + expect(res.body.data.length).toEqual(1); + expect(res.body.data[0].id).toEqual(user.id); + }); + + test('should list people who have this permission through free circle', async () => { + const user = await generator.createUser(); + const token = await generator.createAccessToken({}, user); + + const permissionCircle = await generator.createCircle(); + const viewPermission = await generator.createPermission({ scope: 'global', action: 'view', object: 'member' }); + await generator.createCircleMembership(permissionCircle, user); + await generator.createCirclePermission(permissionCircle, viewPermission); + + const otherUser = await generator.createUser(); + const permission = await generator.createPermission(); + const circle = await generator.createCircle(); + await generator.createCircleMembership(circle, otherUser); + await generator.createCirclePermission(circle, permission); + + const res = await request({ + uri: '/permissions/' + permission.id + '/members', + method: 'GET', + headers: { 'X-Auth-Token': token.value } + }); + + expect(res.statusCode).toEqual(200); + expect(res.body.success).toEqual(true); + expect(res.body).toHaveProperty('data'); + expect(res.body).not.toHaveProperty('errors'); + expect(res.body.data.length).toEqual(1); + expect(res.body.data[0].id).toEqual(otherUser.id); + }); + + test('should list people who have this permission through bound circle', async () => { + const user = await generator.createUser(); + const token = await generator.createAccessToken({}, user); + + const permissionCircle = await generator.createCircle(); + const viewPermission = await generator.createPermission({ scope: 'global', action: 'view', object: 'member' }); + await generator.createCircleMembership(permissionCircle, user); + await generator.createCirclePermission(permissionCircle, viewPermission); + + const otherUser = await generator.createUser(); + const permission = await generator.createPermission(); + const body = await generator.createBody(); + const circle = await generator.createCircle({ body_id: body.id }); + await generator.createCircleMembership(circle, otherUser); + await generator.createCirclePermission(circle, permission); + + const res = await request({ + uri: '/permissions/' + permission.id + '/members', + method: 'GET', + headers: { 'X-Auth-Token': token.value } + }); + + expect(res.statusCode).toEqual(200); + expect(res.body.success).toEqual(true); + expect(res.body).toHaveProperty('data'); + expect(res.body).not.toHaveProperty('errors'); + expect(res.body.data.length).toEqual(1); + expect(res.body.data[0].id).toEqual(otherUser.id); + }); + + test('should list people who have this permission through indirect circle', async () => { + const user = await generator.createUser(); + const token = await generator.createAccessToken({}, user); + + const permissionCircle = await generator.createCircle(); + const viewPermission = await generator.createPermission({ scope: 'global', action: 'view', object: 'member' }); + await generator.createCircleMembership(permissionCircle, user); + await generator.createCirclePermission(permissionCircle, viewPermission); + + const otherUser = await generator.createUser(); + const permission = await generator.createPermission(); + const body = await generator.createBody(); + + const firstCircle = await generator.createCircle(); + const secondCircle = await generator.createCircle({ parent_circle_id: firstCircle.id }); + const thirdCircle = await generator.createCircle({ body_id: body.id, parent_circle_id: secondCircle.id }); + await generator.createCircleMembership(thirdCircle, otherUser); + await generator.createCirclePermission(firstCircle, permission); + + const res = await request({ + uri: '/permissions/' + permission.id + '/members', + method: 'GET', + headers: { 'X-Auth-Token': token.value } + }); + + expect(res.statusCode).toEqual(200); + expect(res.body.success).toEqual(true); + expect(res.body).toHaveProperty('data'); + expect(res.body).not.toHaveProperty('errors'); + expect(res.body.data.length).toEqual(1); + expect(res.body.data[0].id).toEqual(otherUser.id); + }); +});