Skip to content

Commit

Permalink
feat(permission): list permission members
Browse files Browse the repository at this point in the history
  • Loading branch information
serge1peshcoff committed Mar 31, 2020
1 parent a0bafaf commit 4da4d20
Show file tree
Hide file tree
Showing 4 changed files with 258 additions and 1 deletion.
36 changes: 36 additions & 0 deletions lib/permissions-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
41 changes: 40 additions & 1 deletion middlewares/permissions.js
Original file line number Diff line number Diff line change
@@ -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({
Expand Down Expand Up @@ -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 }
});
};
181 changes: 181 additions & 0 deletions test/api/permissions-members.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});

0 comments on commit 4da4d20

Please sign in to comment.