diff --git a/README.md b/README.md index 0d8bc10..c69cf42 100644 --- a/README.md +++ b/README.md @@ -94,5 +94,6 @@ The GroupContains relation contains these fields: - id: the relationship UUID - type: the relationship type, 'group' or 'user' +- roles: the roles of the user in the group - createdAt: the created at date string - createdBy: the created by user id \ No newline at end of file diff --git a/app-constants.js b/app-constants.js index b5a5db0..c770831 100644 --- a/app-constants.js +++ b/app-constants.js @@ -20,10 +20,13 @@ const GroupStatus = { InActive: 'inactive' } +const GroupRoleName = ['groupManager', 'groupAdmin'] + module.exports = { UserRoles, MembershipTypes, EVENT_ORIGINATOR, EVENT_MIME_TYPE, - GroupStatus + GroupStatus, + GroupRoleName } diff --git a/config/default.js b/config/default.js index 16922f4..6dfb3a6 100644 --- a/config/default.js +++ b/config/default.js @@ -37,6 +37,12 @@ module.exports = { KAFKA_GROUP_DELETE_TOPIC: process.env.KAFKA_GROUP_DELETE_TOPIC || 'groups.notification.delete', KAFKA_GROUP_MEMBER_ADD_TOPIC: process.env.KAFKA_GROUP_MEMBER_ADD_TOPIC || 'groups.notification.member.add', KAFKA_GROUP_MEMBER_DELETE_TOPIC: process.env.KAFKA_GROUP_MEMBER_DELETE_TOPIC || 'groups.notification.member.delete', + KAFKA_GROUP_UNIVERSAL_MEMBER_ADD_TOPIC: process.env.KAFKA_GROUP_UNIVERSAL_MEMBER_ADD_TOPIC || 'groups.notification.universalmember.add', + KAFKA_GROUP_UNIVERSAL_MEMBER_DELETE_TOPIC: process.env.KAFKA_GROUP_UNIVERSAL_MEMBER_DELETE_TOPIC || 'groups.notification.universalmember.delete', + KAFKA_GROUP_MEMBER_ROLE_ADD_TOPIC: process.env.KAFKA_GROUP_MEMBER_ROLE_ADD_TOPIC || 'groups.notification.create', + KAFKA_GROUP_MEMBER_ROLE_DELETE_TOPIC: process.env.KAFKA_GROUP_MEMBER_ROLE_DELETE_TOPIC || 'groups.notification.create', + KAFKA_SUBGROUP_CREATE_TOPIC: process.env.KAFKA_SUBGROUP_CREATE_TOPIC || 'groups.notification.create', + KAFKA_SUBGROUP_DELETE_TOPIC: process.env.KAFKA_SUBGROUP_DELETE_TOPIC || 'groups.notification.create', USER_ROLES: { Admin: 'Administrator', diff --git a/docs/swagger.yml b/docs/swagger.yml index f1eaad5..764f9f7 100644 --- a/docs/swagger.yml +++ b/docs/swagger.yml @@ -44,7 +44,7 @@ paths: description: | Add a member to the specified group - If the group is private, the user needs to be a member of the group, or an admin. + If the group is private, the user needs to be a member of the group, or an admin or the user making the request is having the role of groupManager or groupAdmin. tags: - group membership security: @@ -92,7 +92,7 @@ paths: description: | Remove a member from specified group - The user has to have admin role and the group allows self registration. + The user has to have admin role or the role of groupManager or groupAdmin and the group allows self registration. tags: - group membership security: @@ -365,6 +365,131 @@ paths: $ref: "#/components/responses/NotFound" 500: $ref: "#/components/responses/InternalServerError" + + /groups/{groupId}/subGroup: + post: + description: | + Creation of a new sub group + + The user has to have admin role or have a "groupAdmin" role for that group. + tags: + - sub groups + security: + - bearer: [] + parameters: + - $ref: '#/components/parameters/groupId' + requestBody: + $ref: '#/components/requestBodies/NewGroupBodyParam' + responses: + 200: + $ref: "#/components/responses/GroupResponse" + 400: + $ref: "#/components/responses/BadRequest" + 401: + $ref: "#/components/responses/Unauthenticated" + 403: + $ref: "#/components/responses/Forbidden" + 500: + $ref: "#/components/responses/InternalServerError" + + /groups/{groupId}/subGroup/{subGroupId}: + delete: + description: | + Creation of a new sub group + + The user has to have admin role or have a "groupAdmin" role for that group. + tags: + - sub groups + security: + - bearer: [] + parameters: + - $ref: '#/components/parameters/groupId' + - $ref: '#/components/parameters/subGroupId' + responses: + 204: + description: The resource was deleted successfully. + 400: + $ref: "#/components/responses/BadRequest" + 401: + $ref: "#/components/responses/Unauthenticated" + 403: + $ref: "#/components/responses/Forbidden" + 500: + $ref: "#/components/responses/InternalServerError" + + /groupRoles/users/{memberId}: + get: + description: | + Returns the groups and roles of the user identified by memberId. + + The user has to have admin role. + tags: + - group roles + security: + - bearer: [] + parameters: + - $ref: '#/components/parameters/memberId' + - $ref: '#/components/parameters/page' + - $ref: '#/components/parameters/perPage' + responses: + 200: + $ref: '#/components/responses/GroupMemberRoleResponse' + 400: + $ref: "#/components/responses/BadRequest" + 401: + $ref: "#/components/responses/Unauthenticated" + 403: + $ref: "#/components/responses/Forbidden" + 500: + $ref: "#/components/responses/InternalServerError" + post: + description: | + Creation of new group role for a user + + The user has to have admin role. + tags: + - group roles + security: + - bearer: [] + parameters: + - $ref: '#/components/parameters/memberId' + requestBody: + $ref: '#/components/requestBodies/GroupRoleBodyParam' + responses: + 201: + description: CREATED + 400: + $ref: "#/components/responses/BadRequest" + 401: + $ref: "#/components/responses/Unauthenticated" + 403: + $ref: "#/components/responses/Forbidden" + 500: + $ref: "#/components/responses/InternalServerError" + delete: + description: | + Delete a group role + + The user has to have admin role. + tags: + - group roles + security: + - bearer: [] + parameters: + - $ref: '#/components/parameters/memberId' + requestBody: + $ref: '#/components/requestBodies/GroupRoleBodyParam' + responses: + 204: + description: The resource was deleted successfully. + 400: + $ref: "#/components/responses/BadRequest" + 401: + $ref: "#/components/responses/Unauthenticated" + 403: + $ref: "#/components/responses/Forbidden" + 500: + $ref: "#/components/responses/InternalServerError" /groups/health: get: @@ -400,6 +525,14 @@ components: schema: type: string example: '10ba038e-48da-123b-96e8-8d3b99b6d18a' + subGroupId: + name: subGroupId + in: path + description: The sub group id. + required: true + schema: + type: string + example: '10ba038e-48da-123b-96e8-8d3b99b6d18a' oldId: name: oldId in: path @@ -617,10 +750,15 @@ components: content: application/json: schema: - type: object - properties: - result: - $ref: '#/components/schemas/Group' + $ref: '#/components/schemas/Group' + GroupMemberRoleResponse: + description: The group role response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/GroupRole' schemas: GroupMembership: @@ -695,6 +833,24 @@ components: type: array items: $ref: '#/components/schemas/Group' + + GroupRole: + description: The group role entity + properties: + groupId: + type: string + description: The group id + role: + type: string + enum: ['groupManager', 'groupAdmin'] + description: The group role + createdAt: + type: string + format: date-time + description: The time the group role created at + createdBy: + type: string + description: The id of the user who created the group role requestBodies: NewGroupMembershipBodyParam: @@ -738,4 +894,20 @@ components: status: type: string enum: ['active', 'inactive'] - description: Value indicating the status of the group \ No newline at end of file + description: Value indicating the status of the group + GroupRoleBodyParam: + description: A JSON object containing group role body information + required: true + content: + application/json: + schema: + type: object + properties: + groupId: + type: string + description: The group id + example: '10ba038e-48da-123b-96e8-8d3b99b6d18a' + role: + type: string + enum: ['groupManager', 'groupAdmin'] + description: The group role \ No newline at end of file diff --git a/src/common/helper.js b/src/common/helper.js index 8718674..263f278 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -7,9 +7,11 @@ const busApi = require('tc-bus-api-wrapper') const config = require('config') const neo4j = require('neo4j-driver') const querystring = require('querystring') +const uuid = require('uuid/v4') let validate = require('uuid-validate') const errors = require('./errors') +const logger = require('./logger') const constants = require('../../app-constants') // Bus API Client @@ -74,13 +76,9 @@ async function ensureExists(tx, model, id, isAdmin = false) { if (model === 'Group') { if (validate(id, 4)) { if (!isAdmin) { - res = await tx.run(`MATCH (e:${model} {id: {id}, status: '${constants.GroupStatus.Active}'}) RETURN e`, { - id - }) + res = await tx.run(`MATCH (e:${model} {id: {id}, status: '${constants.GroupStatus.Active}'}) RETURN e`, {id}) } else { - res = await tx.run(`MATCH (e:${model} {id: {id}}) RETURN e`, { - id - }) + res = await tx.run(`MATCH (e:${model} {id: {id}}) RETURN e`, {id}) } } else { if (!isAdmin) { @@ -88,9 +86,7 @@ async function ensureExists(tx, model, id, isAdmin = false) { id }) } else { - res = await tx.run(`MATCH (e:${model} {oldId: {id}}) RETURN e`, { - id - }) + res = await tx.run(`MATCH (e:${model} {oldId: {id}}) RETURN e`, {id}) } } @@ -98,12 +94,18 @@ async function ensureExists(tx, model, id, isAdmin = false) { throw new errors.NotFoundError(`Not found ${model} of id ${id}`) } } else if (model === 'User') { - res = await tx.run(`MATCH (e:${model} {id: {id}}) RETURN e`, { - id - }) + if (validate(id, 4)) { + res = await tx.run(`MATCH (e:${model} {universalUID: {id}}) RETURN e`, {id}) - if (res && res.records.length === 0) { - res = await tx.run(`CREATE (user:User {id: {id}}) RETURN user`, { id }) + if (res && res.records.length === 0) { + res = await tx.run(`CREATE (user:User {id: '00000000', universalUID: {id}}) RETURN user`, {id}) + } + } else { + res = await tx.run(`MATCH (e:${model} {id: {id}}) RETURN e`, {id}) + + if (res && res.records.length === 0) { + res = await tx.run(`CREATE (user:User {id: {id}, universalUID: '00000000'}) RETURN user`, {id}) + } } } @@ -123,7 +125,7 @@ async function ensureGroupMember(session, groupId, userId) { try { const memberCheckRes = await session.run( 'MATCH (g:Group {id: {groupId}})-[r:GroupContains {type: {membershipType}}]->(u:User {id: {userId}}) RETURN r', - { groupId, membershipType: config.MEMBERSHIP_TYPES.User, userId } + {groupId, membershipType: config.MEMBERSHIP_TYPES.User, userId} ) if (memberCheckRes.records.length === 0) { throw new errors.ForbiddenError(`User is not member of the group`) @@ -133,6 +135,26 @@ async function ensureGroupMember(session, groupId, userId) { } } +/** + * Return whether the user has one of the group roles or not. + * @param {Object} session the db session + * @param {String} groupId the group id + * @param {String} userId the user id + * @param {Array} roles an array of group roles + * @returns {Boolean} true if user has one of the group roles + */ +async function hasGroupRole (session, groupId, userId, roles) { + const memberCheckRes = await session.run( + 'MATCH (g:Group {id: {groupId}})-[r:GroupContains {type: {membershipType}}]->(u:User {id: {userId}}) RETURN r', + { groupId, membershipType: config.MEMBERSHIP_TYPES.User, userId } + ) + if (memberCheckRes.records.length === 0) { + return false + } + const existRoles = memberCheckRes.records[0]._fields[0].properties.roles || [] + return _.some(existRoles, r => _.some(roles, role => JSON.parse(r).role === role)) +} + /** * Get child groups. * @param {Object} session the db session @@ -143,7 +165,7 @@ async function getChildGroups(session, groupId) { try { const res = await session.run( 'MATCH (g:Group {id: {groupId}})-[r:GroupContains]->(c:Group) RETURN c ORDER BY c.oldId', - { groupId } + {groupId} ) return _.map(res.records, (record) => record.get(0).properties) } catch (error) { @@ -161,7 +183,7 @@ async function getParentGroups(session, groupId) { try { const res = await session.run( 'MATCH (g:Group)-[r:GroupContains]->(c:Group {id: {groupId}}) RETURN g ORDER BY g.oldId', - { groupId } + {groupId} ) return _.map(res.records, (record) => record.get(0).properties) } catch (error) { @@ -176,7 +198,7 @@ async function getParentGroups(session, groupId) { * @returns {String} link for the page */ function getPageLink(req, page) { - const q = _.assignIn({}, req.query, { page }) + const q = _.assignIn({}, req.query, {page}) return `${req.protocol}://${req.get('Host')}${req.baseUrl}${req.path}?${querystring.stringify(q)}` } @@ -188,6 +210,10 @@ function getPageLink(req, page) { */ function setResHeaders(req, res, result) { const totalPages = Math.ceil(result.total / result.perPage) + if (result.page > 1) { + res.set('X-Prev-Page', result.page - 1) + } + if (result.page < totalPages) { res.set('X-Next-Page', result.page + 1) } @@ -206,6 +232,14 @@ function setResHeaders(req, res, result) { } res.set('Link', link) } + + // Allow browsers access pagination data in headers + let accessControlExposeHeaders = res.get('Access-Control-Expose-Headers') || '' + accessControlExposeHeaders += accessControlExposeHeaders ? ', ' : '' + // append new values, to not override values set by someone else + accessControlExposeHeaders += 'X-Page, X-Per-Page, X-Total, X-Total-Pages, X-Prev-Page, X-Next-Page' + + res.set('Access-Control-Expose-Headers', accessControlExposeHeaders) } /** @@ -293,6 +327,73 @@ async function postBusEvent(topic, payload) { }) } +async function createGroup (tx, data, currentUser) { + // check whether group name is already used + const nameCheckRes = await tx.run('MATCH (g:Group {name: {name}}) RETURN g LIMIT 1', { + name: data.name + }) + if (nameCheckRes.records.length > 0) { + throw new errors.ConflictError(`The group name ${data.name} is already used`) + } + + // create group + const groupData = data + + // generate next group id + groupData.id = uuid() + groupData.createdAt = new Date().toISOString() + groupData.createdBy = currentUser === 'M2M' ? '00000000' : currentUser.userId + groupData.domain = groupData.domain ? groupData.domain : '' + groupData.ssoId = groupData.ssoId ? groupData.ssoId : '' + groupData.organizationId = groupData.organizationId ? groupData.organizationId : '' + + const createRes = await tx.run( + 'CREATE (group:Group {id: {id}, name: {name}, description: {description}, privateGroup: {privateGroup}, selfRegister: {selfRegister}, createdAt: {createdAt}, createdBy: {createdBy}, domain: {domain}, ssoId: {ssoId}, organizationId: {organizationId}, status: {status}}) RETURN group', + groupData + ) + + return createRes.records[0]._fields[0].properties +} + +async function deleteGroup (tx, group) { + const groupsToDelete = [group] + let index = 0 + while (index < groupsToDelete.length) { + const g = groupsToDelete[index] + index += 1 + + const childGroups = await getChildGroups(tx, g.id) + for (let i = 0; i < childGroups.length; i += 1) { + const child = childGroups[i] + if (_.find(groupsToDelete, (gtd) => gtd.id === child.id)) { + // the child was checked, ignore duplicate processing + continue + } + // delete child if it doesn't belong to other group + const parents = await getParentGroups(tx, child.id) + if (parents.length <= 1) { + groupsToDelete.push(child) + } + } + } + + logger.debug(`Groups to delete ${JSON.stringify(groupsToDelete)}`) + + for (let i = 0; i < groupsToDelete.length; i += 1) { + const id = groupsToDelete[i].id + // delete group's relationships + await tx.run('MATCH (g:Group {id: {groupId}})-[r]-() DELETE r', { + groupId: id + }) + + // delete group + await tx.run('MATCH (g:Group {id: {groupId}}) DELETE g', { + groupId: id + }) + } + return groupsToDelete +} + module.exports = { wrapExpress, autoWrapExpress, @@ -304,5 +405,8 @@ module.exports = { setResHeaders, checkIfExists, hasAdminRole, - postBusEvent + hasGroupRole, + postBusEvent, + createGroup, + deleteGroup } diff --git a/src/controllers/GroupController.js b/src/controllers/GroupController.js index 2e236b5..00be029 100644 --- a/src/controllers/GroupController.js +++ b/src/controllers/GroupController.js @@ -17,7 +17,7 @@ async function searchGroups(req, res) { criteria.memberId = req.authUser.userId criteria.membershipType = config.MEMBERSHIP_TYPES.User } - const result = await service.searchGroups(criteria, !req.authUser.isMachine && helper.hasAdminRole(req.authUser)) + const result = await service.searchGroups(criteria, req.authUser.isMachine || helper.hasAdminRole(req.authUser)) helper.setResHeaders(req, res, result) res.send(result) } diff --git a/src/controllers/GroupMembershipController.js b/src/controllers/GroupMembershipController.js index 2c9629f..1d0daa2 100644 --- a/src/controllers/GroupMembershipController.js +++ b/src/controllers/GroupMembershipController.js @@ -56,7 +56,8 @@ async function deleteGroupMember(req, res) { const result = await service.deleteGroupMember( req.authUser.isMachine ? 'M2M' : req.authUser, req.params.groupId, - req.params.memberId + req.params.memberId ? req.params.memberId : null, + Object.keys(req.query).length !== 0 ? req.query : null ) res.send(result) } @@ -71,6 +72,16 @@ async function getGroupMembersCount(req, res) { res.send(result) } +/** + * Get list of mapping of groups and members count + * @param req the request + * @param res the response + */ +async function listGroupsMemberCount(req, res) { + const result = await service.listGroupsMemberCount(req.query) + res.send(result) +} + /** * Get group members * @param req the request @@ -82,11 +93,26 @@ async function getMemberGroups(req, res) { res.send(result) } +/** + * Get group members + * @param req the request + * @param res the response + */ +async function searchMemberGroups(req, res) { + console.log('sssss') + console.log(JSON.stringify(req.query)) + const result = await service.getMemberGroups(req.authUser.isMachine ? 'M2M' : req.authUser, {}, req.query) + helper.setResHeaders(req, res, result) + res.send(result) +} + module.exports = { getGroupMembers, addGroupMember, getGroupMember, deleteGroupMember, getGroupMembersCount, - getMemberGroups + listGroupsMemberCount, + getMemberGroups, + searchMemberGroups } diff --git a/src/controllers/GroupRoleController.js b/src/controllers/GroupRoleController.js new file mode 100644 index 0000000..51d6913 --- /dev/null +++ b/src/controllers/GroupRoleController.js @@ -0,0 +1,49 @@ +/** + * Controller for group role endpoints + */ +const service = require('../services/GroupRoleService') +const helper = require('../common/helper') + +/** + * Get the groups and roles of the user + * @param req the request + * @param res the response + */ +async function getGroupRoles (req, res) { + const criteria = req.query + const result = await service.getGroupRole(req.params.userId, criteria) + helper.setResHeaders(req, res, result) + res.send(result.result) +} + +/** + * Add a new combination of role and group to the group roles for the user + * @param req the request + * @param res the response + */ +async function addGroupRole (req, res) { + await service.addGroupRole(req.authUser.isMachine ? 'M2M' : req.authUser, + req.params.userId, + req.body.groupId, + req.body.role) + res.status(201).end() +} + +/** + * Deletes the role and the groupId combination for the user + * @param req the request + * @param res the response + */ +async function deleteGroupRole (req, res) { + await service.deleteGroupRole(req.params.userId, + req.body.groupId, + req.body.role, + !req.authUser.isMachine && helper.hasAdminRole(req.authUser)) + res.sendStatus(204) +} + +module.exports = { + getGroupRoles, + addGroupRole, + deleteGroupRole +} diff --git a/src/controllers/SubGroupController.js b/src/controllers/SubGroupController.js new file mode 100644 index 0000000..f52d290 --- /dev/null +++ b/src/controllers/SubGroupController.js @@ -0,0 +1,35 @@ +/** + * Controller for sub group endpoints + */ +const service = require('../services/SubGroupService') + +/** + * Create group + * @param req the request + * @param res the response + */ +async function createSubGroup (req, res) { + const result = await service.createSubGroup(req.authUser.isMachine ? 'M2M' : req.authUser, + req.params.groupId, + req.body + ) + res.send(result) +} + +/** + * Delete sub group + * @param req the request + * @param res the response + */ +async function deleteSubGroup (req, res) { + await service.deleteSubGroup(req.authUser.isMachine ? 'M2M' : req.authUser, + req.params.groupId, + req.params.subGroupId + ) + res.sendStatus(204) +} + +module.exports = { + createSubGroup, + deleteSubGroup +} diff --git a/src/routes.js b/src/routes.js index 801f956..5572f3d 100644 --- a/src/routes.js +++ b/src/routes.js @@ -73,6 +73,13 @@ module.exports = { auth: 'jwt', access: [constants.UserRoles.Admin, constants.UserRoles.User], scopes: ['write:groups', 'all:groups'] + }, + delete: { + controller: 'GroupMembershipController', + method: 'deleteGroupMember', + auth: 'jwt', + access: [constants.UserRoles.Admin, constants.UserRoles.User], + scopes: ['write:groups', 'all:groups'] } }, '/groups/:groupId/members/:memberId': { @@ -87,7 +94,7 @@ module.exports = { controller: 'GroupMembershipController', method: 'deleteGroupMember', auth: 'jwt', - access: [constants.UserRoles.Admin], + access: [constants.UserRoles.Admin, constants.UserRoles.User], scopes: ['write:groups', 'all:groups'] } }, @@ -97,6 +104,30 @@ module.exports = { method: 'getGroupMembersCount' } }, + '/groups/:groupId/subGroup': { + post: { + controller: 'SubGroupController', + method: 'createSubGroup', + auth: 'jwt', + access: [constants.UserRoles.Admin, constants.UserRoles.User], + scopes: ['write:groups', 'all:groups'] + } + }, + '/groups/:groupId/subGroup/:subGroupId': { + delete: { + controller: 'SubGroupController', + method: 'deleteSubGroup', + auth: 'jwt', + access: [constants.UserRoles.Admin, constants.UserRoles.User], + scopes: ['write:groups', 'all:groups'] + } + }, + '/groups/memberGroups/groupMembersCount': { + get: { + controller: 'GroupMembershipController', + method: 'listGroupsMemberCount' + } + }, '/groups/memberGroups/:memberId': { get: { controller: 'GroupMembershipController', @@ -106,6 +137,38 @@ module.exports = { scopes: ['read:groups'] } }, + '/groups/memberGroups/': { + get: { + controller: 'GroupMembershipController', + method: 'searchMemberGroups', + auth: 'jwt', + access: [constants.UserRoles.Admin, constants.UserRoles.User], + scopes: ['read:groups'] + } + }, + '/groupRoles/users/:userId': { + get: { + controller: 'GroupRoleController', + method: 'getGroupRoles', + auth: 'jwt', + access: [constants.UserRoles.Admin], + scopes: ['read:groups', 'write:groups', 'all:groups'] + }, + post: { + controller: 'GroupRoleController', + method: 'addGroupRole', + auth: 'jwt', + access: [constants.UserRoles.Admin], + scopes: ['write:groups', 'all:groups'] + }, + delete: { + controller: 'GroupRoleController', + method: 'deleteGroupRole', + auth: 'jwt', + access: [constants.UserRoles.Admin], + scopes: ['write:groups', 'all:groups'] + } + }, '/health': { get: { controller: 'HealthController', diff --git a/src/services/GroupMembershipService.js b/src/services/GroupMembershipService.js index 153dd09..49c4025 100644 --- a/src/services/GroupMembershipService.js +++ b/src/services/GroupMembershipService.js @@ -10,6 +10,7 @@ const logger = require('../common/logger') const errors = require('../common/errors') const validate = require('uuid-validate') const constants = require('../../app-constants') +const {ConsoleTransportOptions} = require('winston/lib/winston/transports') /** * Add group member. @@ -34,24 +35,27 @@ async function addGroupMember(currentUser, groupId, data) { data.oldId = group.oldId groupId = group.id + const memberId = data.memberId ? data.memberId : data.universalUID + if ( currentUser !== 'M2M' && !helper.hasAdminRole(currentUser) && + !(await helper.hasGroupRole(tx, groupId, currentUser.userId, ['groupManager', 'groupAdmin'])) && !( group.selfRegister && data.membershipType === config.MEMBERSHIP_TYPES.User && - Number(currentUser.userId) === Number(data.memberId) + Number(currentUser.userId) === Number(memberId) ) ) { throw new errors.ForbiddenError('You are not allowed to perform this action!') } if (data.membershipType === config.MEMBERSHIP_TYPES.Group) { - if (data.memberId === groupId) { + if (memberId === groupId) { throw new errors.BadRequestError('A group can not add to itself.') } - logger.debug(`Check for groupId ${data.memberId} exist or not`) - const childGroup = await helper.ensureExists(tx, 'Group', data.memberId) + logger.debug(`Check for groupId ${memberId} exist or not`) + const childGroup = await helper.ensureExists(tx, 'Group', memberId) data.memberOldId = childGroup.oldId // if parent group is private, the sub group must be private too @@ -59,16 +63,26 @@ async function addGroupMember(currentUser, groupId, data) { throw new errors.ConflictError('Parent group is private, the child group must be private too.') } } else { - logger.debug(`Check for memberId ${data.memberId} exist or not`) - await helper.ensureExists(tx, 'User', data.memberId) + logger.debug(`Check for memberId ${memberId} exist or not`) + await helper.ensureExists(tx, 'User', memberId) } - logger.debug(`check member ${data.memberId} is part of group ${groupId}`) + logger.debug(`check member ${memberId} is part of group ${groupId}`) const targetObjectType = data.membershipType === config.MEMBERSHIP_TYPES.Group ? 'Group' : 'User' - const memberCheckRes = await tx.run( - `MATCH (g:Group {id: {groupId}})-[r:GroupContains]->(o:${targetObjectType} {id: {memberId}}) RETURN o`, - { groupId, memberId: data.memberId } - ) + + let memberCheckRes + if (data.universalUID) { + memberCheckRes = await tx.run( + `MATCH (g:Group {id: {groupId}})-[r:GroupContains]->(o:${targetObjectType} {universalUID: {memberId}}) RETURN o`, + {groupId, memberId} + ) + } else { + memberCheckRes = await tx.run( + `MATCH (g:Group {id: {groupId}})-[r:GroupContains]->(o:${targetObjectType} {id: {memberId}}) RETURN o`, + {groupId, memberId: data.memberId} + ) + } + if (memberCheckRes.records.length > 0) { throw new errors.ConflictError('The member is already in the group') } @@ -77,7 +91,7 @@ async function addGroupMember(currentUser, groupId, data) { if (data.membershipType === config.MEMBERSHIP_TYPES.Group) { const pathRes = await tx.run( 'MATCH p=shortestPath( (g1:Group {id: {fromId}})-[*]->(g2:Group {id: {toId}}) ) RETURN p', - { fromId: data.memberId, toId: groupId } + {fromId: data.memberId, toId: groupId} ) if (pathRes.records.length > 0) { throw new errors.ConflictError('There is cyclical group reference') @@ -87,11 +101,17 @@ async function addGroupMember(currentUser, groupId, data) { // add membership const membershipId = uuid() const createdAt = new Date().toISOString() - const query = `MATCH (g:Group {id: {groupId}}) MATCH (o:${targetObjectType} {id: {memberId}}) CREATE (g)-[r:GroupContains {id: {membershipId}, type: {membershipType}, createdAt: {createdAt}, createdBy: {createdBy}}]->(o) RETURN r` + + let query + if (validate(memberId, 4) && data.universalUID) { + query = `MATCH (g:Group {id: {groupId}}) MATCH (o:User {universalUID: {memberId}}) CREATE (g)-[r:GroupContains {id: {membershipId}, type: {membershipType}, createdAt: {createdAt}, createdBy: {createdBy}}]->(o) RETURN r` + } else { + query = `MATCH (g:Group {id: {groupId}}) MATCH (o:${targetObjectType} {id: {memberId}}) CREATE (g)-[r:GroupContains {id: {membershipId}, type: {membershipType}, createdAt: {createdAt}, createdBy: {createdBy}}]->(o) RETURN r` + } const params = { groupId, - memberId: data.memberId, + memberId, membershipId, membershipType: data.membershipType, createdAt, @@ -107,9 +127,10 @@ async function addGroupMember(currentUser, groupId, data) { oldId: data.oldId, name: group.name, createdAt, - ...(currentUser === 'M2M' ? {} : { createdBy: currentUser.userId }), - memberId: data.memberId, - ...(data.memberOldId ? { memberOldId: data.memberOldId } : {}), + ...(currentUser === 'M2M' ? {} : {createdBy: currentUser.userId}), + ...(data.memberId ? {memberId: data.memberId} : {}), + ...(data.universalUID ? {universalUID: data.universalUID} : {}), + ...(data.memberOldId ? {memberOldId: data.memberOldId} : {}), membershipType: data.membershipType } @@ -129,16 +150,28 @@ async function addGroupMember(currentUser, groupId, data) { } } -addGroupMember.schema = { - currentUser: Joi.any(), - groupId: Joi.id(), // defined in app-bootstrap - data: Joi.object() - .keys({ - memberId: Joi.id(), - membershipType: Joi.string().valid(_.values(config.MEMBERSHIP_TYPES)).required() - }) - .required() -} +addGroupMember.schema = Joi.alternatives().try( + Joi.object().keys({ + currentUser: Joi.any(), + groupId: Joi.id(), // defined in app-bootstrap + data: Joi.object() + .keys({ + memberId: Joi.id(), + membershipType: Joi.string().valid(_.values(config.MEMBERSHIP_TYPES)).required() + }) + .required() + }), + Joi.object().keys({ + currentUser: Joi.any(), + groupId: Joi.id(), // defined in app-bootstrap + data: Joi.object() + .keys({ + universalUID: Joi.id(), + membershipType: Joi.string().valid(_.values(config.MEMBERSHIP_TYPES)).required() + }) + .required() + }) +) /** * Delete group member. @@ -147,7 +180,7 @@ addGroupMember.schema = { * @param {String} memberId the member id * @returns {Object} the deleted group membership */ -async function deleteGroupMember(currentUser, groupId, memberId) { +async function deleteGroupMember(currentUser, groupId, memberId, query) { logger.debug(`Enter in deleteGroupMember - Group = ${groupId} memberId = ${memberId}`) let session = helper.createDBSession() let tx = session.beginTransaction() @@ -163,18 +196,31 @@ async function deleteGroupMember(currentUser, groupId, memberId) { groupId = group.id const oldId = group.oldId const name = group.name + const universalUID = query ? query.universalUID : null if ( currentUser !== 'M2M' && !helper.hasAdminRole(currentUser) && + !(await helper.hasGroupRole(tx, groupId, currentUser.userId, ['groupManager', 'groupAdmin'])) && !(group.selfRegister && currentUser.userId === memberId) ) { throw new errors.ForbiddenError('You are not allowed to perform this action!') } // delete membership - const query = 'MATCH (g:Group {id: {groupId}})-[r:GroupContains]->(o {id: {memberId}}) DELETE r' - await tx.run(query, { groupId, memberId }) + if (universalUID) { + const query = 'MATCH (g:Group {id: {groupId}})-[r:GroupContains]->(o {universalUID: {universalUID}}) DELETE r' + await tx.run(query, {groupId, universalUID}) + + const matchClause = 'MATCH (u:User {universalUID: {universalUID}})' + const params = {universalUID} + + const res = await tx.run(`${matchClause} RETURN u.id as memberId`, params) + memberId = _.head(_.head(res.records)._fields) + } else { + const query = 'MATCH (g:Group {id: {groupId}})-[r:GroupContains]->(o {id: {memberId}}) DELETE r' + await tx.run(query, {groupId, memberId}) + } if (validate(memberId, 4)) { const getMember = await helper.ensureExists(tx, 'Group', memberId) @@ -207,7 +253,8 @@ async function deleteGroupMember(currentUser, groupId, memberId) { deleteGroupMember.schema = { currentUser: Joi.any(), groupId: Joi.id(), // defined in app-bootstrap - memberId: Joi.id() + memberId: Joi.optionalId().allow('', null), + query: Joi.object().allow('', null) } /** @@ -235,7 +282,7 @@ async function getGroupMembers(currentUser, groupId, criteria) { } const matchClause = 'MATCH (g:Group {id: {groupId}})-[r:GroupContains]->(o)' - const params = { groupId } + const params = {groupId} // query total record count const totalRes = await session.run(`${matchClause} RETURN COUNT(o)`, params) @@ -258,6 +305,7 @@ async function getGroupMembers(currentUser, groupId, criteria) { createdAt: r.createdAt, createdBy: r.createdBy, memberId: o.id, + universalUID: o.universalUID, membershipType: r.type } }) @@ -299,7 +347,7 @@ async function getGroupMemberWithSession(session, groupId, memberId) { groupId = group.id const query = 'MATCH (g:Group {id: {groupId}})-[r:GroupContains]->(o {id: {memberId}}) RETURN r' - const membershipRes = await session.run(query, { groupId, memberId }) + const membershipRes = await session.run(query, {groupId, memberId}) if (membershipRes.records.length === 0) { throw new errors.NotFoundError('The member is not in the group') } @@ -370,14 +418,14 @@ async function getGroupMembersCount(groupId, query) { let queryToExecute = '' if (query.includeSubGroups) { - queryToExecute = 'MATCH (g:Group {id: {groupId}})-[r:GroupContains*1..10]->(o:User) RETURN COUNT(o) AS count' + queryToExecute = 'MATCH (g:Group {id: {groupId}})-[r:GroupContains*1..]->(o:User) RETURN COUNT(o) AS count' } else { queryToExecute = 'MATCH (g:Group {id: {groupId}})-[r:GroupContains]->(o:User) RETURN COUNT(o) AS count' } - const res = await session.run(queryToExecute, { groupId }) + const res = await session.run(queryToExecute, {groupId}) - return { count: res.records[0]._fields[0].low } + return {count: res.records[0]._fields[0].low} } catch (error) { logger.error(error) throw error @@ -388,17 +436,107 @@ async function getGroupMembersCount(groupId, query) { } getGroupMembersCount.schema = { - groupId: Joi.id(), // defined in app-bootstrap + groupId: Joi.optionalId(), // defined in app-bootstrap query: Joi.object().keys({ includeSubGroups: Joi.boolean().default(false) }) } +/** + * Get list of groups for specified user and member count of those groups. Optionally may include sub groups. + * @param {Object} query the query parameters + * @returns {Object} list of groupId and memberCount mapping + */ +async function listGroupsMemberCount(query) { + const session = helper.createDBSession() + try { + let queryToExecute = '' + + if (query.includeSubGroups) { + queryToExecute = + 'MATCH (g:Group)-[r:GroupContains*1..]->(o:User) WHERE exists(g.oldId) AND g.status = {status} RETURN g.oldId, g.id, COUNT(o) AS count order by g.oldId' + } else { + queryToExecute = + 'MATCH (g:Group)-[r:GroupContains]->(o:User) WHERE exists(g.oldId) AND g.status = {status} RETURN g.oldId, g.id, COUNT(o) AS count order by g.oldId' + } + let res = await session.run(queryToExecute, {status: constants.GroupStatus.Active}) + + const groupsMemberCount = [] + res.records.forEach(function (record) { + let groupMemberCount = { + id: record._fields[1], + oldId: record._fields[0], + count: record._fields[2].low + } + groupsMemberCount.push(groupMemberCount) + }) + + if (query.includeSubGroups) { + if (query.universalUID) { + queryToExecute = + 'MATCH (g:Group)-[r:GroupContains*1..]->(o:User {universalUID: {universalUID}}) WHERE exists(g.oldId) AND g.status = {status} RETURN DISTINCT g.oldId order by g.oldId' + res = await session.run(queryToExecute, { + universalUID: query.universalUID, + status: constants.GroupStatus.Active + }) + } + + if (query.organizationId) { + queryToExecute = + 'MATCH (g:Group)-[r:GroupContains*1..]->(o:User) WHERE g.organizationId = {organizationId} AND exists(g.oldId) AND g.status = {status} RETURN DISTINCT g.oldId order by g.oldId' + res = await session.run(queryToExecute, { + organizationId: query.organizationId, + status: constants.GroupStatus.Active + }) + } + } else { + if (query.universalUID) { + queryToExecute = + 'MATCH (g:Group)-[r:GroupContains]->(o:User {universalUID: {universalUID}}) WHERE exists(g.oldId) AND g.status = {status} RETURN DISTINCT g.oldId order by g.oldId' + res = await session.run(queryToExecute, { + universalUID: query.universalUID, + status: constants.GroupStatus.Active + }) + } + + if (query.organizationId) { + queryToExecute = + 'MATCH (g:Group)-[r:GroupContains]->(o:User) WHERE g.organizationId = {organizationId} AND exists(g.oldId) AND g.status = {status} RETURN DISTINCT g.oldId order by g.oldId' + res = await session.run(queryToExecute, { + organizationId: query.organizationId, + status: constants.GroupStatus.Active + }) + } + } + + const groupList = _.flatten(_.map(res.records, '_fields')) + const finalRes = _.filter(groupsMemberCount, function (n) { + return _.includes(groupList, n.oldId) + }) + + return finalRes + } catch (error) { + logger.error(error) + throw error + } finally { + logger.debug('Session Close') + await session.close() + } +} + +listGroupsMemberCount.schema = { + query: Joi.object().keys({ + includeSubGroups: Joi.boolean().default(false), + universalUID: Joi.optionalId(), + organizationId: Joi.optionalId() + }) +} + /** * Get member groups * @param {Object} currentUser the current user * @param {Object} memberId - * @param {Object} criteria the search criteria + * @param {Object} query the search criteria * @returns {Object} the search result */ async function getMemberGroups(currentUser, memberId) { @@ -429,6 +567,7 @@ module.exports = { getGroupMember, deleteGroupMember, getGroupMembersCount, + listGroupsMemberCount, getMemberGroups } diff --git a/src/services/GroupRoleService.js b/src/services/GroupRoleService.js new file mode 100644 index 0000000..4fc391a --- /dev/null +++ b/src/services/GroupRoleService.js @@ -0,0 +1,178 @@ +/** + * This service provides operations of groups + */ +const _ = require('lodash') +const config = require('config') +const Joi = require('joi') +const helper = require('../common/helper') +const logger = require('../common/logger') +const errors = require('../common/errors') +const constants = require('../../app-constants') + +/** + * Get the groups and roles of the user + * @param {String} userId the user id + * @param {Object} criteria the pagination properties + * @returns {Object} an object contains an array of objects, where the object has two properties: the groupId and the role + */ +async function getGroupRole (userId, criteria) { + logger.debug(`Get Group Role - UserId - ${userId} , Criteria - ${JSON.stringify(criteria)}`) + const session = helper.createDBSession() + try { + const matchClause = `MATCH (g:Group)-[r:GroupContains {type: "${config.MEMBERSHIP_TYPES.User}"}]->(o {id: "${userId}"}) UNWIND r.roles as role` + const totalRes = await session.run(`${matchClause} RETURN COUNT(role)`) + const total = totalRes.records[0].get(0).low || 0 + + const pageRes = await session.run( + `${matchClause} RETURN g.id, role ORDER BY g.id, role SKIP ${(criteria.page - 1) * criteria.perPage} LIMIT ${ + criteria.perPage + }` + ) + + const result = _.map(pageRes.records, (record) => ({ groupId: record._fields[0], ...JSON.parse(record._fields[1]) })) + return { result, total, perPage: criteria.perPage, page: criteria.page } + } catch (error) { + logger.error(error) + throw error + } finally { + logger.debug('Session Close') + await session.close() + } +} + +getGroupRole.schema = { + userId: Joi.id(), + criteria: Joi.object().keys({ + page: Joi.page(), + perPage: Joi.perPage() + }) +} + +/** + * Add a new combination of role and group to the group roles for the user + * @param {Object} currentUser the current user + * @param {String} userId the id of user to add role + * @param {String} groupId the id of group to add role + * @param {String} role the name of role to add + * @returns {Object} the added role + */ +async function addGroupRole (currentUser, userId, groupId, role) { + logger.debug(`Add Group Role - user - ${userId} , group - ${groupId}, role - ${role}`) + const session = helper.createDBSession() + const tx = session.beginTransaction() + try { + await helper.ensureExists(tx, 'Group', groupId, currentUser !== 'M2M' && helper.hasAdminRole(currentUser)) + await helper.ensureExists(tx, 'User', userId, currentUser !== 'M2M' && helper.hasAdminRole(currentUser)) + + const res = await tx.run(`MATCH (g:Group {id: "${groupId}"})-[r:GroupContains {type: "${config.MEMBERSHIP_TYPES.User}"}]->(o {id: "${userId}"}) return r`) + + if (res.records.length === 0) { + throw new errors.BadRequestError(`Not found Relation between member: ${userId} and Group: ${groupId}`) + } + const membership = res.records[0]._fields[0].properties + const roles = membership.roles || [] + + if (_.some(roles, r => JSON.parse(r).role === role)) { + throw new errors.ConflictError(`The group role: ${role} of the member: ${userId} is already in the group: ${groupId}`) + } + const roleObject = { + role, + createdAt: new Date().toISOString(), + createdBy: currentUser === 'M2M' ? '00000000' : currentUser.userId + } + roles.push(JSON.stringify(roleObject, ['role', 'createdAt', 'createdBy'])) + + logger.debug(`Membership ${JSON.stringify(membership)} to add role ${role}`) + + await tx.run('MATCH (:Group)-[r:GroupContains {type: {type}, id: {id}}]-> () SET r.roles={roles} RETURN r', { type: config.MEMBERSHIP_TYPES.User, id: membership.id, roles }) + + await helper.postBusEvent(config.KAFKA_GROUP_MEMBER_ROLE_ADD_TOPIC, { id: membership.id, userId, groupId, role }) + await tx.commit() + return membership + } catch (error) { + logger.error(error) + logger.debug('Transaction Rollback') + await tx.rollback() + if (error.name === 'NotFoundError') { + throw new errors.BadRequestError(`Not found Group of id ${groupId}`) + } else { + throw error + } + } finally { + logger.debug('Session Close') + await session.close() + } +} + +addGroupRole.schema = { + currentUser: Joi.any(), + userId: Joi.id(), + groupId: Joi.id(), + role: Joi.string().valid(constants.GroupRoleName).required() +} + +/** + * Delete group role + * @param {String} userId the user id + * @param {String} groupId the group id + * @param {String} role the role name + * @param {Boolean} isAdmin flag indicating whether the current user is an admin or not + * @returns {Object} the deleted group role + */ +async function deleteGroupRole (userId, groupId, role, isAdmin) { + const session = helper.createDBSession() + const tx = session.beginTransaction() + try { + logger.debug(`Delete Group Role - user - ${userId} , group - ${groupId}, role - ${role}`) + await helper.ensureExists(tx, 'Group', groupId, isAdmin) + await helper.ensureExists(tx, 'User', userId, isAdmin) + + const res = await tx.run(`MATCH (g:Group {id: "${groupId}"})-[r:GroupContains {type: "${config.MEMBERSHIP_TYPES.User}"}]->(o {id: "${userId}"}) return r`) + + if (res.records.length === 0) { + throw new errors.BadRequestError(`Not found Group Role: ${role} of Member: ${userId} in the Group ${groupId}`) + } + const membership = res.records[0]._fields[0].properties + const roles = membership.roles || [] + + if (!_.some(roles, r => JSON.parse(r).role === role)) { + throw new errors.BadRequestError(`Not found Group Role: ${role} of Member: ${userId} in the Group ${groupId}`) + } + _.remove(roles, r => JSON.parse(r).role === role) + + logger.debug(`Membership ${JSON.stringify(membership)} to delete role ${role}`) + + await tx.run('MATCH (:Group)-[r:GroupContains {type: {type}, id: {id}}]-> () SET r.roles={roles} RETURN r', { type: config.MEMBERSHIP_TYPES.User, id: membership.id, roles }) + + await helper.postBusEvent(config.KAFKA_GROUP_MEMBER_ROLE_DELETE_TOPIC, { id: membership.id, userId, groupId, role }) + await tx.commit() + return membership + } catch (error) { + logger.error(error) + logger.debug('Transaction Rollback') + await tx.rollback() + if (error.name === 'NotFoundError') { + throw new errors.BadRequestError(`Not found Group of id ${groupId}`) + } else { + throw error + } + } finally { + logger.debug('Session Close') + await session.close() + } +} + +deleteGroupRole.schema = { + groupId: Joi.id(), + userId: Joi.id(), + role: Joi.string().valid(constants.GroupRoleName).required(), + isAdmin: Joi.boolean().required() +} + +module.exports = { + getGroupRole, + addGroupRole, + deleteGroupRole +} + +logger.buildService(module.exports) diff --git a/src/services/GroupService.js b/src/services/GroupService.js index 27aab31..61d40b2 100644 --- a/src/services/GroupService.js +++ b/src/services/GroupService.js @@ -4,7 +4,6 @@ const _ = require('lodash') const config = require('config') const Joi = require('joi') -const uuid = require('uuid/v4') const helper = require('../common/helper') const logger = require('../common/logger') const errors = require('../common/errors') @@ -16,21 +15,24 @@ const constants = require('../../app-constants') * @param {Boolean} isAdmin flag indicating whether the current user is an admin or not * @returns {Object} the search result */ -async function searchGroups(criteria, isAdmin = false) { +async function searchGroups(criteria, isAdmin) { logger.debug(`Search Group - Criteria - ${JSON.stringify(criteria)}`) - if (criteria.memberId && !criteria.membershipType) { + if ((criteria.memberId || criteria.universalUID) && !criteria.membershipType) { throw new errors.BadRequestError('The membershipType parameter should be provided if memberId is provided.') } - if (!criteria.memberId && criteria.membershipType) { + if (!(criteria.memberId || criteria.universalUID) && criteria.membershipType) { throw new errors.BadRequestError('The memberId parameter should be provided if membershipType is provided.') } const session = helper.createDBSession() try { let matchClause + if (criteria.memberId) { matchClause = `MATCH (g:Group)-[r:GroupContains {type: "${criteria.membershipType}"}]->(o {id: "${criteria.memberId}"})` + } else if (criteria.universalUID) { + matchClause = `MATCH (g:Group)-[r:GroupContains {type: "${criteria.membershipType}"}]->(o {universalUID: "${criteria.universalUID}"})` } else { matchClause = `MATCH (g:Group)` } @@ -91,13 +93,16 @@ async function searchGroups(criteria, isAdmin = false) { whereClause = ` WHERE g.status = '${constants.GroupStatus.Active}'` } else { whereClause = whereClause.concat(` AND g.status = '${constants.GroupStatus.Active}'`) - } + } } // query total record count const totalRes = await session.run(`${matchClause}${whereClause} RETURN COUNT(g)`) const total = totalRes.records[0].get(0).low || 0 + console.log(`${matchClause}${whereClause} RETURN g ORDER BY g.oldId SKIP ${(criteria.page - 1) * criteria.perPage} + LIMIT ${criteria.perPage}`) + // query page of records let result = [] if (criteria.page <= Math.ceil(total / criteria.perPage)) { @@ -149,6 +154,7 @@ searchGroups.schema = { isAdmin: Joi.boolean(), criteria: Joi.object().keys({ memberId: Joi.optionalId(), // defined in app-bootstrap + universalUID: Joi.optionalId(), membershipType: Joi.string().valid(_.values(config.MEMBERSHIP_TYPES)), name: Joi.string(), page: Joi.page(), @@ -160,7 +166,10 @@ searchGroups.schema = { privateGroup: Joi.boolean(), includeSubGroups: Joi.boolean().default(false), includeParentGroup: Joi.boolean().default(false), - oneLevel: Joi.boolean() + oneLevel: Joi.boolean(), + status: Joi.string() + .valid([constants.GroupStatus.Active, constants.GroupStatus.InActive]) + .default(constants.GroupStatus.Active) }) } @@ -176,31 +185,7 @@ async function createGroup(currentUser, data) { try { logger.debug(`Create Group - user - ${currentUser} , data - ${JSON.stringify(data)}`) - // check whether group name is already used - const nameCheckRes = await tx.run('MATCH (g:Group {name: {name}}) RETURN g LIMIT 1', { - name: data.name - }) - if (nameCheckRes.records.length > 0) { - throw new errors.ConflictError(`The group name ${data.name} is already used`) - } - - // create group - const groupData = data - - // generate next group id - groupData.id = uuid() - groupData.createdAt = new Date().toISOString() - groupData.createdBy = currentUser === 'M2M' ? '00000000' : currentUser.userId - groupData.domain = groupData.domain ? groupData.domain : '' - groupData.ssoId = groupData.ssoId ? groupData.ssoId : '' - groupData.organizationId = groupData.organizationId ? groupData.organizationId : '' - - const createRes = await tx.run( - `CREATE (group:Group {id: {id}, name: {name}, description: {description}, privateGroup: {privateGroup}, selfRegister: {selfRegister}, createdAt: {createdAt}, createdBy: {createdBy}, domain: {domain}, ssoId: {ssoId}, organizationId: {organizationId}, status: {status}}) RETURN group`, - groupData - ) - - const group = createRes.records[0]._fields[0].properties + const group = await helper.createGroup(tx, data, currentUser) logger.debug(`Group = ${JSON.stringify(group)}`) @@ -459,41 +444,7 @@ async function deleteGroup(groupId, isAdmin) { logger.debug(`Delete Group - ${groupId}`) const group = await helper.ensureExists(tx, 'Group', groupId, isAdmin) - const groupsToDelete = [group] - let index = 0 - while (index < groupsToDelete.length) { - const g = groupsToDelete[index] - index += 1 - - const childGroups = await helper.getChildGroups(tx, g.id) - for (let i = 0; i < childGroups.length; i += 1) { - const child = childGroups[i] - if (_.find(groupsToDelete, (gtd) => gtd.id === child.id)) { - // the child was checked, ignore duplicate processing - continue - } - // delete child if it doesn't belong to other group - const parents = await helper.getParentGroups(tx, child.id) - if (parents.length <= 1) { - groupsToDelete.push(child) - } - } - } - - logger.debug(`Groups to delete ${JSON.stringify(groupsToDelete)}`) - - for (let i = 0; i < groupsToDelete.length; i += 1) { - const id = groupsToDelete[i].id - // delete group's relationships - await tx.run('MATCH (g:Group {id: {groupId}})-[r]-() DELETE r', { - groupId: id - }) - - // delete group - await tx.run('MATCH (g:Group {id: {groupId}}) DELETE g', { - groupId: id - }) - } + const groupsToDelete = await helper.deleteGroup(tx, group) const kafkaPayload = {} kafkaPayload.groups = groupsToDelete diff --git a/src/services/SubGroupService.js b/src/services/SubGroupService.js new file mode 100644 index 0000000..9e799a6 --- /dev/null +++ b/src/services/SubGroupService.js @@ -0,0 +1,152 @@ +/** + * This service provides operations of sub groups + */ +const config = require('config') +const Joi = require('joi') +const uuid = require('uuid/v4') +const helper = require('../common/helper') +const logger = require('../common/logger') +const errors = require('../common/errors') +const constants = require('../../app-constants') + +/** + * Create sub group. + * @param {Object} currentUser the current user + * @param {String} groupId the parent group id + * @param {Object} data the sub group data to create group + * @returns {Object} the created group + */ +async function createSubGroup (currentUser, groupId, data) { + logger.debug(`Create Sub Group - user - ${currentUser} , groupId - ${groupId} , data - ${JSON.stringify(data)}`) + + const session = helper.createDBSession() + const tx = session.beginTransaction() + try { + if ( + currentUser !== 'M2M' && + !helper.hasAdminRole(currentUser) && + !(await helper.hasGroupRole(tx, groupId, currentUser.userId, ['groupAdmin'])) + ) { + throw new errors.ForbiddenError('You are not allowed to perform this action!') + } + + await helper.ensureExists( + tx, + 'Group', + groupId, + currentUser !== 'M2M' && helper.hasAdminRole(currentUser) + ) + + const subGroup = await helper.createGroup(tx, data, currentUser) + + logger.debug(`SubGroup = ${JSON.stringify(subGroup)}`) + + const membershipId = uuid() + + await tx.run('MATCH (g:Group {id: {groupId}}) MATCH (o:Group {id: {subGroupId}}) CREATE (g)-[r:GroupContains {id: {membershipId}, type: {membershipType}, createdAt: {createdAt}, createdBy: {createdBy}}]->(o) RETURN r', + { groupId, subGroupId: subGroup.id, membershipId, membershipType: config.MEMBERSHIP_TYPES.Group, createdAt: new Date().toISOString(), createdBy: currentUser === 'M2M' ? '00000000' : currentUser.userId }) + + const result = { + id: membershipId, + groupId, + subGroup + } + + // post bus event + await helper.postBusEvent(config.KAFKA_SUBGROUP_CREATE_TOPIC, result) + await tx.commit() + return subGroup + } catch (error) { + logger.error(error) + logger.debug('Transaction Rollback') + await tx.rollback() + throw error + } finally { + logger.debug('Session Close') + await session.close() + } +} + +createSubGroup.schema = { + currentUser: Joi.any(), + groupId: Joi.string().required(), + data: Joi.object() + .keys({ + name: Joi.string().min(3).max(150).required(), + description: Joi.string().min(3).max(2048), + privateGroup: Joi.boolean().required(), + selfRegister: Joi.boolean().required(), + domain: Joi.string(), + ssoId: Joi.string(), + organizationId: Joi.optionalId(), + status: Joi.string() + .valid([constants.GroupStatus.Active, constants.GroupStatus.InActive]) + .default(constants.GroupStatus.Active) + }) + .required() +} + +/** + * Delete sub group + * @param {Object} currentUser the current user + * @param {String} groupId the group id + * @param {String} subGroupId the sub group id + * @returns {Object} the deleted group + */ +async function deleteSubGroup (currentUser, groupId, subGroupId) { + logger.debug(`Delete Sub Group - ${groupId}, Sub Group - ${subGroupId}`) + const session = helper.createDBSession() + const tx = session.beginTransaction() + try { + if ( + currentUser !== 'M2M' && + !helper.hasAdminRole(currentUser) && + !(await helper.hasGroupRole(tx, groupId, currentUser.userId, ['groupAdmin'])) + ) { + throw new errors.ForbiddenError('You are not allowed to perform this action!') + } + + const group = await helper.ensureExists(tx, 'Group', groupId, currentUser !== 'M2M' && helper.hasAdminRole(currentUser)) + const subGroup = await helper.ensureExists(tx, 'Group', subGroupId, currentUser !== 'M2M' && helper.hasAdminRole(currentUser)) + + const res = await tx.run(`MATCH (g:Group {id: "${groupId}"})-[r:GroupContains {type: "${config.MEMBERSHIP_TYPES.Group}"}]->(o {id: "${subGroupId}"}) return r`) + if (res.records.length === 0) { + throw new errors.BadRequestError(`The Gourp: ${subGroupId} is not the child of Group: ${subGroupId}`) + } + + // delete relationship + await tx.run('MATCH (g:Group {id: {groupId}})-[r:GroupContains]->(o {id: {subGroupId}}) DELETE r', { groupId, subGroupId }) + + // delete sub group + const groupsToDelete = await helper.deleteGroup(tx, subGroup) + + const kafkaPayload = { + groupId, + subGroup: groupsToDelete + } + await helper.postBusEvent(config.KAFKA_SUBGROUP_DELETE_TOPIC, kafkaPayload) + await tx.commit() + return group + } catch (error) { + logger.error(error) + logger.debug('Transaction Rollback') + await tx.rollback() + throw error + } finally { + logger.debug('Session Close') + await session.close() + } +} + +deleteSubGroup.schema = { + currentUser: Joi.any(), + groupId: Joi.string().required(), + subGroupId: Joi.string().required() +} + +module.exports = { + createSubGroup, + deleteSubGroup +} + +logger.buildService(module.exports)