/*******************************************************************************
* 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.junit.Assert.fail;
import static org.junit.Assert.assertEquals;
import java.io.IOException;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Before;
import co.mitro.analysis.AuditLogProcessor.ActionType;
import co.mitro.core.crypto.CrappyKeyFactory;
import co.mitro.core.crypto.KeyInterfaces;
import co.mitro.core.crypto.KeyInterfaces.CryptoError;
import co.mitro.core.crypto.KeyInterfaces.PrivateKeyInterface;
import co.mitro.core.exceptions.MitroServletException;
import co.mitro.core.server.Manager;
import co.mitro.core.server.ManagerFactory;
import co.mitro.core.server.ManagerFactory.ConnectionMode;
import co.mitro.core.server.SecretsBundle;
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.DBGroupSecret;
import co.mitro.core.server.data.DBIdentity;
import co.mitro.core.server.data.DBProcessedAudit;
import co.mitro.core.server.data.DBServerVisibleSecret;
import co.mitro.core.server.data.RPC.CreateOrganizationRequest;
import co.mitro.core.server.data.RPC.CreateOrganizationRequest.PrivateGroupKeys;
import co.mitro.core.server.data.RPC.CreateOrganizationResponse;
import co.mitro.core.servlets.MitroServlet.MitroRequestContext;
import co.mitro.twofactor.TwoFactorSigningService;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.j256.ormlite.jdbc.JdbcConnectionSource;
import com.j256.ormlite.stmt.QueryBuilder;
import com.j256.ormlite.support.DatabaseConnection;
/** Creates a Manager that uses H2, the in-memory DB. */
public class MemoryDBFixture {
protected static final KeyInterfaces.KeyFactory keyFactory = new CrappyKeyFactory();
// DATABASE_TO_UPPER must be disabled in order to deal with mixed case table names (ormlite bug probably)
// (add TRACE_LEVEL_SYSTEM_OUT=2 for query logging).
private final static String DATABASE_URL = "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=FALSE";
protected static final String DEVICE_ID = "DEVICE1";
protected static final Gson gson = new Gson();
protected static final String ENCRYPTED_GROUP_KEY = "encrypted group key";
protected ManagerFactory managerFactory;
protected Manager manager;
/** A member of testGroup when the test starts. */
protected DBIdentity testIdentity;
protected KeyInterfaces.PrivateKeyInterface testIdentityKey;
/** Not a member of any groups when the test starts. */
protected DBIdentity testIdentity2;
protected String testIdentityLoginToken;
/** Contains a single member, testIdentity, when the test starts. */
protected DBGroup testGroup;
protected String testIdentityLoginTokenSignature;
protected Map<Integer, PrivateKeyInterface> groupToPrivateKeyMap;
@Before
public void memorySetUp() throws SQLException, CyclicGroupError, CryptoError, MitroServletException {
// Create a fake SecretsBundle
TwoFactorSigningService.initialize(SecretsBundle.generateForTest());
groupToPrivateKeyMap = Maps.newHashMap();
JdbcConnectionSource connectionSource = new JdbcConnectionSource(DATABASE_URL);
connectionSource.getReadWriteConnection().executeStatement(
"DROP ALL OBJECTS", DatabaseConnection.DEFAULT_RESULT_FLAGS);
managerFactory = new ManagerFactory(DATABASE_URL, new Manager.Pool(),
ManagerFactory.IDLE_TXN_POLL_SECONDS, TimeUnit.SECONDS, ConnectionMode.READ_WRITE);
manager = managerFactory.newManager();
testIdentityKey = keyFactory.generate();
testIdentity = createIdentity("user@example.com", testIdentityKey);
testIdentityLoginToken = GetMyPrivateKey.makeLoginTokenString(testIdentity, null, null);
testIdentityLoginTokenSignature = testIdentityKey.sign(testIdentityLoginToken);
testIdentity2 = createIdentity("user2@example.com", null);
testGroup = createGroupContainingIdentity(testIdentity);
manager.commitTransaction();
// remove the audit log that commit writes so that tests start with an empty log
connectionSource.getReadWriteConnection().executeStatement(
"DELETE FROM audit;", DatabaseConnection.DEFAULT_RESULT_FLAGS);
connectionSource.getReadWriteConnection().commit(null);
}
protected DBIdentity createIdentity(String name) throws SQLException, MitroServletException {
return createIdentity(name, null);
}
protected void authorizeIdentityForDefaultDevice(DBIdentity thisIdentity) throws SQLException, MitroServletException {
// authorize this identity for the common device id.
authorizeIdentityForDevice(thisIdentity, DEVICE_ID);
}
protected void authorizeIdentityForDevice(DBIdentity thisIdentity, String deviceId) throws SQLException, MitroServletException {
// authorize this identity for the common device id.
GetMyDeviceKey.maybeGetOrCreateDeviceKey(manager, thisIdentity, deviceId, false, null);
}
protected DBGroup createOrganization(DBIdentity actor, String name, Iterable<DBIdentity> admins, Iterable<DBIdentity> members) throws IOException, SQLException, MitroServletException {
CreateOrganization servlet = new CreateOrganization(managerFactory, keyFactory);
CreateOrganizationRequest request = new CreateOrganizationRequest();
request.name = name;
request.adminEncryptedKeys = Maps.newHashMap();
request.memberGroupKeys = Maps.newHashMap();
for (DBIdentity admin : admins) {
request.adminEncryptedKeys.put(admin.getName(), "key for " + admin.getName());
addMemberKeys(request, admin);
}
for (DBIdentity user : members) {
addMemberKeys(request, user);
}
PrivateKeyInterface key;
try {
key = keyFactory.generate();
request.publicKey = key.exportPublicKey().toString();
} catch(CryptoError e) {
throw new MitroServletException(e);
}
CreateOrganizationResponse out = (CreateOrganizationResponse) servlet.processCommand(new MitroRequestContext(actor, gson.toJson(request), manager, null));
groupToPrivateKeyMap.put(out.organizationGroupId, key);
return manager.groupDao.queryForId(out.organizationGroupId);
}
private static void addMemberKeys(CreateOrganizationRequest request,
DBIdentity user) {
PrivateGroupKeys pgk = new PrivateGroupKeys();
pgk.publicKey = "public key for " + user.getName();
pgk.keyEncryptedForUser = "key encrypted for user:" + user.getName();
pgk.keyEncryptedForOrganization = "key encrypted for org:" + user.getName();
request.memberGroupKeys.put(user.getName(), pgk);
}
protected DBIdentity createIdentity(String name, PrivateKeyInterface key) throws SQLException, MitroServletException {
DBIdentity identity = new DBIdentity();
identity.setName(name);
if (null == key) {
identity.setPublicKeyString("identity public key " + identity.getName());
} else {
try {
identity.setPublicKeyString(key.exportPublicKey().toString());
} catch (CryptoError e) {
// TODO Auto-generated catch block
throw new RuntimeException(e);
}
}
DBIdentity.createUserInDb(manager, identity);
// authorize this identity for the main device id
this.authorizeIdentityForDefaultDevice(identity);
return identity;
}
@After
public void memoryTearDown() {
if (manager != null && manager.getConnectionSource() != null) {
manager.getConnectionSource().closeQuietly();
}
}
protected DBGroup createGroupContainingIdentity(DBIdentity identity)
throws SQLException, CyclicGroupError {
return createGroupContainingIdentity(manager, identity);
}
protected DBAcl addToGroup(DBIdentity identity, DBGroup group, AccessLevelType accessLevel)
throws CyclicGroupError, SQLException {
return addToGroup(manager, identity, group, accessLevel);
}
public DBGroup createGroupContainingIdentity(Manager manager, DBIdentity identity)
throws SQLException, CyclicGroupError {
DBGroup group = new DBGroup();
group.setName("hello");
group.setPublicKeyString("temp");
manager.groupDao.create(group);
setKeyForGroup(group);
manager.groupDao.update(group);
addToGroup(manager, identity, group, DBAcl.AccessLevelType.ADMIN);
manager.groupDao.refresh(group);
return group;
}
// TODO: this exists only for MitroServletTest. This needs to be refactored.
public static DBGroup createGroupContainingIdentityStatic(Manager manager, DBIdentity identity)
throws SQLException, CyclicGroupError {
DBGroup group = new DBGroup();
group.setName("hello");
group.setPublicKeyString("public key strings");
manager.groupDao.create(group);
addToGroup(manager, identity, group, DBAcl.AccessLevelType.ADMIN);
manager.groupDao.refresh(group);
return group;
}
private void setKeyForGroup(DBGroup group) {
try {
PrivateKeyInterface key = keyFactory.generate();
group.setPublicKeyString(key.exportPublicKey().toString());
groupToPrivateKeyMap.put(group.getId(), key);
} catch (CryptoError e) {
throw new RuntimeException(e);
}
}
public static DBAcl addToGroup(Manager manager, DBIdentity identity, DBGroup group,
AccessLevelType accessLevel) throws CyclicGroupError, SQLException {
DBAcl acl = makeAcl(identity, group, accessLevel);
manager.aclDao.create(acl);
return acl;
}
public static DBAcl makeAcl(DBGroup memberGroup, DBGroup group, AccessLevelType accessLevel)
throws CyclicGroupError {
DBAcl acl = makeAcl(group, accessLevel);
acl.setMemberGroup(memberGroup);
return acl;
}
public static DBAcl makeAcl(DBIdentity identity, DBGroup group, AccessLevelType accessLevel)
throws CyclicGroupError {
DBAcl acl = makeAcl(group, accessLevel);
acl.setMemberIdentity(identity);
return acl;
}
public static DBAcl makeAcl(DBGroup group, AccessLevelType accessLevel)
throws CyclicGroupError {
DBAcl acl = new DBAcl();
acl.setGroup(group);
acl.setGroupKeyEncryptedForMe(ENCRYPTED_GROUP_KEY);
acl.setLevel(accessLevel);
return acl;
}
/** Creates a new secret in group, sharing it with an org.
* @param org org with which to share or null*/
protected DBServerVisibleSecret createSecret(DBGroup group, String clientVisibleEncrypted,
String criticalDataEncrypted, DBGroup org)
throws SQLException {
DBServerVisibleSecret secret = new DBServerVisibleSecret();
manager.svsDao.create(secret);
addSecretToGroup(secret, group, clientVisibleEncrypted, criticalDataEncrypted);
if (org != null) {
assert(org.isTopLevelOrganization());
addSecretToGroup(secret, org, clientVisibleEncrypted, criticalDataEncrypted);
}
return secret;
}
protected DBGroupSecret addSecretToGroup(DBServerVisibleSecret secret, DBGroup group,
String clientVisibleEncrypted, String criticalDataEncrypted)
throws SQLException {
DBGroupSecret groupSecret = new DBGroupSecret();
groupSecret.setServerVisibleSecret(secret);
groupSecret.setGroup(group);
groupSecret.setClientVisibleDataEncrypted(clientVisibleEncrypted);
groupSecret.setCriticalDataEncrypted(criticalDataEncrypted);
manager.groupSecretDao.create(groupSecret);
return groupSecret;
}
// TODO: Remove after we inject this dependency everywhere
protected void replaceDefaultManagerDbForTest() {
ManagerFactory.setDatabaseUrlForTest(DATABASE_URL);
}
protected DBGroup getPrivateOrgGroup(DBGroup organization, DBIdentity member)
throws SQLException {
// get all of member's groups
QueryBuilder<DBAcl, Integer> directMemberQuery =
manager.aclDao.queryBuilder().selectColumns(DBAcl.GROUP_ID_FIELD_NAME);
directMemberQuery.where().eq(DBAcl.MEMBER_IDENTITY_FIELD_NAME, member.getId());
// from those groups, get the groups that are also part of organization
QueryBuilder<DBAcl, Integer> query = manager.aclDao.queryBuilder();
query.where().in(DBAcl.GROUP_ID_FIELD_NAME, directMemberQuery).and()
.eq(DBAcl.MEMBER_GROUP_FIELD_NAME, organization.getId());
// find the private group from the list organization groups this user belongs to
for (DBAcl acl : query.query()) {
DBGroup group = acl.loadGroup(manager.groupDao);
if (group.isPrivateUserGroup()) {
return group;
}
}
fail("did not find member's private organization group");
return null;
}
public static DBAcl addOrgToGroup(Manager manager, DBGroup org, DBGroup group,
AccessLevelType accessLevel) throws CyclicGroupError, SQLException {
DBAcl acl = makeAcl(org, group, accessLevel);
manager.aclDao.create(acl);
return acl;
}
protected boolean hasAudit(Manager manager, ActionType action,
DBIdentity actor, DBIdentity object, int secretId) throws SQLException {
String txnId = manager.getTransactionId();
manager.commitTransaction();
List<DBProcessedAudit> audits = manager.processedAuditDao.queryForFieldValues(
ImmutableMap.of(
DBProcessedAudit.ACTION_FIELD_NAME, action,
DBProcessedAudit.TRANSACTION_ID_FIELD_NAME, (Object)txnId,
DBProcessedAudit.ACTOR_FIELD_NAME, actor,
DBProcessedAudit.AFFECTED_SECRET_FIELD_NAME, secretId,
DBProcessedAudit.AFFECTED_USER_FIELD_NAME, object
));
if (audits.isEmpty()) {
return false;
}
for (DBProcessedAudit audit : audits) {
if (actor != null) {
assertEquals(actor.getName(), audit.getActorName());
}
if (object != null) {
assertEquals(object.getName(), audit.getAffectedUserName());
}
}
return true;
}
}