/*******************************************************************************
* 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 static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import co.mitro.core.accesscontrol.AuthenticatedDB;
import co.mitro.core.exceptions.MitroServletException;
import co.mitro.core.exceptions.UserVisibleException;
import co.mitro.core.server.data.DBAcl;
import co.mitro.core.server.data.DBAcl.CyclicGroupError;
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.DBServerVisibleSecret;
import co.mitro.core.server.data.RPC.CreateOrganizationRequest.PrivateGroupKeys;
import co.mitro.core.server.data.RPC.MutateOrganizationRequest;
import co.mitro.core.server.data.RPC.MutateOrganizationResponse;
import co.mitro.core.servlets.MitroServlet.MitroRequestContext;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.j256.ormlite.stmt.DeleteBuilder;
public class MutateOrganizationTest extends OrganizationsFixture {
protected MutateOrganizationResponse resp;
protected MutateOrganizationRequest rqst;
protected MutateOrganization mutate;
// NB: The @Before methods of superclasses will be run before those of the current class.
// No other ordering is defined.
@Before
public void mutateSetup() {
rqst = new MutateOrganizationRequest();
rqst.promotedMemberEncryptedKeys = Maps.newHashMap();
rqst.orgId = org.getId();
rqst.newMemberGroupKeys = Maps.newHashMap();
mutate = new MutateOrganization(managerFactory, keyFactory);
}
private void expectException(DBIdentity identity, String substr) throws IOException, SQLException {
try {
getMutateResponse(identity);
fail("expected exception");
} catch (MitroServletException expected) {
if (null != substr) {
assertThat(expected.getMessage().toLowerCase(), containsString(substr.toLowerCase()));
}
}
}
private void getMutateResponse(DBIdentity identity) throws MitroServletException, IOException, SQLException {
resp = (MutateOrganizationResponse) mutate.processCommand(
new MitroRequestContext(identity, gson.toJson(rqst), manager, null));
}
@Test
public void testAdminRemovalFailures() throws SQLException, MitroServletException, IOException {
// bad admin removal requests
// try to remove a non-existent user
rqst.adminsToDemote = ImmutableList.of("unknown_user@example.com");
expectException(testIdentity, "are not admins and could not be deleted");
// try to remove a non-existent user and a real one
rqst.adminsToDemote = ImmutableList.of("unknown_user@example.com", testIdentity.getName());
expectException(testIdentity, "are not admins and could not be deleted");
// try to remove a member who isn't an admin
rqst.adminsToDemote = ImmutableList.of(testIdentity2.getName());
expectException(testIdentity, "are not admins and could not be deleted");
// try to remove a member who isn't an admin
rqst.adminsToDemote = ImmutableList.of("unkonwn", testIdentity2.getName());
expectException(testIdentity, "are not admins and could not be deleted");
}
@Test
public void testMemberRemovalFailures() throws SQLException, MitroServletException, IOException {
// bad member removal requests
// try to remove a non-existent user
rqst.membersToRemove = ImmutableList.of("unknown_user@example.com");
expectException(testIdentity, "Invalid members to remove");
// try to remove a non-existent user and a real one
rqst.membersToRemove = ImmutableList.of("unknown_user@example.com", testIdentity2.getName());
expectException(testIdentity, "Invalid members to remove");
// try to remove a member is an admin without removing him as an admin
rqst.membersToRemove = ImmutableList.of(testIdentity.getName());
expectException(testIdentity, "Cannot remove members who are admins");
rqst.membersToRemove = ImmutableList.of(testIdentity.getName(), testIdentity2.getName());
expectException(testIdentity, "Cannot remove members who are admins");
rqst.membersToRemove = ImmutableList.of("unknown", testIdentity.getName());
expectException(testIdentity, "Invalid members to remove");
}
@Test
public void removeAndPromoteMember() throws IOException, SQLException {
// remove a member and promote them at the same time
DBIdentity member = members.iterator().next();
rqst.membersToRemove = ImmutableList.of(member.getName());
rqst.promotedMemberEncryptedKeys.put(member.getName(), "org key encrypted for identity");
expectException(testIdentity, "Cannot add admins without them being members");
}
@Test
public void testUnprivilegedRemovalFailures() throws SQLException, MitroServletException, IOException {
// bad member removal requests
// try to remove a non-existent user
rqst.membersToRemove = ImmutableList.of(testIdentity2.getName());
expectException(testIdentity2, "no access");
rqst.membersToRemove = ImmutableList.of("moo");
expectException(testIdentity2, "no access");
}
@Test
public void testRemoveAllAdminsFailure() throws SQLException, MitroServletException, IOException {
rqst.adminsToDemote = Lists.newArrayList();
for (DBIdentity a : admins) {
rqst.adminsToDemote.add(a.getName());
}
// must be user visible
try {
getMutateResponse(testIdentity);
fail("expected exception");
} catch (UserVisibleException expected) {
assertThat(expected.getUserVisibleMessage(), containsString("all admins"));
}
}
@Test
public void testAddAdminButNotUserFailure() throws SQLException, MitroServletException, IOException {
rqst.promotedMemberEncryptedKeys.put(((DBIdentity)outsiders.toArray()[0]).getName(), "org key for admin");
expectException(testIdentity, "cannot add admins without");
}
@Test
public void testDemoteAdmins() throws SQLException, MitroServletException, IOException {
rqst.adminsToDemote = ImmutableList.of(testIdentity.getName());
getMutateResponse(testIdentity);
admins.remove(testIdentity);
// confusingly admins and members are exclusive -- they have the same meaning as the JS code.
members.add(testIdentity);
assertTrue(members.contains(testIdentity));
checkAll();
}
@Test
public void testPromoteUsers() throws SQLException, MitroServletException, IOException {
DBIdentity member = members.iterator().next();
rqst.promotedMemberEncryptedKeys.put(member.getName(), "org key for admin");
getMutateResponse(testIdentity);
admins.add(member);
checkAll();
}
@Test
public void testAddMember() throws SQLException, MitroServletException, IOException {
DBIdentity newMember = outsiders.iterator().next();
addNewMemberToRequest(newMember.getName());
getMutateResponse(testIdentity);
members.add(newMember);
checkAll();
}
private void addNewMemberToRequest(String emailAddress) {
PrivateGroupKeys memberKeys = new PrivateGroupKeys();
memberKeys.publicKey = "pub key";
memberKeys.keyEncryptedForUser = "userkey";
memberKeys.keyEncryptedForOrganization = "orgkey";
rqst.newMemberGroupKeys.put(emailAddress, memberKeys);
}
@Test
public void testReAddMemberFailure() throws SQLException, MitroServletException, IOException {
DBIdentity existingMember = members.iterator().next();
addNewMemberToRequest(existingMember.getName());
expectException(testIdentity, "duplicate members");
}
@Test
public void testReAddAdminFailure() throws SQLException, MitroServletException, IOException {
DBIdentity existingAdmin = admins.iterator().next();
rqst.promotedMemberEncryptedKeys.put(existingAdmin.getName(), "org key for admin");
expectException(testIdentity, "duplicate admins");
}
@Test
public void testAddMemberAndAdmin() throws SQLException, MitroServletException, IOException {
DBIdentity newMemberAndAdmin = outsiders.iterator().next();
addNewMemberToRequest(newMemberAndAdmin.getName());
rqst.promotedMemberEncryptedKeys.put(newMemberAndAdmin.getName(), "org key for admin");
getMutateResponse(testIdentity);
admins.add(newMemberAndAdmin);
members.add(newMemberAndAdmin);
checkAll();
}
@Test
public void testMemberWithOtherGroupRemoval() throws SQLException, MitroServletException, IOException, CyclicGroupError {
DBGroup namedOrgGroup = createGroupContainingIdentity(testIdentity);
DBAcl acl = addToGroup(testIdentity2, namedOrgGroup, DBAcl.AccessLevelType.ADMIN);
addOrgToGroup(manager, org, namedOrgGroup, DBAcl.AccessLevelType.ADMIN);
rqst.adminsToDemote = Lists.newArrayList();
rqst.membersToRemove = Lists.newArrayList();
rqst.adminsToDemote.add(testIdentity.getName());
rqst.membersToRemove.add(testIdentity.getName());
getMutateResponse(testIdentity);
// the named group must not have been deleted
boolean found = false;
for (DBGroup g : org.getAllOrgGroups(manager)) {
if (g.getId() == namedOrgGroup.getId()) {
found = true;
break;
}
}
assertTrue(found);
// the user must not have access to the group
namedOrgGroup = manager.groupDao.queryForId(namedOrgGroup.getId());
for (DBAcl a : namedOrgGroup.getAcls()) {
if (a.getMemberIdentityIdAsInteger() != null) {
assertFalse(a.getMemberIdentityIdAsInteger().intValue() == testIdentity.getId());
}
}
admins.remove(testIdentity);
members.remove(testIdentity);
checkAll();
}
@Test
public void testMemberRemovePrivateGroup() throws SQLException, MitroServletException, IOException, CyclicGroupError {
DBIdentity member = members.iterator().next();
// find this member's private group
DBGroup privateMemberGroup = getPrivateOrgGroup(org, member);
assertEquals(2, privateMemberGroup.getAcls().size());
// add a secret to this member's private group
DBServerVisibleSecret secret = createSecret(privateMemberGroup, "client", "critical", org);
// remove member from the organization
rqst.membersToRemove = Lists.newArrayList(member.getName());
getMutateResponse(testIdentity);
// member's private group no longer exists
assertNull(manager.groupDao.queryForId(privateMemberGroup.getId()));
// the secret exists but only has 1 group secret (org membership for orphaned secret)
secret = manager.svsDao.queryForId(secret.getId());
assertEquals(1, secret.getGroupSecrets().size());
}
@Test
public void emptyOrgMemberRemove() throws SQLException, MitroServletException, IOException, CyclicGroupError {
// remove all secrets from the organization
DeleteBuilder<DBGroupSecret, Integer> deleteBuilder = manager.groupSecretDao.deleteBuilder();
deleteBuilder.where().in(DBGroupSecret.SVS_ID_NAME, orgSecret.getId(), orphanedOrgSecret.getId());
assertEquals(4, deleteBuilder.delete());
// remove member from an "empty" organization
// had a bug where we attempted an IN query on an empty list
rqst.membersToRemove = new ArrayList<>();
rqst.membersToRemove.add(members.iterator().next().getName());
getMutateResponse(testIdentity);
}
@SuppressWarnings("deprecation")
@Test
public void testMemberRemoveAccess() throws SQLException, MitroServletException, IOException, CyclicGroupError {
Iterator<DBIdentity> it = members.iterator();
DBIdentity member1 = it.next();
DBIdentity member2 = it.next();
// namedNonOrgGroup contains member and testIdentity2
DBGroup namedNonOrgGroup = createGroupContainingIdentity(member1);
addToGroup(testIdentity2, namedNonOrgGroup, DBAcl.AccessLevelType.ADMIN);
// namedOrgGroup contains member1 and member2
DBGroup namedOrgGroup = createGroupContainingIdentity(member1);
addToGroup(member2, namedOrgGroup, DBAcl.AccessLevelType.ADMIN);
addOrgToGroup(manager, org, namedOrgGroup, DBAcl.AccessLevelType.ADMIN);
// Add org secret to both groups
addSecretToGroup(orgSecret, namedOrgGroup, "client", "critical");
addSecretToGroup(orgSecret, namedNonOrgGroup, "client", "critical");
// all three have access to the secret
AuthenticatedDB member1Db = AuthenticatedDB.deprecatedNew(manager, member1);
AuthenticatedDB member2Db = AuthenticatedDB.deprecatedNew(manager, member2);
AuthenticatedDB testIdentity2Db = AuthenticatedDB.deprecatedNew(manager, testIdentity2);
assertEquals(orgSecret.getId(), member1Db.getSecretAsUser(orgSecret.getId()).getId());
assertEquals(orgSecret.getId(), member2Db.getSecretAsUser(orgSecret.getId()).getId());
assertEquals(orgSecret.getId(), testIdentity2Db.getSecretAsUser(orgSecret.getId()).getId());
// remove member1 from the organization
rqst.adminsToDemote = Lists.newArrayList();
rqst.membersToRemove = Lists.newArrayList();
rqst.adminsToDemote = Lists.newArrayList();
rqst.membersToRemove.add(member1.getName());
getMutateResponse(testIdentity);
// member1 no longer has access. member2 has access via namedOrgGroup. testIdentity2 loses access
assertNull(member1Db.getSecretAsUser(orgSecret.getId()));
assertEquals(orgSecret.getId(), member2Db.getSecretAsUser(orgSecret.getId()).getId());
assertNull(testIdentity2Db.getSecretAsUser(orgSecret.getId()));
}
private void checkAdmins() throws SQLException {
Collection<DBAcl> acls = org.getAcls();
Set<DBIdentity> actualAdmins = Sets.newHashSet();
assertEquals(admins.size(), acls.size());
for (DBAcl a : acls) {
DBIdentity i = a.loadMemberIdentity(manager.identityDao);
actualAdmins.add(i);
}
assertTrue((Sets.symmetricDifference(actualAdmins, admins)).isEmpty());
}
private void checkMembers() throws SQLException {
Set<Integer> actualMemberIds = MutateOrganization.getMemberIdsAndPrivateGroupIdsForOrg(manager, org).keySet();
Set<Integer> expectedMemberIds = Sets.newHashSet();
for (DBIdentity i : members) {
expectedMemberIds.add(i.getId());
}
for (DBIdentity i : admins) {
expectedMemberIds.add(i.getId());
}
assertTrue((Sets.symmetricDifference(actualMemberIds, expectedMemberIds)).isEmpty());
}
public void checkAll() throws SQLException {
checkAdmins();
checkMembers();
}
}