src/server/app/teams/services/teams.server.service.js
'use strict';
let
path = require('path'),
q = require('q'),
_ = require('lodash'),
deps = require(path.resolve('./src/server/dependencies.js')),
config = deps.config,
dbs = deps.dbs,
auditService = deps.auditService,
util = deps.utilService,
Tag = dbs.admin.model('Tag'),
TeamMember = dbs.admin.model('TeamUser'),
Team = dbs.admin.model('Team'),
TeamRole = dbs.admin.model('TeamRole');
module.exports = function() {
let teamRolesMap = {
member: { priority: 1 },
editor: { priority: 5 },
admin: { priority: 7 }
};
// Array of team role keys
let teamRoles = _.keys(teamRolesMap);
/**
* Copies the mutable fields from src to dest
*
* @param dest
* @param src
*/
function copyTeamMutableFields(dest, src) {
dest.name = src.name;
dest.description = src.description;
dest.requiresExternalTeams = src.requiresExternalTeams;
}
/**
* Gets the role of this user in this team.
*
* @param team The team object of interest
* @param user The user object of interest
* @returns Returns the role of the user in the team or null if user doesn't belong to team.
*/
function getTeamRole(user, team) {
let ndx = _.findIndex(user.teams, (t) => t._id.equals(team._id));
if (-1 !== ndx) {
return user.teams[ndx].role;
}
return null;
}
/**
* Checks if the user meets the required external teams for this team
* If the user is bypassed, they automatically meet the required external teams
*
* @param user
* @param team
* @returns {boolean}
*/
function meetsRequiredExternalTeams(user, team) {
if(true === user.bypassAccessCheck) {
return true;
} else {
// Check the required external teams against the user's externalGroups
return _.intersection(team.requiresExternalTeams, user.externalGroups).length > 0;
}
}
function meetsRoleRequirement(user, team, role) {
// Check role of the user in this team
let userRole = getActiveTeamRole(user, team);
if (null != userRole && meetsOrExceedsRole(userRole, role)) {
return q();
}
else {
return q.reject({ status: 403, type: 'missing-roles', message: 'The user does not have the required roles for the team' });
}
}
/**
* Checks if user role meets or exceeds the requestedRole according to
* a pre-defined role hierarchy
*
* @returns {boolean}
*/
function meetsOrExceedsRole(userRole, requestedRole) {
if (null != userRole && teamRolesMap.hasOwnProperty(userRole) && null != requestedRole && teamRolesMap.hasOwnProperty(requestedRole)) {
return (teamRolesMap[userRole].priority >= teamRolesMap[requestedRole].priority);
}
return false;
}
/**
* Gets the team role for the specified user
* and also applies the business logic of if they are implicitly a member
* of the team or if they are an inactive explicit member of a team
*
* @returns Returns a role, or null if the user is not a member of the team
*/
function getActiveTeamRole(user, team) {
// No matter what, we need to get these
let teamRole = getTeamRole(user, team);
let proxyPkiMode = config.auth.strategy === 'proxy-pki';
let teamHasRequirements = (_.isArray(team.requiresExternalTeams) && team.requiresExternalTeams.length > 0);
// If we are in proxy-pki mode and the team has external requirements
if(proxyPkiMode && teamHasRequirements) {
// If the user is active
if(meetsRequiredExternalTeams(user, team)) {
// Return either the team role (if defined), or the default role
return (null != teamRole) ? teamRole : 'member';
}
// The user is inactive
else {
// Return null since no matter what, they are not a member of this team
return null;
}
}
// We are not in proxy-pki mode, or the team has no requirements
else {
// Return the team role
return teamRole;
}
}
/**
* Checks if there are resources that belong to the team
*
* @param team The team object of interest
* @returns A promise that resolves if there are no more resources in the team, and rejects otherwise
*/
function verifyNoResourcesInTeam(team) {
return q();
}
/**
* Checks if the member is the last admin of the team
*
* @param user The user object of interest
* @param team The team object of interest
* @returns {Promise} Returns a promise that resolves if the user is not the last admin, and rejects otherwise
*/
function verifyNotLastAdmin(user, team) {
// Search for all users who have the admin role set to true
return TeamMember.find({
_id: { $ne: user._id },
teams: { $elemMatch: { _id: team._id, role: 'admin' } }
})
.exec()
.then(function(results) {
// Just need to make sure we find one active admin who isn't this user
let adminFound = results.some(function(u) {
let role = getActiveTeamRole(u, team);
return (null != role && role === 'admin');
});
if(adminFound) {
return q();
}
else {
return q.reject({ status: 400, type: 'bad-request', message: 'Team must have at least one admin' });
}
});
}
/**
* Validates that the roles are one of the accepted values
*/
function validateTeamRole(role) {
if (-1 !== teamRoles.indexOf(role)) {
return q();
}
else {
return q.reject({ status: 400, type: 'bad-argument', message: 'Team role does not exist' });
}
}
/**
* Creates a new team with the requested metadata
*
* @param teamInfo
* @param creator The user requesting the create
* @returns {Promise} Returns a promise that resolves if team is successfully created, and rejects otherwise
*/
function createTeam(teamInfo, creator, headers) {
// Create the new team model
let newTeam = new Team(teamInfo);
// Write the auto-generated metadata
newTeam.creator = creator;
newTeam.created = Date.now();
newTeam.updated = Date.now();
newTeam.creatorName = creator.name;
// Audit the creation action
return auditService.audit('team created', 'team', 'create', TeamMember.auditCopy(creator), Team.auditCopy(newTeam), headers)
.then(function() {
// Save the new team
return newTeam.save();
})
.then(function(team) {
// Add creator as first team member with admin role
return addMemberToTeam(creator, team, 'admin', creator);
});
}
/**
* Updates an existing team with fresh metadata
*
* @param team The team object to update
* @param updatedTeam
* @param user The user requesting the update
* @returns {Promise} Returns a promise that resolves if team is successfully updated, and rejects otherwise
*/
function updateTeam(team, updatedTeam, user, headers) {
// Make a copy of the original team for auditing purposes
let originalTeam = Team.auditCopy(team);
// Update the updated date
team.updated = Date.now();
// Copy in the fields that can be changed by the user
copyTeamMutableFields(team, updatedTeam);
// Audit the update action
return auditService.audit('team updated', 'team', 'update', TeamMember.auditCopy(user), {before: originalTeam, after: Team.auditCopy(team)}, headers)
.then(function() {
// Save the updated team
return team.save();
});
}
/**
* Deletes an existing team, after verifying that team contains no more resources.
*
* @param team The team object to delete
* @param user The user requesting the delete
* @returns {Promise} Returns a promise that resolves if team is successfully deleted, and rejects otherwise
*/
function deleteTeam(team, user, headers) {
return verifyNoResourcesInTeam(team)
.then(function() {
// Audit the team delete attempt
return auditService.audit('team deleted', 'team', 'delete', TeamMember.auditCopy(user), Team.auditCopy(team), headers);
})
.then(function() {
// Delete the team and update all members in the team
return q.allSettled([
team.remove(),
TeamMember.update(
{'teams._id': team._id },
{ $pull: { teams: { _id: team._id } } }
),
Tag.remove({owner: team._id})
]);
});
}
function searchTeams(search, query, queryParams, user) {
let page = util.getPage(queryParams);
let limit = util.getLimit(queryParams, 1000);
let offset = page * limit;
// Default to sorting by ID
let sortArr = [{property: '_id', direction: 'DESC'}];
if (null != queryParams.sort && null != queryParams.dir) {
sortArr = [{property: queryParams.sort, direction: queryParams.dir}];
}
return q()
.then(function() {
// If user is not an admin, constrain the results to the user's teams
if (null == user.roles || !user.roles.admin) {
let userObj = user.toObject();
let userTeams = [];
if (null != userObj.teams && _.isArray(userObj.teams)) {
// Get list of user's teams by id
userTeams = userObj.teams.map((t) => t._id.toString());
}
// If the query already has a filter by team, take the intersection
if (null != query._id && null != query._id.$in) {
userTeams = userTeams.filter((t) => query._id.$in.indexOf(t) > -1);
}
// If no remaining teams, return no results
if (userTeams.length === 0) {
return q();
}
else {
query._id = {
$in: userTeams
};
}
}
return Team.search(query, search, limit, offset, sortArr);
})
.then(function(result) {
if (null == result) {
return q({
totalSize: 0,
pageNumber: 0,
pageSize: limit,
totalPages: 0,
elements: []
});
}
else {
return q({
totalSize: result.count,
pageNumber: page,
pageSize: limit,
totalPages: Math.ceil(result.count / limit),
elements: result.results
});
}
});
}
function searchTeamMembers(search, query, queryParams, team) {
let page = util.getPage(queryParams);
let limit = util.getLimit(queryParams);
let offset = page * limit;
// Default to sorting by ID
let sortArr = [{property: '_id', direction: 'DESC'}];
if (null != queryParams.sort && null != queryParams.dir) {
sortArr = [{property: queryParams.sort, direction: queryParams.dir}];
}
return q()
.then(function() {
// Inject the team query parameters
// Finds members explicitly added to the team using the id OR
// members implicitly added by having the externalGroup required by requiresExternalTeam
query = query || {};
query.$or = [
{'teams._id': team._id},
{'externalGroups': {$in: (_.isArray(team.requiresExternalTeams)) ? team.requiresExternalTeams : []}}
];
return TeamMember.search(query, search, limit, offset, sortArr);
})
.then(function(result) {
// Success
// Create the return copy of the users
let members = [];
result.results.forEach((element) => {
members.push(TeamMember.teamCopy(element, team._id));
});
return q({
totalSize: result.count,
pageNumber: page,
pageSize: limit,
totalPages: Math.ceil(result.count/limit),
elements: members
});
});
}
/**
* Adds a new member to the existing team.
*
* @param user The user to add to the team
* @param team The team object
* @param role The role of the user in this team
* @param requester The user requesting the add
* @returns {Promise} Returns a promise that resolves if the user is successfully added to the team, and rejects otherwise
*/
function addMemberToTeam(user, team, role, requester, headers) {
// Audit the member add request
return auditService.audit(`team ${role} added`, 'team-role', 'user add', TeamMember.auditCopy(requester), Team.auditCopyTeamMember(team, user, role), headers)
.then(function() {
return TeamMember.update({ _id: user._id }, { $addToSet: { teams: new TeamRole({ _id: team._id, role: role }) } }).exec();
});
}
function updateMemberRole(user, team, role, requester, headers) {
let currentRole = getTeamRole(user, team);
let updateRolePromise = (null != currentRole && currentRole === 'admin') ? verifyNotLastAdmin(user, team) : q();
return updateRolePromise
.then(function() {
return validateTeamRole(role);
})
.then(function() {
// Audit the member update request
return auditService.audit(`team role changed to ${role}`, 'team-role', 'user add', TeamMember.auditCopy(requester), Team.auditCopyTeamMember(team, user, role), headers);
})
.then(function() {
return TeamMember.findOneAndUpdate({ _id: user._id, 'teams._id': team._id }, { $set: { 'teams.$.role': role } }).exec();
});
}
/**
* Removes an existing member from an existing team, after verifying that member is not the last admin
* on the team.
*
* @param user The user to remove
* @param team The team object
* @param requester The user requesting the removal
* @returns {Promise} Returns a promise that resolves if the user is successfully removed from the team, and rejects otherwise
*/
function removeMemberFromTeam(user, team, requester, headers) {
// Verify the user is not the last admin in the team
return verifyNotLastAdmin(user, team)
.then(function () {
// Audit the user remove
return auditService.audit('team member removed', 'team-role', 'user remove', TeamMember.auditCopy(requester), Team.auditCopyTeamMember(team, user, ''), headers);
})
.then(function () {
// Apply the update
return TeamMember.update({_id: user._id}, {$pull: {teams: {_id: team._id}}}).exec();
});
}
return {
createTeam: createTeam,
updateTeam: updateTeam,
deleteTeam: deleteTeam,
searchTeams: searchTeams,
searchTeamMembers: searchTeamMembers,
meetsOrExceedsRole: meetsOrExceedsRole,
meetsRoleRequirement: meetsRoleRequirement,
meetsRequiredExternalTeams: meetsRequiredExternalTeams,
getActiveTeamRole: getActiveTeamRole,
addMemberToTeam: addMemberToTeam,
updateMemberRole: updateMemberRole,
removeMemberFromTeam: removeMemberFromTeam
};
};