Mercurial > hg > release > thermostat-0.5
changeset 831:6cd46e3563ce
Implement authentication for cmd channel.
Reviewed-by: jerboaa, neugens
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2012-December/004459.html
line wrap: on
line diff
--- a/agent/command/pom.xml Tue Dec 04 20:14:25 2012 -0500 +++ b/agent/command/pom.xml Mon Dec 10 14:30:36 2012 +0100 @@ -81,6 +81,12 @@ </dependency> <dependency> + <groupId>commons-codec</groupId> + <artifactId>commons-codec</artifactId> + <version>${commons-codec.version}</version> + </dependency> + + <dependency> <groupId>com.redhat.thermostat</groupId> <artifactId>thermostat-common-command</artifactId> <version>${project.version}</version>
--- a/agent/command/src/main/java/com/redhat/thermostat/agent/command/internal/ServerHandler.java Tue Dec 04 20:14:25 2012 -0500 +++ b/agent/command/src/main/java/com/redhat/thermostat/agent/command/internal/ServerHandler.java Mon Dec 10 14:30:36 2012 +0100 @@ -39,6 +39,7 @@ import java.util.logging.Level; import java.util.logging.Logger; +import org.apache.commons.codec.binary.Base64; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelFuture; import org.jboss.netty.channel.ChannelFutureListener; @@ -46,14 +47,20 @@ import org.jboss.netty.channel.ExceptionEvent; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.channel.SimpleChannelHandler; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceReference; import com.redhat.thermostat.agent.command.ReceiverRegistry; import com.redhat.thermostat.agent.command.RequestReceiver; import com.redhat.thermostat.common.command.Request; +import com.redhat.thermostat.common.command.Request.RequestType; import com.redhat.thermostat.common.command.Response; -import com.redhat.thermostat.common.command.Request.RequestType; import com.redhat.thermostat.common.command.Response.ResponseType; import com.redhat.thermostat.common.utils.LoggingUtils; +import com.redhat.thermostat.storage.core.AuthToken; +import com.redhat.thermostat.storage.core.SecureStorage; +import com.redhat.thermostat.storage.core.Storage; class ServerHandler extends SimpleChannelHandler { @@ -68,13 +75,19 @@ @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) { Request request = (Request) e.getMessage(); - logger.info("Request received: " + request.getType().toString()); - RequestReceiver receiver = receivers.getReceiver(request.getReceiver()); + boolean authSucceeded = authenticateRequestIfNecessary(request); Response response = null; - if (receiver != null) { - response = receiver.receive(request); + if (! authSucceeded) { + response = new Response(ResponseType.AUTH_FAILED); } else { - response = new Response(ResponseType.ERROR); + logger.info("Request received: " + request.getType().toString()); + RequestReceiver receiver = receivers.getReceiver(request + .getReceiver()); + if (receiver != null) { + response = receiver.receive(request); + } else { + response = new Response(ResponseType.ERROR); + } } Channel channel = ctx.getChannel(); if (channel.isConnected()) { @@ -86,6 +99,26 @@ } } + private boolean authenticateRequestIfNecessary(Request request) { + BundleContext bCtx = FrameworkUtil.getBundle(getClass()).getBundleContext(); + ServiceReference storageRef = bCtx.getServiceReference(Storage.class.getName()); + Storage storage = (Storage) bCtx.getService(storageRef); + if (storage instanceof SecureStorage) { + return authenticateRequest(request, (SecureStorage) storage); + } else { + return true; + } + } + + private boolean authenticateRequest(Request request, SecureStorage storage) { + String clientTokenStr = request.getParameter(Request.CLIENT_TOKEN); + byte[] clientToken = Base64.decodeBase64(clientTokenStr); + String authTokenStr = request.getParameter(Request.AUTH_TOKEN); + byte[] authToken = Base64.decodeBase64(authTokenStr); + AuthToken token = new AuthToken(authToken, clientToken); + return storage.verifyToken(token); + } + @Override public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) { logger.log(Level.WARNING, "Unexpected exception from downstream.", e.getCause());
--- a/client/command/pom.xml Tue Dec 04 20:14:25 2012 -0500 +++ b/client/command/pom.xml Mon Dec 10 14:30:36 2012 +0100 @@ -72,6 +72,11 @@ </dependency> <dependency> + <groupId>commons-codec</groupId> + <artifactId>commons-codec</artifactId> + <version>${commons-codec.version}</version> + </dependency> + <dependency> <groupId>com.redhat.thermostat</groupId> <artifactId>thermostat-common-core</artifactId> <version>${project.version}</version>
--- a/client/command/src/main/java/com/redhat/thermostat/client/command/internal/RequestQueueImpl.java Tue Dec 04 20:14:25 2012 -0500 +++ b/client/command/src/main/java/com/redhat/thermostat/client/command/internal/RequestQueueImpl.java Mon Dec 10 14:30:36 2012 +0100 @@ -39,15 +39,23 @@ import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; +import org.apache.commons.codec.binary.Base64; import org.jboss.netty.bootstrap.ClientBootstrap; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelFuture; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceReference; import com.redhat.thermostat.client.command.RequestQueue; import com.redhat.thermostat.common.command.Request; import com.redhat.thermostat.common.command.RequestResponseListener; import com.redhat.thermostat.common.command.Response; import com.redhat.thermostat.common.command.Response.ResponseType; +import com.redhat.thermostat.storage.core.AuthToken; +import com.redhat.thermostat.storage.core.SecureStorage; +import com.redhat.thermostat.storage.core.Storage; +import com.redhat.thermostat.storage.core.StorageException; class RequestQueueImpl implements RequestQueue { @@ -64,9 +72,29 @@ @Override public void putRequest(Request request) { + authenticateRequest(request); queue.add(request); } + private void authenticateRequest(Request request) { + BundleContext bCtx = FrameworkUtil.getBundle(getClass()).getBundleContext(); + ServiceReference storageRef = bCtx.getServiceReference(Storage.class.getName()); + Storage storage = (Storage) bCtx.getService(storageRef); + if (storage instanceof SecureStorage) { + authenticateRequest(request, (SecureStorage) storage); + } + } + + private void authenticateRequest(Request request, SecureStorage storage) { + try { + AuthToken token = storage.generateToken(); + request.setParameter(Request.CLIENT_TOKEN, Base64.encodeBase64String(token.getClientToken())); + request.setParameter(Request.AUTH_TOKEN, Base64.encodeBase64String(token.getToken())); + } catch (StorageException ex) { + fireComplete(request, new Response(ResponseType.AUTH_FAILED)); + } + } + synchronized void startProcessingRequests() { if (!running()) { processing = true; @@ -112,12 +140,17 @@ c.write(request); } else { Response response = new Response(ResponseType.ERROR); - // TODO add more information once Response supports parameters. - for (RequestResponseListener listener : request.getListeners()) { - listener.fireComplete(request, response); - } + fireComplete(request, response); } } } + + } + + private void fireComplete(Request request, Response response) { + // TODO add more information once Response supports parameters. + for (RequestResponseListener listener : request.getListeners()) { + listener.fireComplete(request, response); + } } }
--- a/common/command/src/main/java/com/redhat/thermostat/common/command/Request.java Tue Dec 04 20:14:25 2012 -0500 +++ b/common/command/src/main/java/com/redhat/thermostat/common/command/Request.java Mon Dec 10 14:30:36 2012 +0100 @@ -59,6 +59,9 @@ private static final String RECEIVER = "receiver"; + public static final String CLIENT_TOKEN = "client-token"; + public static final String AUTH_TOKEN = "auth-token"; + public Request(RequestType type, SocketAddress target) { this.type = type; parameters = new TreeMap<>();
--- a/common/command/src/main/java/com/redhat/thermostat/common/command/Response.java Tue Dec 04 20:14:25 2012 -0500 +++ b/common/command/src/main/java/com/redhat/thermostat/common/command/Response.java Mon Dec 10 14:30:36 2012 +0100 @@ -48,7 +48,8 @@ NOK, // Request has been acknowledged and refused agent-side. NOOP, // Request has been acknowledged, but no action deemed necessary agent-side. ERROR, // An error on the agent side. - EXCEPTION; // Exception caught by channel handler. Agent-side status unknown. + EXCEPTION, // Exception caught by channel handler. Agent-side status unknown. + AUTH_FAILED; // When authentication fails in SecureStorage. } ResponseType type;
--- a/distribution/config/commands/agent.properties Tue Dec 04 20:14:25 2012 -0500 +++ b/distribution/config/commands/agent.properties Mon Dec 10 14:30:36 2012 +0100 @@ -19,6 +19,7 @@ thermostat-storage-mongodb-${project.version}.jar, \ mongo.jar, \ commons-beanutils.jar, \ + commons-codec.jar, \ commons-collections.jar, \ commons-logging.jar, \ netty.jar
--- a/distribution/config/commands/gui.properties Tue Dec 04 20:14:25 2012 -0500 +++ b/distribution/config/commands/gui.properties Mon Dec 10 14:30:36 2012 +0100 @@ -2,6 +2,7 @@ mongo.jar, \ commons-beanutils.jar, \ commons-collections.jar, \ + commons-codec.jar, \ commons-logging.jar, \ gson.jar, \ thermostat-web-common-@project.version@.jar, \
--- a/distribution/config/commands/storage.properties Tue Dec 04 20:14:25 2012 -0500 +++ b/distribution/config/commands/storage.properties Mon Dec 10 14:30:36 2012 +0100 @@ -3,6 +3,7 @@ thermostat-agent-cli-@project.version@.jar, \ thermostat-common-command-@project.version@.jar, \ thermostat-agent-command-@project.version@.jar, \ + commons-codec.jar, \ netty.jar description = starts and stops the thermostat storage
--- a/distribution/config/commands/webservice.properties Tue Dec 04 20:14:25 2012 -0500 +++ b/distribution/config/commands/webservice.properties Mon Dec 10 14:30:36 2012 +0100 @@ -7,6 +7,7 @@ thermostat-storage-mongodb-${project.version}.jar, \ mongo.jar, \ commons-beanutils.jar, \ + commons-codec.jar, \ commons-collections.jar, \ commons-logging.jar, \ commons-fileupload.jar, \
--- a/distribution/pom.xml Tue Dec 04 20:14:25 2012 -0500 +++ b/distribution/pom.xml Mon Dec 10 14:30:36 2012 +0100 @@ -205,6 +205,8 @@ <phase>prepare-package</phase> <configuration> <target> + <symlink link="${project.build.directory}/libs/commons-codec.jar" + resource="${project.build.directory}/libs/commons-codec-1.7.jar" /> <symlink link="${project.build.directory}/libs/netty.jar" resource="${project.build.directory}/libs/netty-3.2.4.Final.jar" /> <symlink link="${project.build.directory}/libs/jline2.jar"
--- a/pom.xml Tue Dec 04 20:14:25 2012 -0500 +++ b/pom.xml Mon Dec 10 14:30:36 2012 +0100 @@ -76,6 +76,8 @@ <commons-io.version>2.4</commons-io.version> <commons-collections.version>3.2.1</commons-collections.version> <commons-logging.version>1.1.1</commons-logging.version> + <commons-codec.version>1.7</commons-codec.version> + <jline.version>2.9</jline.version> <lucene.version>3.6.0_1</lucene.version> <!--
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/storage/core/src/main/java/com/redhat/thermostat/storage/core/AuthToken.java Mon Dec 10 14:30:36 2012 +0100 @@ -0,0 +1,64 @@ +/* + * Copyright 2012 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat 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 2, or (at your + * option) any later version. + * + * Thermostat 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 Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + + +package com.redhat.thermostat.storage.core; + +import java.util.Arrays; + + +public class AuthToken { + + private byte[] token; + private byte[] clientToken; + + public AuthToken(byte[] token, byte[] clientToken) { + this.token = token; + this.clientToken = clientToken; + } + + public byte[] getToken() { + return token; + } + + public byte[] getClientToken() { + return clientToken; + } + + public String toString() { + return "AuthToken: client-token: " + Arrays.toString(clientToken) + ", token: " + Arrays.toString(token); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/storage/core/src/main/java/com/redhat/thermostat/storage/core/SecureStorage.java Mon Dec 10 14:30:36 2012 +0100 @@ -0,0 +1,77 @@ +/* + * Copyright 2012 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat 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 2, or (at your + * option) any later version. + * + * Thermostat 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 Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + + +package com.redhat.thermostat.storage.core; + +/** + * Provides authentication service to the command channel API. + * + * The protocol works as follows: + * + * <ol> + * <li> The client asks the SecureStorage for an authentication token using {@link #generateToken()}. + * This should happen over an authenticated and secured connection, thus authenticating the client + * in the storage. The returned AuthToken will carry a client-token (that has been generated by the + * client) and an auth-token (generated by the storage). If authentication fails at this stage + * (e.g. because the client does not have the necessary privileges/roles), a StorageException is + * thrown. + * </li> + * <li> The AuthToken (both parts, client and auth token), are sent together with the command request to + * the command receiver (usually an agent). + * </li> + * <li> The agent takes the tokens, and calls the SecureStorage's {@link #verifyToken(AuthToken)} to + * verify the validity of the tokens. Again, this needs to happen through an authenticated and + * secured connection, thus authenticating the receiver. The storage verifies that it generated + * the same token for an authenticated client before, and replies with true if it succeeds, and + * false otherwise. + */ +public interface SecureStorage { + + /** + * Generates a token in the storage that can be used to authenticate cmd channel requests. + * + * @throws StorageException if authentication fails at this point + */ + AuthToken generateToken() throws StorageException; + + /** + * Verifies the specified token in the storage. + * + * @return <code>true</code> if authentication succeeded, <code>false</code> otherwise + */ + boolean verifyToken(AuthToken token); +}
--- a/web/client/src/main/java/com/redhat/thermostat/web/client/WebStorage.java Tue Dec 04 20:14:25 2012 -0500 +++ b/web/client/src/main/java/com/redhat/thermostat/web/client/WebStorage.java Mon Dec 10 14:30:36 2012 +0100 @@ -46,6 +46,7 @@ import java.lang.reflect.Array; import java.net.MalformedURLException; import java.net.URL; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -53,6 +54,7 @@ import java.util.Map; import java.util.UUID; +import org.apache.commons.codec.binary.Base64; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; @@ -73,11 +75,13 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.redhat.thermostat.storage.core.AuthToken; import com.redhat.thermostat.storage.core.Category; import com.redhat.thermostat.storage.core.Connection; import com.redhat.thermostat.storage.core.Cursor; import com.redhat.thermostat.storage.core.Query; import com.redhat.thermostat.storage.core.Remove; +import com.redhat.thermostat.storage.core.SecureStorage; import com.redhat.thermostat.storage.core.Storage; import com.redhat.thermostat.storage.core.StorageException; import com.redhat.thermostat.storage.core.Update; @@ -89,7 +93,7 @@ import com.redhat.thermostat.web.common.WebRemove; import com.redhat.thermostat.web.common.WebUpdate; -public class WebStorage implements Storage { +public class WebStorage implements Storage, SecureStorage { private static class CloseableHttpEntity implements Closeable, HttpEntity { @@ -259,6 +263,7 @@ private DefaultHttpClient httpClient; private String username; private String password; + private SecureRandom random; public WebStorage() { categoryIds = new HashMap<>(); @@ -266,6 +271,7 @@ ClientConnectionManager connManager = new ThreadSafeClientConnManager(); DefaultHttpClient client = new DefaultHttpClient(connManager); httpClient = client; + random = new SecureRandom(); } private void initAuthentication(DefaultHttpClient client) throws MalformedURLException { @@ -480,4 +486,46 @@ this.password = password; } + @Override + public AuthToken generateToken() throws StorageException { + byte[] token = new byte[256]; + random.nextBytes(token); + NameValuePair clientTokenParam = new BasicNameValuePair("client-token", Base64.encodeBase64String(token)); + List<NameValuePair> formparams = Arrays.asList(clientTokenParam); + try (CloseableHttpEntity entity = post(endpoint + "/generate-token", formparams)) { + byte[] authToken = EntityUtils.toByteArray(entity); + return new AuthToken(authToken, token); + } catch (IOException ex) { + throw new StorageException(ex); + } + } + + @Override + public boolean verifyToken(AuthToken authToken) { + byte[] clientToken = authToken.getClientToken(); + byte[] token = authToken.getToken(); + NameValuePair clientTokenParam = new BasicNameValuePair("client-token", Base64.encodeBase64String(clientToken)); + NameValuePair tokenParam = new BasicNameValuePair("token", Base64.encodeBase64String(token)); + List<NameValuePair> formparams = Arrays.asList(clientTokenParam, tokenParam); + HttpResponse response = null; + try { + HttpEntity entity = new UrlEncodedFormEntity(formparams, "UTF-8"); + HttpPost httpPost = new HttpPost(endpoint + "/verify-token"); + httpPost.setEntity(entity); + response = httpClient.execute(httpPost); + StatusLine status = response.getStatusLine(); + return status.getStatusCode() == 200; + } catch (IOException ex) { + throw new StorageException(ex); + } finally { + if (response != null) { + try { + EntityUtils.consume(response.getEntity()); + } catch (IOException ex) { + throw new StorageException(ex); + } + } + } + } + }
--- a/web/client/src/test/java/com/redhat/thermostat/web/client/WebStorageTest.java Tue Dec 04 20:14:25 2012 -0500 +++ b/web/client/src/test/java/com/redhat/thermostat/web/client/WebStorageTest.java Mon Dec 10 14:30:36 2012 +0100 @@ -52,6 +52,7 @@ import java.io.StringReader; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; +import java.net.URLEncoder; import java.util.Arrays; import java.util.Enumeration; import java.util.HashMap; @@ -63,6 +64,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.codec.binary.Base64; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.AbstractHandler; @@ -76,6 +78,8 @@ import com.google.gson.JsonArray; import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; +import com.redhat.thermostat.storage.core.AuthToken; +import com.redhat.thermostat.storage.core.AuthToken; import com.redhat.thermostat.storage.core.Categories; import com.redhat.thermostat.storage.core.Category; import com.redhat.thermostat.storage.core.Cursor; @@ -103,6 +107,7 @@ private Map<String,String> headers; private String method; private String requestURI; + private int responseStatus; private static Category category; private static Key<String> key1; @@ -140,6 +145,7 @@ headers = new HashMap<>(); requestURI = null; method = null; + responseStatus = HttpServletResponse.SC_OK; registerCategory(); } @@ -172,7 +178,7 @@ } requestBody = body.toString(); // Send response body. - response.setStatus(HttpServletResponse.SC_OK); + response.setStatus(responseStatus); if (responseBody != null) { response.getWriter().write(responseBody); } @@ -322,7 +328,6 @@ StringReader reader = new StringReader(requestBody); BufferedReader bufRead = new BufferedReader(reader); String line = URLDecoder.decode(bufRead.readLine(), "UTF-8"); - System.err.println("line: " + line); String[] parts = line.split("="); assertEquals("remove", parts[0]); WebRemove actualRemove = gson.fromJson(parts[1], WebRemove.class); @@ -469,4 +474,70 @@ assertEquals("POST", method); assertTrue(requestURI.endsWith("/purge")); } + + @Test + public void testGenerateToken() throws UnsupportedEncodingException { + responseBody = "flufftoken"; + + AuthToken authToken = storage.generateToken(); + + assertTrue(requestURI.endsWith("/generate-token")); + assertEquals("POST", method); + + String[] requestParts = requestBody.split("="); + assertEquals("client-token", requestParts[0]); + String clientTokenParam = URLDecoder.decode(requestParts[1], "UTF-8"); + byte[] clientToken = Base64.decodeBase64(clientTokenParam); + assertEquals(256, clientToken.length); + + assertTrue(authToken instanceof AuthToken); + AuthToken token = (AuthToken) authToken; + byte[] tokenBytes = token.getToken(); + assertEquals("flufftoken", new String(tokenBytes)); + + assertTrue(Arrays.equals(clientToken, token.getClientToken())); + + // Send another request and verify that we send a different client-token every time. + storage.generateToken(); + + requestParts = requestBody.split("="); + assertEquals("client-token", requestParts[0]); + clientTokenParam = URLDecoder.decode(requestParts[1], "UTF-8"); + byte[] clientToken2 = Base64.decodeBase64(clientTokenParam); + assertFalse(Arrays.equals(clientToken, clientToken2)); + + } + + @Test + public void testVerifyToken() throws UnsupportedEncodingException { + byte[] token = "stuff".getBytes(); + byte[] clientToken = "fluff".getBytes(); + AuthToken authToken = new AuthToken(token, clientToken); + + responseStatus = 200; + boolean ok = storage.verifyToken(authToken); + assertTrue(requestURI.endsWith("/verify-token")); + assertEquals("POST", method); + String[] requestParts = requestBody.split("&"); + assertEquals(2, requestParts.length); + String[] clientTokenParts = requestParts[0].split("="); + assertEquals(2, clientTokenParts.length); + assertEquals("client-token", clientTokenParts[0]); + String urlDecoded = URLDecoder.decode(clientTokenParts[1], "UTF-8"); + String base64decoded = new String(Base64.decodeBase64(urlDecoded)); + assertEquals("fluff", base64decoded); + String[] authTokenParts = requestParts[1].split("="); + assertEquals(2, authTokenParts.length); + assertEquals("token", authTokenParts[0]); + urlDecoded = URLDecoder.decode(authTokenParts[1], "UTF-8"); + base64decoded = new String(Base64.decodeBase64(urlDecoded)); + assertEquals("stuff", base64decoded); + assertTrue(ok); + + // Try another one in which verification fails. + responseStatus = 401; + ok = storage.verifyToken(authToken); + assertFalse(ok); + + } }
--- a/web/cmd/src/main/java/com/redhat/thermostat/web/cmd/WebServiceLauncher.java Tue Dec 04 20:14:25 2012 -0500 +++ b/web/cmd/src/main/java/com/redhat/thermostat/web/cmd/WebServiceLauncher.java Mon Dec 10 14:30:36 2012 +0100 @@ -107,7 +107,7 @@ ConstraintMapping constraintMap = new ConstraintMapping(); Constraint constraint = new Constraint(); constraint.setAuthenticate(true); - constraint.setRoles(new String[] { "thermostat-client", "thermostat-agent" }); + constraint.setRoles(new String[] { "thermostat-client", "thermostat-agent", "thermostat-cmd-channel" }); constraint.setName("Entire Application"); constraintMap.setPathSpec("/*"); constraintMap.setConstraint(constraint); @@ -121,12 +121,12 @@ @Override protected void loadUsers() throws IOException { - putUser("thermostat", new Password("thermostat"), new String[] { "thermostat-agent", "thermostat-client" }); + putUser("thermostat", new Password("thermostat"), new String[] { "thermostat-agent", "thermostat-client", "thermostat-cmd-channel" }); } @Override protected UserIdentity loadUser(String username) { - return new DefaultUserIdentity(null, null, new String[] { "thermostat-agent", "thermostat-client" }); + return new DefaultUserIdentity(null, null, new String[] { "thermostat-agent", "thermostat-client", "thermostat-cmd-channel" }); } }); ctx.setSecurityHandler(secHandler);
--- a/web/server/pom.xml Tue Dec 04 20:14:25 2012 -0500 +++ b/web/server/pom.xml Mon Dec 10 14:30:36 2012 +0100 @@ -105,6 +105,11 @@ <artifactId>commons-io</artifactId> <version>${commons-io.version}</version> </dependency> + <dependency> + <groupId>commons-codec</groupId> + <artifactId>commons-codec</artifactId> + <version>${commons-codec.version}</version> + </dependency> <dependency> <groupId>org.osgi</groupId> @@ -117,7 +122,6 @@ <artifactId>thermostat-web-common</artifactId> <version>${project.version}</version> </dependency> - </dependencies> <build>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/main/java/com/redhat/thermostat/web/server/TokenManager.java Mon Dec 10 14:30:36 2012 +0100 @@ -0,0 +1,96 @@ +/* + * Copyright 2012 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat 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 2, or (at your + * option) any later version. + * + * Thermostat 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 Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + + +package com.redhat.thermostat.web.server; + +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; + +class TokenManager { + + private static final int TOKEN_LENGTH = 256; + + private SecureRandom random = new SecureRandom(); + + private Map<String,byte[]> tokens = Collections.synchronizedMap(new HashMap<String,byte[]>()); + + // Maybe use a ScheduledExecutorService if this turns out to not scale well enough. + private Timer timer = new Timer(); + + private int timeout = 30 * 1000; + + void setTimeout(int timeout) { + this.timeout = timeout; + } + + byte[] generateToken(String clientToken) { + byte[] token = new byte[TOKEN_LENGTH]; + random.nextBytes(token); + tokens.put(clientToken, token); + scheduleRemoval(clientToken); + return token; + } + + private void scheduleRemoval(final String clientToken) { + TimerTask task = new TimerTask() { + + @Override + public void run() { + tokens.remove(clientToken); + } + }; + timer.schedule(task, timeout); + } + + boolean verifyToken(String clientToken, byte[] token) { + if (tokens.containsKey(clientToken)) { + byte[] storedToken = tokens.get(clientToken); + boolean verified = Arrays.equals(token, storedToken); + if (verified) { + tokens.remove(clientToken); + } + return verified; + } + return false; + } + +}
--- a/web/server/src/main/java/com/redhat/thermostat/web/server/WebStorageEndPoint.java Tue Dec 04 20:14:25 2012 -0500 +++ b/web/server/src/main/java/com/redhat/thermostat/web/server/WebStorageEndPoint.java Mon Dec 10 14:30:36 2012 +0100 @@ -50,6 +50,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.codec.binary.Base64; import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.FileItemFactory; import org.apache.commons.fileupload.FileUploadException; @@ -65,10 +66,10 @@ import com.redhat.thermostat.storage.core.Cursor; import com.redhat.thermostat.storage.core.Key; import com.redhat.thermostat.storage.core.Query; +import com.redhat.thermostat.storage.core.Query.Criteria; import com.redhat.thermostat.storage.core.Remove; import com.redhat.thermostat.storage.core.Storage; import com.redhat.thermostat.storage.core.Update; -import com.redhat.thermostat.storage.core.Query.Criteria; import com.redhat.thermostat.storage.model.AgentIdPojo; import com.redhat.thermostat.storage.model.Pojo; import com.redhat.thermostat.web.common.Qualifier; @@ -82,7 +83,12 @@ @SuppressWarnings("serial") public class WebStorageEndPoint extends HttpServlet { + private static final String TOKEN_MANAGER_TIMEOUT_PARAM = "token-manager-timeout"; + private static final String TOKEN_MANAGER_KEY = "token-manager"; private static final String ROLE_THERMOSTAT_AGENT = "thermostat-agent"; + private static final String ROLE_THERMOSTAT_CLIENT = "thermostat-client"; + private static final String ROLE_CMD_CHANNEL = "thermostat-cmd-channel"; + private Storage storage; private Gson gson; @@ -98,6 +104,12 @@ gson = new GsonBuilder().registerTypeHierarchyAdapter(Pojo.class, new ThermostatGSONConverter()).create(); categoryIds = new HashMap<>(); categories = new HashMap<>(); + TokenManager tokenManager = new TokenManager(); + String timeoutParam = getInitParameter(TOKEN_MANAGER_TIMEOUT_PARAM); + if (timeoutParam != null) { + tokenManager.setTimeout(Integer.parseInt(timeoutParam)); + } + getServletContext().setAttribute(TOKEN_MANAGER_KEY, tokenManager); } @Override @@ -132,6 +144,10 @@ purge(req, resp); } else if (cmd.equals("ping")) { ping(req, resp); + } else if (cmd.equals("generate-token")) { + generateToken(req, resp); + } else if (cmd.equals("verify-token")) { + verifyToken(req, resp); } } @@ -356,4 +372,36 @@ resp.flushBuffer(); } + private void generateToken(HttpServletRequest req, HttpServletResponse resp) throws IOException { + if (! req.isUserInRole(ROLE_CMD_CHANNEL)) { + resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + TokenManager tokenManager = (TokenManager) getServletContext().getAttribute(TOKEN_MANAGER_KEY); + assert tokenManager != null; + String clientToken = req.getParameter("client-token"); + byte[] token = tokenManager.generateToken(clientToken); + resp.setContentType("application/octet-stream"); + resp.setContentLength(token.length); + resp.getOutputStream().write(token); + } + + private void verifyToken(HttpServletRequest req, HttpServletResponse resp) { + if (! req.isUserInRole(ROLE_CMD_CHANNEL)) { + resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + TokenManager tokenManager = (TokenManager) getServletContext().getAttribute(TOKEN_MANAGER_KEY); + assert tokenManager != null; + String clientToken = req.getParameter("client-token"); + byte[] token = Base64.decodeBase64(req.getParameter("token")); + boolean verified = tokenManager.verifyToken(clientToken, token); + if (! verified) { + resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } else { + resp.setStatus(HttpServletResponse.SC_OK); + } + } + + }
--- a/web/server/src/main/webapp/WEB-INF/web.xml Tue Dec 04 20:14:25 2012 -0500 +++ b/web/server/src/main/webapp/WEB-INF/web.xml Mon Dec 10 14:30:36 2012 +0100 @@ -8,6 +8,11 @@ <servlet> <servlet-name>reststorage-servlet</servlet-name> <servlet-class>com.redhat.thermostat.web.server.WebStorageEndPoint</servlet-class> + <!-- The timeout of the token manager in ms. We use 500ms for testing. --> + <init-param> + <param-name>token-manager-timeout</param-name> + <param-value>500</param-value> + </init-param> </servlet> <servlet-mapping>
--- a/web/server/src/test/java/com/redhat/thermostat/web/server/WebStorageEndpointTest.java Tue Dec 04 20:14:25 2012 -0500 +++ b/web/server/src/test/java/com/redhat/thermostat/web/server/WebStorageEndpointTest.java Mon Dec 10 14:30:36 2012 +0100 @@ -59,6 +59,7 @@ import java.util.HashMap; import java.util.Map; +import org.apache.commons.codec.binary.Base64; import org.eclipse.jetty.security.DefaultUserIdentity; import org.eclipse.jetty.security.MappedLoginService; import org.eclipse.jetty.server.Server; @@ -177,10 +178,15 @@ @Override protected void loadUsers() throws IOException { putUser("testname", new Password("testpasswd"), new String[] { "thermostat-agent" }); + putUser("test-no-role", new Password("testpasswd"), new String[] { "fluff" }); + putUser("test-cmd-channel", new Password("testpasswd"), new String[] { "thermostat-cmd-channel" }); } @Override protected UserIdentity loadUser(String username) { + if (username.equals("test-cmd-channel")) { + return new DefaultUserIdentity(null, null, new String[] { "thermostat-cmd-channel" }); + } return new DefaultUserIdentity(null, null, new String[] { "thermostat-agent" }); } }); @@ -289,10 +295,7 @@ URL url = new URL(endpoint + "/put-pojo"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); - BASE64Encoder enc = new BASE64Encoder(); - String userpassword = "testname:testpasswd"; - String encodedAuthorization = enc.encode( userpassword.getBytes() ); - conn.setRequestProperty("Authorization", "Basic "+ encodedAuthorization); + sendAuthorization(conn, "testname", "testpasswd"); conn.setDoOutput(true); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); @@ -310,6 +313,13 @@ verify(mockStorage).putPojo(category, true, expected1); } + private void sendAuthorization(HttpURLConnection conn, String username, String passwd) { + BASE64Encoder enc = new BASE64Encoder(); + String userpassword = username + ":" + passwd; + String encodedAuthorization = enc.encode( userpassword.getBytes() ); + conn.setRequestProperty("Authorization", "Basic "+ encodedAuthorization); + } + @Test public void testRemovePojo() throws IOException { @@ -423,7 +433,6 @@ out.write("--fluff--\r\n"); out.flush(); int status = conn.getResponseCode(); - System.err.println("status: " + status); ArgumentCaptor<InputStream> inCaptor = ArgumentCaptor.forClass(InputStream.class); verify(mockStorage).saveFile(eq("fluff"), inCaptor.capture()); InputStream in = inCaptor.getValue(); @@ -510,6 +519,118 @@ } private String getEndpoint() { - return "http://testname:testpasswd@localhost:" + port + "/storage"; + return "http://localhost:" + port + "/storage"; + } + + @Test + public void testBasicGenerateToken() throws IOException { + + verifyGenerateToken(); + } + + @Test + public void testGenerateTokenWithoutAuth() throws IOException { + + String endpoint = getEndpoint(); + URL url = new URL(endpoint + "/generate-token"); + + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + sendAuthorization(conn, "test-no-role", "testpasswd"); + conn.setDoOutput(true); + conn.setDoInput(true); + OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream()); + out.write("client-token=fluff"); + out.flush(); + assertEquals(401, conn.getResponseCode()); + } + + @Test + public void testBasicGenerateVerifyToken() throws IOException { + + byte[] token = verifyGenerateToken(); + + String endpoint = getEndpoint(); + URL url = new URL(endpoint + "/verify-token"); + + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-type", "application/x-www-form-urlencoded"); + sendAuthorization(conn, "test-cmd-channel", "testpasswd"); + conn.setDoOutput(true); + conn.setDoInput(true); + OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream()); + out.write("client-token=fluff&token=" + URLEncoder.encode(Base64.encodeBase64String(token), "UTF-8")); + out.flush(); + assertEquals(200, conn.getResponseCode()); + } + + @Test + public void testTokenTimeout() throws IOException, InterruptedException { + + byte[] token = verifyGenerateToken(); + + Thread.sleep(700); // Timeout is set to 500ms for tests, 700ms should be enough for everybody. ;-) + + String endpoint = getEndpoint(); + URL url = new URL(endpoint + "/verify-token"); + + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-type", "application/x-www-form-urlencoded"); + sendAuthorization(conn, "test-cmd-channel", "testpasswd"); + conn.setDoOutput(true); + conn.setDoInput(true); + OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream()); + out.write("client-token=fluff&token=" + URLEncoder.encode(Base64.encodeBase64String(token), "UTF-8")); + out.flush(); + assertEquals(401, conn.getResponseCode()); + } + + @Test + public void testVerifyNonExistentToken() throws IOException { + + byte[] token = "fluff".getBytes(); + + String endpoint = getEndpoint(); + URL url = new URL(endpoint + "/verify-token"); + + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-type", "application/x-www-form-urlencoded"); + sendAuthorization(conn, "test-cmd-channel", "testpasswd"); + conn.setDoOutput(true); + conn.setDoInput(true); + OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream()); + out.write("client-token=fluff&token=" + URLEncoder.encode(Base64.encodeBase64String(token), "UTF-8")); + out.flush(); + assertEquals(401, conn.getResponseCode()); + } + + private byte[] verifyGenerateToken() throws IOException { + String endpoint = getEndpoint(); + URL url = new URL(endpoint + "/generate-token"); + + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + sendAuthorization(conn, "test-cmd-channel", "testpasswd"); + conn.setDoOutput(true); + conn.setDoInput(true); + OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream()); + out.write("client-token=fluff"); + out.flush(); + InputStream in = conn.getInputStream(); + int length = conn.getContentLength(); + byte[] token = new byte[length]; + assertEquals(256, length); + int totalRead = 0; + while (totalRead < length) { + int read = in.read(token, totalRead, length - totalRead); + if (read < 0) { + fail(); + } + totalRead += read; + } + return token; } }
--- a/web/war/src/main/webapp/WEB-INF/web.xml Tue Dec 04 20:14:25 2012 -0500 +++ b/web/war/src/main/webapp/WEB-INF/web.xml Mon Dec 10 14:30:36 2012 +0100 @@ -15,6 +15,11 @@ <param-name>storage.endpoint</param-name> <param-value>mongodb://127.0.0.1:27518</param-value> </init-param> + <!-- The timeout of the token manager in ms --> + <init-param> + <param-name>token-manager-timeout</param-name> + <param-value>3000</param-value> + </init-param> <servlet-name>reststorage-servlet</servlet-name> <servlet-class>com.redhat.thermostat.web.server.WebStorageEndPoint</servlet-class> </servlet> @@ -32,6 +37,7 @@ <auth-constraint> <role-name>thermostat-agent</role-name> <role-name>thermostat-client</role-name> + <role-name>thermostat-cmd-channel</role-name> </auth-constraint> </security-constraint>