/*******************************************************************************
* 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 co.mitro.test.Assert.assertExceptionMessage;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
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.List;
import java.util.concurrent.Callable;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import co.mitro.core.crypto.KeyInterfaces.PrivateKeyInterface;
import co.mitro.core.exceptions.InvalidRequestException;
import co.mitro.core.exceptions.MitroServletException;
import co.mitro.core.exceptions.UserExistsException;
import co.mitro.core.server.data.DBAcl;
import co.mitro.core.server.data.DBEmailQueue;
import co.mitro.core.server.data.DBGroup;
import co.mitro.core.server.data.DBIdentity;
import co.mitro.core.server.data.RPC;
import co.mitro.core.servlets.MitroServlet.MitroRequestContext;
import co.mitro.test.MockHttpServletRequest;
import co.mitro.test.MockHttpServletResponse;
import com.google.common.collect.Lists;
public class AddIdentityTest extends MemoryDBFixture {
private AddIdentity servlet;
private RPC.AddIdentityRequest request;
private int counter = 42;
private String getUniqueEmail() {
String out = "user" + counter + "@example.com";
counter += 1;
return out;
}
@Before
public void setUp() {
// Required for testing signature verification; doPost creates the Manager directly
replaceDefaultManagerDbForTest();
servlet = new AddIdentity(managerFactory, keyFactory);
request = new RPC.AddIdentityRequest();
request.encryptedPrivateKey = "private key";
request.publicKey = "pub key";
request.deviceId = DEVICE_ID;
}
@Test(expected=UserExistsException.class)
public void testAlreadyExists() throws IOException, SQLException, MitroServletException {
// this identity already exists: expected to fail
request.userId = testIdentity.getName();
servlet.processCommand(new MitroRequestContext(null, gson.toJson(request), manager, null));
}
@Test
public void testSuccess() throws IOException, SQLException, MitroServletException {
request.userId = getUniqueEmail();
RPC.AddIdentityResponse r = runAndVerifyAddIdentity();
assertEquals(null, r.privateGroupId);
}
@Test
public void testAutoGroupCreation() throws IOException, SQLException, MitroServletException {
request.userId = getUniqueEmail();
request.groupKeyEncryptedForMe = "groupKeyForMe";
request.groupPublicKey = "group public key";
RPC.AddIdentityResponse r = runAndVerifyAddIdentity();
assertNotNull(r.privateGroupId);
DBGroup group = manager.groupDao.queryForId(r.privateGroupId);
assertEquals("", group.getName());
List<DBAcl> acl = Lists.newArrayList(group.getAcls());
assertEquals(request.groupPublicKey, group.getPublicKeyString());
assertEquals(false, group.isAutoDelete());
assertEquals(1, acl.size());
assertEquals(request.groupKeyEncryptedForMe, acl.get(0).getGroupKeyEncryptedForMe());
assertEquals(DBAcl.AccessLevelType.ADMIN, acl.get(0).getLevel());
DBIdentity i = acl.get(0).getMemberIdentityId();
manager.identityDao.refresh(i);
assertEquals(request.userId, i.getName());
}
@Test
public void testBadEmails() throws Exception {
Callable<Void> tryAdd = new Callable<Void>() {
@Override
public Void call() throws Exception {
runAndVerifyAddIdentity();
return null;
}
};
request.userId = "someone@example.com ";
assertExceptionMessage("not a valid email address", InvalidRequestException.class, tryAdd);
}
@Test
@Ignore
public void testEmailsAreNormalized() throws Exception {
// TODO: Normalize email addresses throughout the entire system!
request.userId = "Capitalized@Example.com";
servlet.processCommand(new MitroRequestContext(null, gson.toJson(request), manager, null));
// user is created with the normalized address
assertNull(DBIdentity.getIdentityForUserName(manager, request.userId));
String normalized = Util.normalizeEmailAddress(request.userId);
DBIdentity i = DBIdentity.getIdentityForUserName(manager, normalized);
assertEquals(normalized, i.getName());
}
protected RPC.AddIdentityResponse runAndVerifyAddIdentity()
throws IOException, SQLException, MitroServletException {
RPC.MitroRPC response = servlet.processCommand(new MitroRequestContext(null, gson.toJson(request), manager, null));
RPC.AddIdentityResponse r = (RPC.AddIdentityResponse) response;
assertFalse(r.verified);
DBIdentity i = DBIdentity.getIdentityForUserName(manager, request.userId);
assertFalse(i.isVerified());
// Check the validation message
List<DBEmailQueue> emails = manager.emailDao.queryForAll();
assertEquals(1, emails.size());
assertEquals(DBEmailQueue.Type.ADDRESS_VERIFICATION, emails.get(0).getType());
assertEquals(request.userId, emails.get(0).getArguments()[0]);
assertEquals(i.getVerificationUid(), emails.get(0).getArguments()[1]);
return r;
}
@Test
public void testSignatureVerification() throws Exception {
// Failed request: signature does not verify
request.userId = getUniqueEmail();
RPC.SignedRequest r = new RPC.SignedRequest();
r.identity = request.userId;
r.request = gson.toJson(request);
MockHttpServletRequest httpRequest = new MockHttpServletRequest();
httpRequest.setRequestBody(gson.toJson(r).getBytes("UTF-8"));
MockHttpServletResponse httpResponse = new MockHttpServletResponse();
servlet.doPost(httpRequest, httpResponse);
RPC.MitroException exception =
gson.fromJson(httpResponse.getOutput(), RPC.MitroException.class);
// // DO NOT REMOVE! old versions of extensions expect reasons to have size == 1.
assertEquals(1, exception.reasons.size());
assertThat(exception.userVisibleError, containsString("Error"));
// successful request: valid signature
PrivateKeyInterface key = keyFactory.generate();
request.publicKey = key.exportPublicKey().toString();
r.request = gson.toJson(request);
r.signature = key.sign(r.request);
httpRequest.setRequestBody(gson.toJson(r).getBytes("UTF-8"));
httpResponse = new MockHttpServletResponse();
servlet.doPost(httpRequest, httpResponse);
RPC.AddIdentityResponse r2 =
gson.fromJson(httpResponse.getOutput(), RPC.AddIdentityResponse.class);
assertFalse(r2.verified);
DBIdentity i = DBIdentity.getIdentityForUserName(manager, request.userId);
assertNotNull(i);
// verify the login token
RPC.LoginToken lt = gson.fromJson(r2.unsignedLoginToken,
RPC.LoginToken.class);
assertEquals(lt.email, i.getName());
assertEquals(lt.email, request.userId);
// make sure this token can be used to log in again.
RPC.GetMyPrivateKeyRequest pvtRequest = new RPC.GetMyPrivateKeyRequest();
// 1. Test null signature
pvtRequest.loginToken = r2.unsignedLoginToken;
pvtRequest.userId = request.userId;
processLoginFailure(pvtRequest);
// 1. Test bad signature
pvtRequest.loginToken = r2.unsignedLoginToken;
pvtRequest.loginTokenSignature = key.sign(r2.unsignedLoginToken) + "bad";
pvtRequest.userId = request.userId;
processLoginFailure(pvtRequest);
// 1. Test bad token
pvtRequest.loginToken = r2.unsignedLoginToken;
pvtRequest.loginToken = pvtRequest.loginToken.replace(request.userId, "adifferentuser@example.com");
pvtRequest.loginTokenSignature = key.sign(pvtRequest.loginToken);
pvtRequest.userId = request.userId;
processLoginFailure(pvtRequest);
// finally, do this correctly.
pvtRequest.loginToken = r2.unsignedLoginToken;
pvtRequest.loginTokenSignature = key.sign(pvtRequest.loginToken);
pvtRequest.userId = request.userId;
pvtRequest.deviceId = DEVICE_ID;
GetMyPrivateKey pvtServlet = new GetMyPrivateKey(managerFactory, keyFactory);
pvtServlet.processCommand(new MitroRequestContext(null, gson.toJson(pvtRequest), manager, null));
}
protected void processLoginFailure(RPC.GetMyPrivateKeyRequest pvtRequest)
throws IOException, SQLException {
try {
GetMyPrivateKey pvtServlet = new GetMyPrivateKey(managerFactory, keyFactory);
pvtServlet.processCommand(new MitroRequestContext(null, gson.toJson(pvtRequest), manager, null));
fail("this should have thrown an exception");
} catch (MitroServletException ignored) {
;
}
}
}