/*******************************************************************************
* Copyright (c) 2013, 2014 Lectorius, Inc.
* Authors:
* Vijay Pandurangan (vijayp@mitro.co)
* Evan Jones (ej@mitro.co)
* Adam Hilss (ahilss@mitro.co)
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can contact the authors at inbound@mitro.co.
*******************************************************************************/
package co.mitro.core.servlets;
import java.io.IOException;
import java.sql.SQLException;
import java.util.List;
import java.util.Set;
import javax.servlet.annotation.WebServlet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import co.mitro.core.exceptions.InvalidRequestException;
import co.mitro.core.exceptions.MitroServletException;
import co.mitro.core.exceptions.PermissionException;
import co.mitro.core.server.Manager;
import co.mitro.core.server.data.DBAcl;
import co.mitro.core.server.data.DBAudit;
import co.mitro.core.server.data.DBGroup;
import co.mitro.core.server.data.DBGroupSecret;
import co.mitro.core.server.data.DBServerVisibleSecret;
import co.mitro.core.server.data.DBServerVisibleSecret.InvariantException;
import co.mitro.core.server.data.RPC;
import co.mitro.core.server.data.RPC.AddGroupResponse;
import co.mitro.core.server.data.RPC.MitroRPC;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
@WebServlet("/api/EditGroup")
public class EditGroup extends AbstractAddEditGroup {
private static final long serialVersionUID = 1L;
private static final Logger logger = LoggerFactory.getLogger(EditGroup.class);
@Override
protected MitroRPC processCommand(MitroRequestContext context) throws IOException, SQLException, MitroServletException {
RPC.EditGroupRequest in = gson.fromJson(context.jsonRequest,
RPC.EditGroupRequest.class);
// TODO: Remove this after client JS for editing secrets is improved
// for now, editing a secret causes a client to do getgroup/editgroup with no changes
if (in.acls == null || in.acls.isEmpty()) {
logger.info("ignoring empty EditGroup");
AddGroupResponse response = new AddGroupResponse();
response.groupId = in.groupId;
return response;
}
DBGroup existingGroup = context.manager.groupDao.queryForId(in.groupId);
context.manager.userState.trackGroup(existingGroup);
// TODO: this is a hack and allows org admins unfettered control on groups.
// This is bad for the following reasons:
// - the org admin operation should be different, so that an accident
// can't cause a user to use his admin powers to do bad things
//
// - this will allow synced groups to be manually changed by an admin
// which is bad.
Set<Integer> oldPermittedUsersForMembershipChange = getOrgAdminsForGroup(context.manager, existingGroup);
Set<Integer> oldPermittedUsersForSecretRewrite = Sets.newHashSet(oldPermittedUsersForMembershipChange);
existingGroup.putDirectUsersIntoSet(oldPermittedUsersForMembershipChange, DBAcl.adminAccess());
existingGroup.putDirectUsersIntoSet(oldPermittedUsersForSecretRewrite, DBAcl.modifyGroupSecretsAccess());
assert oldPermittedUsersForSecretRewrite.containsAll(oldPermittedUsersForMembershipChange);
// eliminate existing ACLs. These will be recreated later.
Set<Integer> oldGroups = Sets.newHashSet();
Set<Integer> oldUsers = Sets.newHashSet();
DBAcl.getMemberGroupsAndIdentities(existingGroup.getAcls(), oldGroups, oldUsers);
context.manager.aclDao.delete(existingGroup.getAcls());
AddGroupResponse rval = addEditGroupCommand(context.manager, in, existingGroup);
existingGroup = context.manager.groupDao.queryForId(in.groupId);
Set<Integer> newGroups = Sets.newHashSet();
Set<Integer> newUsers = Sets.newHashSet();
DBAcl.getMemberGroupsAndIdentities(existingGroup.getAcls(), newGroups, newUsers);
if (!oldGroups.equals(newGroups) || !oldUsers.equals(newUsers)) {
if (!oldPermittedUsersForMembershipChange.contains(context.requestor.getId())) {
throw new PermissionException("User does not have permission to modify group membership");
}
}
oldGroups.removeAll(newGroups);
oldUsers.removeAll(newUsers);
context.manager.addAuditLog(DBAudit.ACTION.MODIFY_GROUP, null, null, existingGroup, null, null);
if (null == in.secrets) {
if (!oldGroups.isEmpty() || !oldUsers.isEmpty()) {
throw new InvalidRequestException("removal of members requires rewriting secrets");
}
} else {
// secrets are modified if and only if secrets is set in the request.
// ensure that the user has permission for this operation
if (!oldPermittedUsersForSecretRewrite.contains(context.requestor.getId())) {
throw new PermissionException("User does not have permission to modify group");
}
DBGroup updatedGroup = context.manager.groupDao.queryForId(in.groupId);
List<DBGroupSecret> groupSecrets = Lists.newArrayList(updatedGroup.getGroupSecrets());
if (groupSecrets.size() != in.secrets.size()) {
throw new MitroServletException("you cannot change the number of secrets using EditGroup " + groupSecrets.size() + " vs " + in.secrets.size());
}
for (int i = 0; i < in.secrets.size(); ++i) {
if (groupSecrets.get(i).getServerVisibleSecret().getId() != in.secrets.get(i).secretId) {
logger.error("secret index {} id does not match. DB: {} request: {}",
i, groupSecrets.get(i).getServerVisibleSecret().getId(), in.secrets.get(i).secretId);
throw new MitroServletException("Order of secrets may not be changed");
}
// we only need to check for orphaning secrets here because
// this can only happen when users are removed, and that should
// always result in new secret data being set.
try {
groupSecrets.get(i).getServerVisibleSecret().verifyHasAdministrator(context.manager);
} catch (InvariantException e) {
// this operation cannot be allowed
throw new MitroServletException(e);
}
// TODO: verify signature
groupSecrets.get(i).setClientVisibleDataEncrypted(in.secrets.get(i).encryptedClientData);
groupSecrets.get(i).setCriticalDataEncrypted(in.secrets.get(i).encryptedCriticalData);
context.manager.groupSecretDao.update(groupSecrets.get(i));
}
}
// test to ensure that making a group an org group does not result in secrets being owned by multiple orgs.
context.manager.groupDao.refresh(existingGroup);
Set<DBServerVisibleSecret> secrets = Sets.newHashSet();
for (DBGroupSecret gs : existingGroup.getGroupSecrets()) {
secrets.add(gs.getServerVisibleSecret());
}
AddSecret.preventMutipleOrgSecretOwnership(context.manager, secrets);
return rval;
}
// TODO: this should be merged (?) with AuthenticatedDb::getGroupAsUserOrOrgAdmin
public static Set<Integer> getOrgAdminsForGroup(Manager manager, DBGroup existingGroup) throws SQLException {
// is this group part of an org?
Integer orgId = null;
for (DBAcl acl : existingGroup.getAcls()) {
orgId = acl.getMemberGroupIdAsInteger();
if (orgId != null) {
break;
}
}
Set<Integer> orgAdmins = Sets.newHashSet();
if (orgId != null) {
DBGroup org = manager.groupDao.queryForId(orgId);
assert (org != null);
org.putDirectUsersIntoSet(orgAdmins, DBAcl.adminAccess());
}
return orgAdmins;
}
}