Java tutorial
/******************************************************************************* * 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.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.annotation.WebServlet; import co.mitro.core.accesscontrol.AuthenticatedDB; import co.mitro.core.crypto.KeyInterfaces.KeyFactory; import co.mitro.core.exceptions.MitroServletException; import co.mitro.core.exceptions.UserVisibleException; import co.mitro.core.server.Manager; import co.mitro.core.server.ManagerFactory; import co.mitro.core.server.data.DBAcl; import co.mitro.core.server.data.DBAcl.CyclicGroupError; 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.DBIdentity; import co.mitro.core.server.data.RPC; import co.mitro.core.server.data.RPC.ListMySecretsAndGroupKeysResponse.SecretToPath; import co.mitro.core.server.data.RPC.MitroRPC; import co.mitro.core.server.data.RPC.MutateOrganizationResponse; import co.mitro.core.servlets.ListMySecretsAndGroupKeys.AdminAccess; import co.mitro.core.servlets.ListMySecretsAndGroupKeys.IncludeAuditLogInfo; import com.google.common.base.Joiner; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.j256.ormlite.stmt.DeleteBuilder; @WebServlet("/api/MutateOrganization") public class MutateOrganization extends MitroServlet { private static final long serialVersionUID = 1L; private static final Joiner COMMA_JOINER = Joiner.on(","); public MutateOrganization(ManagerFactory managerFactory, KeyFactory keyFactory) { super(managerFactory, keyFactory); } @Override protected MitroRPC processCommand(MitroRequestContext context) throws IOException, SQLException, MitroServletException { try { RPC.MutateOrganizationRequest in = gson.fromJson(context.jsonRequest, RPC.MutateOrganizationRequest.class); in.promotedMemberEncryptedKeys = MitroServlet.createMapIfNull(in.promotedMemberEncryptedKeys); in.newMemberGroupKeys = MitroServlet.createMapIfNull(in.newMemberGroupKeys); in.adminsToDemote = MitroServlet.uniquifyCollection(in.adminsToDemote); in.membersToRemove = MitroServlet.uniquifyCollection(in.membersToRemove); @SuppressWarnings("deprecation") AuthenticatedDB userDb = AuthenticatedDB.deprecatedNew(context.manager, context.requestor); DBGroup org = userDb.getOrganizationAsAdmin(in.orgId); Set<String> adminsToDemote = Sets.newHashSet(in.adminsToDemote); Collection<DBAcl> aclsToRemove = new HashSet<>(); Set<Integer> existingAdmins = new HashSet<>(); for (DBAcl acl : org.getAcls()) { DBIdentity u = acl.loadMemberIdentity(context.manager.identityDao); assert (u != null); // toplevel groups should not have group members. if (adminsToDemote.contains(u.getName())) { aclsToRemove.add(acl); adminsToDemote.remove(u.getName()); } else { existingAdmins.add(u.getId()); } } // check for an attempt to promote members who are already admins. Set<Integer> duplicateAdmins = Sets.intersection(existingAdmins, DBIdentity.getUserIdsFromNames(context.manager, in.promotedMemberEncryptedKeys.keySet())); if (!duplicateAdmins.isEmpty()) { throw new MitroServletException( "Operation would create duplicate admins: " + COMMA_JOINER.join(duplicateAdmins)); } if (!adminsToDemote.isEmpty()) { throw new MitroServletException("The following users are not admins and could not be deleted:" + COMMA_JOINER.join(adminsToDemote)); } if (existingAdmins.isEmpty() && in.promotedMemberEncryptedKeys.isEmpty()) { throw new UserVisibleException("You cannot remove all admins from an organization"); } // delete ACLs for the admin user on the group. This maybe should be using common code? context.manager.aclDao.delete(aclsToRemove); Map<Integer, Integer> currentMemberIdsToGroupIds = getMemberIdsAndPrivateGroupIdsForOrg(context.manager, org); // Promoted members (new admins) must be members after all changes Set<String> currentMembers = DBIdentity.getUserNamesFromIds(context.manager, currentMemberIdsToGroupIds.keySet()); Set<String> membersAfterChanges = Sets.difference( Sets.union(currentMembers, in.newMemberGroupKeys.keySet()), new HashSet<>(in.membersToRemove)); Set<String> nonMemberAdmins = Sets.difference(in.promotedMemberEncryptedKeys.keySet(), membersAfterChanges); if (!nonMemberAdmins.isEmpty()) { throw new MitroServletException( "Cannot add admins without them being members: " + COMMA_JOINER.join(nonMemberAdmins)); } // check for duplicate users Set<Integer> duplicateMembers = Sets.intersection(currentMemberIdsToGroupIds.keySet(), DBIdentity.getUserIdsFromNames(context.manager, in.newMemberGroupKeys.keySet())); if (!duplicateMembers.isEmpty()) { throw new MitroServletException( "Operation would create duplicate members: " + COMMA_JOINER.join(duplicateMembers)); } // delete all the private groups. This might orphan secrets, which is the intended result. Set<Integer> memberIdsToRemove = DBIdentity.getUserIdsFromNames(context.manager, in.membersToRemove); if (memberIdsToRemove.size() != in.membersToRemove.size()) { throw new MitroServletException("Invalid members to remove."); } Set<Integer> illegalRemovals = Sets.intersection(existingAdmins, memberIdsToRemove); if (!illegalRemovals.isEmpty()) { throw new MitroServletException( "Cannot remove members who are admins:" + COMMA_JOINER.join(illegalRemovals)); } Set<Integer> nonOrgUsers = Sets.difference(memberIdsToRemove, currentMemberIdsToGroupIds.keySet()); if (!nonOrgUsers.isEmpty()) { throw new MitroServletException("The following users are not members and cannot be removed:" + COMMA_JOINER.join(nonOrgUsers)); } Set<Integer> deleteGroupIds = Sets.newHashSet( Maps.filterKeys(currentMemberIdsToGroupIds, Predicates.in(memberIdsToRemove)).values()); if (!deleteGroupIds.isEmpty()) { context.manager.groupDao.deleteIds(deleteGroupIds); DeleteBuilder<DBAcl, Integer> deleter = context.manager.aclDao.deleteBuilder(); deleter.where().in(DBAcl.GROUP_ID_FIELD_NAME, deleteGroupIds); deleter.delete(); DeleteBuilder<DBGroupSecret, Integer> gsDeleter = context.manager.groupSecretDao.deleteBuilder(); gsDeleter.where().in(DBGroupSecret.GROUP_ID_NAME, deleteGroupIds); gsDeleter.delete(); } // Remove the user from all org-owned group to which he belongs. // Note: if the user has access to an org-owned secret via a non-org-owned group, // he will retain access. Set<Integer> allOrgGroupIds = Sets.newHashSet(); for (DBGroup g : org.getAllOrgGroups(context.manager)) { allOrgGroupIds.add(g.getId()); } if (!memberIdsToRemove.isEmpty()) { if (!allOrgGroupIds.isEmpty()) { // Remove users from organization-owned groups (named or otherwise) DeleteBuilder<DBAcl, Integer> deleter = context.manager.aclDao.deleteBuilder(); deleter.where().in(DBAcl.MEMBER_IDENTITY_FIELD_NAME, memberIdsToRemove).and() .in(DBAcl.GROUP_ID_FIELD_NAME, allOrgGroupIds); deleter.delete(); } // Remove users from any org-owned secrets (e.g. via non-org private or named groups) HashMap<Integer, SecretToPath> orgSecretsToPath = new HashMap<>(); ListMySecretsAndGroupKeys.getSecretInfo(context, AdminAccess.FORCE_ACCESS_VIA_TOPLEVEL_GROUPS, orgSecretsToPath, ImmutableSet.of(org.getId()), null, IncludeAuditLogInfo.NO_AUDIT_LOG_INFO); if (orgSecretsToPath.size() > 0) { // Delete any group secret giving these users access to org secrets // strange side effect: personal teams may be mysteriously removed from org secrets // TODO: Potential bug: removing the last personal team will "orphan" the secret String groupSecretDelete = String.format( "DELETE FROM group_secret WHERE id IN (" + "SELECT group_secret.id FROM group_secret, acl WHERE " + " group_secret.\"serverVisibleSecret_id\" IN (%s) AND " + " group_secret.group_id = acl.group_id AND acl.member_identity IN (%s))", COMMA_JOINER.join(orgSecretsToPath.keySet()), COMMA_JOINER.join(memberIdsToRemove)); context.manager.groupSecretDao.executeRaw(groupSecretDelete); } } List<DBAcl> organizationAcls = CreateOrganization.makeAdminAclsForOrganization(userDb, org, in.promotedMemberEncryptedKeys); // TODO: move to authdb? for (DBAcl acl : organizationAcls) { context.manager.aclDao.create(acl); } // create private groups for each new member CreateOrganization.addMembersToOrganization(userDb, org, in.newMemberGroupKeys, context.manager); context.manager.addAuditLog(DBAudit.ACTION.MUTATE_ORGANIZATION, null, null, org, null, ""); MutateOrganizationResponse out = new RPC.MutateOrganizationResponse(); // TODO: validate the group? return out; } catch (CyclicGroupError e) { throw new MitroServletException(e); } } public static Map<Integer, Integer> getMemberIdsAndPrivateGroupIdsForOrg(Manager manager, DBGroup org) throws SQLException { String getMembersAndPrivateGroupsQuery = "SELECT " + // MAX is safe to use here since we require a group membership and a count of 2, so // there is only one possible value. "MAX(counter.member_identity), counter.group_id " + "FROM acl as pvtgroup " + "JOIN acl as counter ON (counter.group_id = pvtgroup.group_id) " + "JOIN groups ON (counter.group_id = groups.id) " + "WHERE pvtgroup.group_identity = " + org.getId() + " AND groups.type = 'PRIVATE' " + "GROUP BY counter.group_id " + "HAVING count(counter.group_id) = 2"; Map<Integer, Integer> rval = Maps.newHashMap(); List<String[]> membersAndPrivateGroupsResults = Lists .newArrayList(manager.groupDao.queryRaw(getMembersAndPrivateGroupsQuery)); for (String[] cols : membersAndPrivateGroupsResults) { rval.put(Integer.parseInt(cols[0], 10), Integer.parseInt(cols[1], 10)); } return rval; } }