co.mitro.core.servlets.MutateOrganization.java Source code

Java tutorial

Introduction

Here is the source code for co.mitro.core.servlets.MutateOrganization.java

Source

/*******************************************************************************
 * 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;
    }

}