/*******************************************************************************
* 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.accesscontrol;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import co.mitro.core.exceptions.InvalidRequestException;
import co.mitro.core.exceptions.MitroServletException;
import co.mitro.core.exceptions.PermissionException;
import co.mitro.core.server.data.DBAcl;
import co.mitro.core.server.data.DBAcl.AccessLevelType;
import co.mitro.core.server.data.DBAcl.CyclicGroupError;
import co.mitro.core.server.data.DBGroup;
import co.mitro.core.server.data.DBIdentity;
import co.mitro.core.server.data.DBServerVisibleSecret;
import co.mitro.core.servlets.MemoryDBFixture;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
public class AuthenticatedDBTest extends MemoryDBFixture {
private AuthenticatedDB testIdentityDb;
private AuthenticatedDB testIdentity2Db;
private AuthenticatedDB testIdentity3Db;
private DBGroup group;
private List<DBAcl> acls;
private DBGroup org;
private DBIdentity testIdentity3;
@SuppressWarnings("deprecation")
@Before
public void setUp() throws CyclicGroupError, IOException, SQLException, MitroServletException {
testIdentityDb = AuthenticatedDB.deprecatedNew(manager, testIdentity);
testIdentity2Db = AuthenticatedDB.deprecatedNew(manager, testIdentity2);
testIdentity3 = createIdentity("user3@example.com", null);
testIdentity3Db = AuthenticatedDB.deprecatedNew(manager, testIdentity3);
// create an unsaved group, with a single ACL giving access to testIdentity
group = new DBGroup();
group.setName("group");
group.setPublicKeyString("public key");
acls = new ArrayList<>();
DBAcl acl = makeAcl(testIdentity, group, AccessLevelType.ADMIN);
acls.add(acl);
// create org with testIdentity as an admin, and testIdentity2 as a member
org = this.createOrganization(
testIdentity, "org", Lists.newArrayList(testIdentity), Lists.newArrayList(testIdentity2));
}
@Test
public void getOrganizationAsAdminSuccess() throws MitroServletException {
assertEquals(testIdentityDb.getOrganizationAsAdmin(org.getId()).getId(), org.getId());
}
@Test
public void getOrganizationAsAdminFail() {
// not an admin
try {
testIdentity2Db.getOrganizationAsAdmin(org.getId());
fail("exception expected");
} catch (MitroServletException expected) {
assertThat(expected.getMessage(), containsString("no access"));
}
// not part of the organization
try {
testIdentity3Db.getOrganizationAsAdmin(org.getId());
fail("exception expected");
} catch (MitroServletException expected) {
assertThat(expected.getMessage(), containsString("no access"));
}
}
@Test
public void getOrganizationAsMemberSuccess() throws MitroServletException, SQLException {
assertEquals(testIdentityDb.getOrganizationAsMember(org.getId()).getId(), org.getId());
assertEquals(testIdentity2Db.getOrganizationAsMember(org.getId()).getId(), org.getId());
}
@Test
public void getOrganizationAsMemberFail() throws SQLException {
// not part of the organization
try {
testIdentity3Db.getOrganizationAsMember(org.getId());
fail("exception expected");
} catch (MitroServletException expected) {
assertThat(expected.getMessage(), containsString("no access"));
}
}
@Test
public void getGroupOrOrgSuccess() throws MitroServletException, SQLException {
assertEquals(testIdentityDb.getGroupOrOrg(org.getId()).getId(), org.getId());
assertEquals(testIdentityDb.getGroupOrOrg(testGroup.getId()).getId(), testGroup.getId());
assertEquals(testIdentity2Db.getGroupOrOrg(org.getId()).getId(), org.getId());
}
@Test
public void getGroupAsUserOrOrgAdmin() throws SQLException, MitroServletException, CyclicGroupError {
assertEquals(null, testIdentity3Db.getGroupAsUserOrOrgAdmin(testGroup.getId()));
assertEquals(testGroup.getId(), testIdentityDb.getGroupAsUserOrOrgAdmin(testGroup.getId()).getId());
// create a new org group.
DBGroup newGroup = createGroupContainingIdentity(testIdentity2);
addOrgToGroup(manager, org, newGroup, AccessLevelType.ADMIN);
// testIdentity does not have direct access to the group.
try {
testIdentityDb.getGroupOrOrg(newGroup.getId());
fail("exception expected");
} catch (MitroServletException expected) {
assertThat(expected.getMessage(), containsString("not in group"));
}
// testIdentity does have access to group indirectly.
assertEquals(newGroup.getId(), testIdentityDb.getGroupAsUserOrOrgAdmin(newGroup.getId()).getId());
// organization member can NOT remove the top level group
assertEquals(null, testIdentity2Db.getGroupAsUserOrOrgAdmin(org.getId()));
}
@Test
public void getGroupForAddSecret() throws SQLException, MitroServletException, CyclicGroupError {
// organization member can add secrets to the top level group
// this is the key difference from getGroupAsUserOrOrgAdmin
assertEquals(org.getId(), testIdentity2Db.getGroupForAddSecret(org.getId()).getId());
// verify identity2 can access a new org group (it is a member)
DBGroup identity2OrgGroup = createGroupContainingIdentity(testIdentity2);
addOrgToGroup(manager, org, identity2OrgGroup, AccessLevelType.ADMIN);
assertEquals(identity2OrgGroup.getId(), testIdentity2Db.getGroupForAddSecret(identity2OrgGroup.getId()).getId());
// identity2 can add secrets to any groups in the org (even if not a member)
// TODO: This should work, but doesn't yet. Implement this and "shared org outsiders"
DBGroup identity1OrgGroup = createGroupContainingIdentity(testIdentity);
addOrgToGroup(manager, org, identity1OrgGroup, AccessLevelType.ADMIN);
assertEquals(identity1OrgGroup.getId(), testIdentity2Db.getGroupForAddSecret(identity1OrgGroup.getId()).getId());
}
@Test
public void getGroupForAddSecretAsOutsider() throws MitroServletException, SQLException, CyclicGroupError {
// Create a group with id1 and id3
DBGroup id1PrivateGroup = getPrivateOrgGroup(org, testIdentity);
DBServerVisibleSecret secret = createSecret(id1PrivateGroup, "client", "critical", org);
DBGroup id1AndId3Group = createGroupContainingIdentity(testIdentity);
addToGroup(testIdentity3, id1AndId3Group, AccessLevelType.ADMIN);
// no access to the org: not a member
try {
testIdentity3Db.getGroupForAddSecret(org.getId());
fail("expected exception");
} catch (MitroServletException e) {
assertThat(e.getMessage(), containsString("should not be able to see group"));
}
// share the secret
addSecretToGroup(secret, id1AndId3Group, "client id3", "critical id3");
assertEquals(org, testIdentity3Db.getGroupForAddSecret(org.getId()));
}
@Test
public void getGroupForAddSecretFailures() throws SQLException, MitroServletException {
// does not exist (previously caused an NPE)
try {
testIdentity2Db.getGroupForAddSecret(-1);
fail("expected exception");
} catch (MitroServletException e) {
assertThat(e.getMessage(), containsString("should not be able to see group"));
}
}
@Test
public void getGroupOrOrgFailure() throws SQLException, PermissionException {
try {
testIdentity3Db.getGroupOrOrg(org.getId());
fail("exception expected");
} catch (MitroServletException expected) {
assertThat(expected.getMessage(), containsString("not member of org"));
}
try {
testIdentity3Db.getGroupOrOrg(testGroup.getId());
fail("exception expected");
} catch (MitroServletException expected) {
assertThat(expected.getMessage(), containsString("not in group"));
}
}
@Test
public void createAdminSuccess() throws Exception {
testIdentityDb.saveNewGroupWithAcls(group, acls);
}
@Test
public void createModifySecretsSuccess() throws Exception {
acls.get(0).setLevel(AccessLevelType.MODIFY_SECRETS_BUT_NOT_MEMBERSHIP);
testIdentityDb.saveNewGroupWithAcls(group, acls);
}
@Test
public void createGroupSuccess() throws Exception {
acls.get(0).setMemberIdentity(null);
acls.get(0).setMemberGroup(testGroup);
testIdentityDb.saveNewGroupWithAcls(group, acls);
}
@Test
public void createGroupInvalidArgs() throws Exception {
// null arguments
DBGroup originalGroup = group;
List<DBAcl> originalAcls = acls;
group = null;
acls = originalAcls;
expectNPE();
group = originalGroup;
acls = null;
expectNPE();
// null ACL
acls = Lists.newArrayList((DBAcl) null);
expectNPE();
// empty ACLs
acls = new ArrayList<>();
expectInvalidRequest("acls must not be empty");
acls = originalAcls;
// acl for a null group
acls.get(0).setGroup(null);
expectNPE();
acls.get(0).setGroup(group);
// acl is not the right level
acls.get(0).setLevel(AccessLevelType.READONLY);
expectInvalidRequest("does not have permission to modify");
acls.get(0).setLevel(AccessLevelType.ADMIN);
// Add an ACL that doesn't reference anything
DBAcl acl = makeAcl(testIdentity, group, AccessLevelType.ADMIN);
acl.setMemberIdentity(null);
acl.setMemberGroup(null);
acls.add(acl);
expectInvalidRequest("must apply to exactly one identity or group");
// Add an ACL that references both a group and identity
acl.setMemberIdentity(testIdentity2);
acl.setMemberGroup(testGroup);
expectInvalidRequest("must apply to exactly one identity or group");
// 2 ACLs specifying the same identity
acl.setMemberIdentity(testIdentity);
acl.setMemberGroup(null);
expectInvalidRequest("duplicate identity");
// 2 ACLs specifying the same groups
acls.get(0).setMemberIdentity(null);
acls.get(0).setMemberGroup(testGroup);
acls.get(1).setMemberIdentity(null);
acls.get(1).setMemberGroup(testGroup);
expectInvalidRequest("duplicate group");
}
@Test
public void createGroupMissingReferences() throws Exception {
// ACL references a user that doesn't exist
// previously we only checked users with ADMIN access; must check all users
DBAcl acl = makeAcl(testIdentity, group, AccessLevelType.READONLY);
DBIdentity unsavedIdentity = new DBIdentity();
unsavedIdentity.setId(100000);
acl.setMemberIdentity(unsavedIdentity);
acls.add(acl);
expectInvalidRequest("some identities do not exist");
acl.setLevel(AccessLevelType.ADMIN);
expectInvalidRequest("some identities do not exist");
// ACL references a group that doesn't exist
DBGroup unsavedGroup = new DBGroup();
unsavedGroup.setId(100000);
acl.setMemberIdentity(null);
acl.setMemberGroup(unsavedGroup);
expectInvalidRequest("some groups are not accessible");
// again doesn't matter about the access level
acl.setLevel(AccessLevelType.READONLY);
expectInvalidRequest("some groups are not accessible");
// testIdentity does not have access to this group
DBGroup otherGroup = createGroupContainingIdentity(testIdentity2);
manager.groupDao.create(otherGroup);
acl.setMemberIdentity(null);
acl.setMemberGroup(otherGroup);
expectInvalidRequest("some groups are not accessible");
}
@Test
public void getSecretNoOrganization() throws SQLException, CyclicGroupError {
// regular secrets, not part of an organization
DBServerVisibleSecret id1Secret = createSecret(testGroup, "client", "critical", null);
DBGroup group2 = createGroupContainingIdentity(testIdentity2);
DBServerVisibleSecret id2Secret = createSecret(group2, "client", "critical", null);
// testIdentity can't access id2Secret, but can access id1Secret
assertNull(testIdentityDb.getSecretAsUser(id2Secret.getId()));
assertNotNull(testIdentityDb.getSecretAsUser(id1Secret.getId()));
}
@Test
public void getSecretOrganization() throws SQLException, CyclicGroupError {
DBGroup id1PrivateGroup = getPrivateOrgGroup(org, testIdentity);
DBServerVisibleSecret id1Secret = createSecret(
id1PrivateGroup, "client", "critical", org);
DBGroup id2PrivateGroup = getPrivateOrgGroup(org, testIdentity2);
DBServerVisibleSecret id2Secret = createSecret(
id2PrivateGroup, "client", "critical", org);
// testIdentity can't access id2Secret, but can access id1Secret
assertNull(testIdentityDb.getSecretAsUser(id2Secret.getId()));
assertNotNull(testIdentityDb.getSecretAsUser(id1Secret.getId()));
assertNotNull(testIdentity2Db.getSecretAsUser(id2Secret.getId()));
assertNull(testIdentity2Db.getSecretAsUser(id1Secret.getId()));
}
@Test
public void getSecretAsOrgAdmin() throws IOException, SQLException, MitroServletException, CyclicGroupError {
List<DBIdentity> admins = ImmutableList.of(testIdentity3);
List<DBIdentity> members = ImmutableList.of();
DBGroup topLevelOrganization = createOrganization(
testIdentity3, "Organization", admins, members);
DBGroup g2 = createGroupContainingIdentity(testIdentity);
addOrgToGroup(manager, topLevelOrganization, g2, AccessLevelType.ADMIN);
DBServerVisibleSecret newSecret = createSecret(g2, "client", "critical", null);
assertEquals(newSecret.getId(), testIdentity3Db.getSecretAsOrgAdmin(newSecret.getId()).getId());
assertEquals(null, testIdentity2Db.getSecretAsOrgAdmin(newSecret.getId()));
}
private void expectInvalidRequest(String expectedSubstring)
throws SQLException {
expectException(InvalidRequestException.class, expectedSubstring);
}
private void expectNPE() {
expectException(NullPointerException.class, null);
}
private void expectException(Class<? extends Exception> expectedType, String expectedSubstring) {
try {
testIdentityDb.saveNewGroupWithAcls(group, acls);
fail("expected exception");
} catch (Exception e) {
assertThat(e, instanceOf(expectedType));
if (expectedSubstring != null) {
assertThat(e.getMessage(), containsString(expectedSubstring));
}
}
}
}