changeset 221:1a1e29c83c7b

Protocol update for commands micro-service. Reviewed-by: neugens Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-July/024338.html
author Severin Gehwolf <sgehwolf@redhat.com>
date Thu, 27 Jul 2017 18:33:26 +0200
parents 96345741f7ce
children 52e4c97adcd7
files services/commands/etc/commands/basic-users.properties services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/channel/WebSocketCommunicationBuilder.java services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/channel/coders/typeadapters/AgentRequestTypeAdapter.java services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/channel/coders/typeadapters/BasicMessageTypeAdapter.java services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/channel/model/AgentRequest.java services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/channel/model/Message.java services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/socket/CommandChannelAgentSocket.java services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/socket/CommandChannelClientSocket.java services/commands/src/main/resources/agent.html services/commands/src/main/resources/agent2.html services/commands/src/main/resources/index.html services/commands/src/test/java/com/redhat/thermostat/gateway/service/commands/channel/coders/AgentRequestEncoderTest.java services/commands/src/test/java/com/redhat/thermostat/gateway/service/commands/channel/coders/EncodeDecodeTest.java services/commands/src/test/java/com/redhat/thermostat/gateway/service/commands/channel/coders/MessageDecoderTest.java services/commands/src/test/java/com/redhat/thermostat/gateway/service/commands/channel/model/AgentRequestTest.java services/commands/src/test/java/com/redhat/thermostat/gateway/service/commands/http/handlers/AuthBasicCoreServerTest.java
diffstat 16 files changed, 234 insertions(+), 78 deletions(-) [+]
line wrap: on
line diff
--- a/services/commands/etc/commands/basic-users.properties	Fri Jul 28 18:48:35 2017 +0200
+++ b/services/commands/etc/commands/basic-users.properties	Thu Jul 27 18:33:26 2017 +0200
@@ -1,5 +1,5 @@
 # Agent user(s)
-foo-agent-user=agent-pwd,thermostat-commands-realm,thermostat-commands-provider-testAgent
-agent-user=agent-pwd,thermostat-commands-realm,thermostat-commands-provider-otherAgent
+foo-agent-user=agent-pwd,thermostat-commands-realm,thermostat-commands-receiver-provider
+agent-user=agent-pwd,thermostat-commands-realm,thermostat-commands-receiver-provider
 # Client user
-bar-client-user=client-pwd,thermostat-commands-realm,thermostat-commands-grant-dump-heap,thermostat-commands-grant-jvm-abc
\ No newline at end of file
+bar-client-user=client-pwd,thermostat-commands-realm,thermostat-commands-grant-kill_vm,thermostat-commands-grant-ping
--- a/services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/channel/WebSocketCommunicationBuilder.java	Fri Jul 28 18:48:35 2017 +0200
+++ b/services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/channel/WebSocketCommunicationBuilder.java	Thu Jul 27 18:33:26 2017 +0200
@@ -48,6 +48,9 @@
     private Session clientSession;
     private Session agentSession;
     private ClientRequest clientRequest;
+    private String action;
+    private String jvmId;
+    private String systemId;
 
     public WebSocketCommunicationBuilder setRequest(ClientRequest request) {
         this.clientRequest = request;
@@ -64,11 +67,29 @@
         return this;
     }
 
+    public WebSocketCommunicationBuilder setAction(String action) {
+        this.action = action;
+        return this;
+    }
+
+    public WebSocketCommunicationBuilder setJvmId(String jvmId) {
+        this.jvmId = jvmId;
+        return this;
+    }
+
+    public WebSocketCommunicationBuilder setSystemId(String systemId) {
+        this.systemId = systemId;
+        return this;
+    }
+
     public ClientAgentCommunication build() {
         Objects.requireNonNull(clientSession);
         Objects.requireNonNull(agentSession);
         Objects.requireNonNull(clientRequest);
-        AgentRequest agentRequest = new AgentRequest(clientRequest.getSequenceId(), clientRequest.getParams());
+        Objects.requireNonNull(action);
+        Objects.requireNonNull(jvmId);
+        Objects.requireNonNull(systemId);
+        AgentRequest agentRequest = new AgentRequest(clientRequest.getSequenceId(), action, systemId, jvmId, clientRequest.getParams());
         AgentGatewayCommunication agent = new AgentGatewayCommunication(clientSession, agentSession, agentRequest);
        return new ClientAgentCommunication(agent, clientRequest);
     }
--- a/services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/channel/coders/typeadapters/AgentRequestTypeAdapter.java	Fri Jul 28 18:48:35 2017 +0200
+++ b/services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/channel/coders/typeadapters/AgentRequestTypeAdapter.java	Thu Jul 27 18:33:26 2017 +0200
@@ -55,6 +55,12 @@
     @Override
     public void write(JsonWriter out, AgentRequest request) throws IOException {
         JsonObject object = getEnvelopeWithTypeAndSequence(request, request.getSequenceId());
+        JsonElement actionElem = gson.toJsonTree(request.getAction());
+        object.add(Message.ACTION_KEY, actionElem);
+        JsonElement jvmIdElem = gson.toJsonTree(request.getJvmId());
+        object.add(Message.JVM_ID_KEY, jvmIdElem);
+        JsonElement systemIdElem = gson.toJsonTree(request.getSystemId());
+        object.add(Message.SYSTEM_ID_KEY, systemIdElem);
         JsonElement parms = gson.toJsonTree(request.getParams());
         object.add(Message.PAYLOAD_KEY, parms);
         gson.toJson(object, out);
--- a/services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/channel/coders/typeadapters/BasicMessageTypeAdapter.java	Fri Jul 28 18:48:35 2017 +0200
+++ b/services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/channel/coders/typeadapters/BasicMessageTypeAdapter.java	Thu Jul 27 18:33:26 2017 +0200
@@ -86,18 +86,14 @@
         RawMessage raw = getRawMessageFromReader(in);
         switch (raw.getMessageType()) {
         case AGENT_REQUEST: {
-            requireSequenceNonNull(raw.getSequenceElement(),
-                                   "Agent request without a sequence!");
-            long sequence = raw.getSequenceElement().getAsLong();
-            SortedMap<String, String> params = decodePayloadAsParamMap(raw.getPayloadElement());
-            return new AgentRequest(sequence, params);
+            return decodeAgentRequest(raw);
         }
         case CLIENT_REQUEST: {
             SortedMap<String, String> params = decodePayloadAsParamMap(raw.getPayloadElement());
             return new ClientRequest(params);
         }
         case RESPONSE: {
-            requireSequenceNonNull(raw.getSequenceElement(),
+            requireElementNonNull(raw.getSequenceElement(),
                                    "Response message without a sequence!");
             long sequence = raw.getSequenceElement().getAsLong();
             ResponseType respType = decodePayloadAsResponseType(raw.getPayloadElement());
@@ -108,6 +104,23 @@
         }
     }
 
+    private Message decodeAgentRequest(RawMessage raw) {
+        requireElementNonNull(raw.getSequenceElement(),
+                               "Agent request without a sequence!");
+        long sequence = raw.getSequenceElement().getAsLong();
+        requireElementNonNull(raw.getActionElement(),
+                              "Agent request without action!");
+        String action = raw.getActionElement().getAsString();
+        requireElementNonNull(raw.getJvmIdElement(),
+                              "Agent request without jvmId!");
+        String jvmId = raw.getJvmIdElement().getAsString();
+        requireElementNonNull(raw.getSystemIdElement(),
+                "Agent request without systemId!");
+        String systemId = raw.getSystemIdElement().getAsString();
+        SortedMap<String, String> params = decodePayloadAsParamMap(raw.getPayloadElement());
+        return new AgentRequest(sequence, action, systemId, jvmId, params);
+    }
+
     private SortedMap<String, String> decodePayloadAsParamMap(JsonObject payloadElem) {
         SortedMap<String, String> paramMap = new TreeMap<>();
         if (payloadElem == null) {
@@ -133,8 +146,8 @@
         return ResponseType.valueOf(typeStr);
     }
 
-    private void requireSequenceNonNull(JsonElement sequenceElem, String msg) throws IllegalStateException {
-        if (sequenceElem == null) {
+    private void requireElementNonNull(JsonElement elem, String msg) throws IllegalStateException {
+        if (elem == null) {
             throw new IllegalStateException(msg);
         }
     }
@@ -151,6 +164,9 @@
         private final JsonElement typeElem;
         private final JsonElement sequenceElem;
         private final JsonObject payloadElem;
+        private final JsonElement actionElem;
+        private final JsonElement jvmIdElem;
+        private final JsonElement systemIdElem;
 
         private RawMessage(JsonObject object) {
             Objects.requireNonNull(object);
@@ -158,6 +174,9 @@
             typeElem = object.get(Message.TYPE_KEY);
             sequenceElem = object.get(Message.SEQUENCE_KEY);
             payloadElem = (JsonObject)object.get(Message.PAYLOAD_KEY);
+            actionElem = object.get(Message.ACTION_KEY);
+            jvmIdElem = object.get(Message.JVM_ID_KEY);
+            systemIdElem = object.get(Message.SYSTEM_ID_KEY);
         }
 
         MessageType getMessageType() {
@@ -170,6 +189,18 @@
             return sequenceElem;
         }
 
+        JsonElement getActionElement() {
+            return actionElem;
+        }
+
+        JsonElement getJvmIdElement() {
+            return jvmIdElem;
+        }
+
+        JsonElement getSystemIdElement() {
+            return systemIdElem;
+        }
+
         JsonObject getPayloadElement() {
             return payloadElem;
         }
--- a/services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/channel/model/AgentRequest.java	Fri Jul 28 18:48:35 2017 +0200
+++ b/services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/channel/model/AgentRequest.java	Thu Jul 27 18:33:26 2017 +0200
@@ -36,19 +36,49 @@
 
 package com.redhat.thermostat.gateway.service.commands.channel.model;
 
+import java.util.Objects;
 import java.util.SortedMap;
 
 /**
- * A Command Channel Request relayed to an agent (a.k.a receiver).
+ * A Command Channel Request relayed to an agent (a.k.a receiver). An agent
+ * message contains additional information which might be security relevant.
+ *
+ * In particular, {@code action}, {@code jvmId} and {@code systemId} are
+ * strings which have been looked at by the authorization layer and can, thus,
+ * be trusted. If there was an authorization problem, no agent request would
+ * have been created.
  */
 public class AgentRequest extends WebSocketRequest implements Message {
 
-    public AgentRequest(long sequence, SortedMap<String, String> params) {
+    private final String action;
+    private final String jvmId;
+    private final String systemId;
+
+    public AgentRequest(long sequence,
+                        String action,
+                        String systemId,
+                        String jvmId,
+                        SortedMap<String, String> params) {
         super(sequence, params);
+        this.action = Objects.requireNonNull(action);
+        this.jvmId = Objects.requireNonNull(jvmId);
+        this.systemId = Objects.requireNonNull(systemId);
     }
 
     @Override
     public MessageType getMessageType() {
         return MessageType.AGENT_REQUEST;
     }
+
+    public String getAction() {
+        return action;
+    }
+
+    public String getJvmId() {
+        return jvmId;
+    }
+
+    public String getSystemId() {
+        return systemId;
+    }
 }
--- a/services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/channel/model/Message.java	Fri Jul 28 18:48:35 2017 +0200
+++ b/services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/channel/model/Message.java	Thu Jul 27 18:33:26 2017 +0200
@@ -45,6 +45,9 @@
     public static final String PAYLOAD_KEY = "payload";
     public static final String TYPE_KEY = "type";
     public static final String SEQUENCE_KEY = "sequence";
+    public static final String ACTION_KEY = "action";
+    public static final String SYSTEM_ID_KEY = "systemId";
+    public static final String JVM_ID_KEY = "jvmId";
 
     public MessageType getMessageType();
 
--- a/services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/socket/CommandChannelAgentSocket.java	Fri Jul 28 18:48:35 2017 +0200
+++ b/services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/socket/CommandChannelAgentSocket.java	Thu Jul 27 18:33:26 2017 +0200
@@ -58,7 +58,7 @@
     private static final Logger logger = LoggingUtil.getLogger(CommandChannelAgentSocket.class);
     private static final long SOCKET_SESSION_IDLE_TIMEOUT = TimeUnit.MINUTES.toMillis(10);
     private static final String UNKNOWN_PAYLOAD = "UNKNOWN";
-    private static final String AGENT_PROVIDER_PREFIX = "thermostat-commands-provider-";
+    private static final String RECEIVER_PROVIDER_ROLE = "thermostat-commands-receiver-provider";
 
     CommandChannelAgentSocket(String id, Session session) {
         super(id, session);
@@ -138,8 +138,7 @@
     protected boolean checkRoles() {
         // FIXME: relies on RoleAwareUser - i.e. specific auth scheme.
         RoleAwareUser user = (RoleAwareUser) session.getUserPrincipal();
-        String roleToCheck = AGENT_PROVIDER_PREFIX + agentId;
-        if (!user.isUserInRole(roleToCheck)) {
+        if (!user.isUserInRole(RECEIVER_PROVIDER_ROLE)) {
             return false;
         }
         return true;
--- a/services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/socket/CommandChannelClientSocket.java	Fri Jul 28 18:48:35 2017 +0200
+++ b/services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/socket/CommandChannelClientSocket.java	Thu Jul 27 18:33:26 2017 +0200
@@ -43,10 +43,10 @@
 import javax.websocket.Session;
 
 import com.redhat.thermostat.gateway.common.core.auth.basic.RoleAwareUser;
-import com.redhat.thermostat.gateway.service.commands.channel.model.ClientRequest;
 import com.redhat.thermostat.gateway.service.commands.channel.ClientAgentCommunication;
 import com.redhat.thermostat.gateway.service.commands.channel.CommunicationsRegistry;
 import com.redhat.thermostat.gateway.service.commands.channel.WebSocketCommunicationBuilder;
+import com.redhat.thermostat.gateway.service.commands.channel.model.ClientRequest;
 import com.redhat.thermostat.gateway.service.commands.channel.model.Message;
 import com.redhat.thermostat.gateway.service.commands.channel.model.WebSocketResponse;
 import com.redhat.thermostat.gateway.service.commands.channel.model.WebSocketResponse.ResponseType;
@@ -54,9 +54,9 @@
 class CommandChannelClientSocket extends CommandChannelSocket {
 
     private static final String CLIENT_GRANT_ACTION_PREFIX = "thermostat-commands-grant-";
-    private static final String CLIENT_GRANT_JVM_PREFIX = "thermostat-commands-grant-jvm-";
     private static final String PATH_PARAM_ACTION = "action";
-    private static final String PATH_PARAM_JVM = "jvmId";
+    private static final String PATH_PARAM_SYSTEM_ID = "systemId";
+    private static final String PATH_PARAM_JVM_ID = "jvmId";
     private static final String PATH_PARAM_SEQ_ID = "seqId";
     private final long socketTimeout;
 
@@ -76,6 +76,7 @@
         } catch (NumberFormatException e) {
             sendErrorResponse(sequence /* will be unknown */);
         }
+        AgentRequestParamsHolder agentParams = getParamsFromSession(session);
         // Note: agent/client session will have the same default timeout.
         //       Thus, it's fine to use the client's set timeout value for
         //       retrieving the agent registry.
@@ -95,24 +96,15 @@
                             .setRequest(req)
                             .setAgentSession(agentSession)
                             .setClientSession(this.session)
+                            .setAction(agentParams.action)
+                            .setJvmId(agentParams.jvmId)
+                            .setSystemId(agentParams.systemId)
                             .build();
         String commId = agentId + req.getSequenceId();
         CommunicationsRegistry.add(commId, clientAgentComm);
         clientAgentComm.perform();
     }
 
-    private void sendErrorResponse(long sequence) {
-        WebSocketResponse resp = new WebSocketResponse(sequence, ResponseType.ERROR);
-        try {
-            synchronized(session) {
-                Basic remote = session.getBasicRemote();
-                remote.sendObject(resp);
-            }
-        } catch (IOException|EncodeException e) {
-            e.printStackTrace(); // cannot really do more. We've lost client connectivity
-        }
-    }
-
     @Override
     protected String getSequence() {
         return session.getPathParameters().get(PATH_PARAM_SEQ_ID);
@@ -127,11 +119,42 @@
         if (!user.isUserInRole(actionAllowedRole)) {
             return false;
         }
-        String jvm = session.getPathParameters().get(PATH_PARAM_JVM);
-        String jvmAllowedRole = CLIENT_GRANT_JVM_PREFIX + jvm;
-        if (!user.isUserInRole(jvmAllowedRole)) {
-            return false;
-        }
+        // TODO: Add roles checks for systemId/jvmId, both part of the
+        //       path parameters of the session.
         return true;
     }
+
+
+
+    private void sendErrorResponse(long sequence) {
+        WebSocketResponse resp = new WebSocketResponse(sequence, ResponseType.ERROR);
+        try {
+            synchronized(session) {
+                Basic remote = session.getBasicRemote();
+                remote.sendObject(resp);
+            }
+        } catch (IOException|EncodeException e) {
+            e.printStackTrace(); // cannot really do more. We've lost client connectivity
+        }
+    }
+
+    private AgentRequestParamsHolder getParamsFromSession(Session session) {
+        String action = session.getPathParameters().get(PATH_PARAM_ACTION);
+        String jvmId = session.getPathParameters().get(PATH_PARAM_JVM_ID);
+        String systemId = session.getPathParameters().get(PATH_PARAM_SYSTEM_ID);
+        return new AgentRequestParamsHolder(action, jvmId, systemId);
+    }
+
+    private static class AgentRequestParamsHolder {
+
+        private final String action;
+        private final String jvmId;
+        private final String systemId;
+
+        AgentRequestParamsHolder(String action, String jvmId, String systemId) {
+            this.action = action;
+            this.jvmId = jvmId;
+            this.systemId = systemId;
+        }
+    }
 }
--- a/services/commands/src/main/resources/agent.html	Fri Jul 28 18:48:35 2017 +0200
+++ b/services/commands/src/main/resources/agent.html	Thu Jul 27 18:33:26 2017 +0200
@@ -46,7 +46,7 @@
         Msg.fromRequest = (function(rawMsg) {
             var jsonObj = JSON.parse(rawMsg);
             Msg.sequence = jsonObj.sequence;
-            Msg.paramStr = jsonObj.payload.receiver;
+            Msg.payload = jsonObj.payload;
         });
 
         CmdChan.socket = null;
@@ -82,9 +82,9 @@
 
         CmdChan.initialize = function() {
             if (window.location.protocol == 'http:') {
-                CmdChan.connect('ws://' + window.location.host + '/commands/v1/systems/foo/agents/testAgent');
+                CmdChan.connect('ws://' + window.location.host + '/commands/v1/systems/ignored_system/agents/testAgent');
             } else {
-                CmdChan.connect('wss://' + window.location.host + '/commands/v1/systems/foo/agents/testAgent');
+                CmdChan.connect('wss://' + window.location.host + '/commands/v1/systems/ignored_system/agents/testAgent');
             }
         };
 
--- a/services/commands/src/main/resources/agent2.html	Fri Jul 28 18:48:35 2017 +0200
+++ b/services/commands/src/main/resources/agent2.html	Thu Jul 27 18:33:26 2017 +0200
@@ -46,7 +46,7 @@
         Msg.fromRequest = (function(rawMsg) {
             var jsonObj = JSON.parse(rawMsg);
             Msg.sequence = jsonObj.sequence;
-            Msg.paramStr = jsonObj.payload.receiver;
+            Msg.payload = jsonObj.payload;
         });
 
         CmdChan.socket = null;
@@ -82,9 +82,9 @@
 
         CmdChan.initialize = function() {
             if (window.location.protocol == 'http:') {
-                CmdChan.connect('ws://' + window.location.host + '/commands/v1/systems/foo/agents/otherAgent');
+                CmdChan.connect('ws://' + window.location.host + '/commands/v1/systems/ignored_system/agents/otherAgent');
             } else {
-                CmdChan.connect('wss://' + window.location.host + '/commands/v1/systems/foo/agents/otherAgent');
+                CmdChan.connect('wss://' + window.location.host + '/commands/v1/systems/ignored_system/agents/otherAgent');
             }
         };
 
--- a/services/commands/src/main/resources/index.html	Fri Jul 28 18:48:35 2017 +0200
+++ b/services/commands/src/main/resources/index.html	Thu Jul 27 18:33:26 2017 +0200
@@ -34,7 +34,7 @@
         CmdChan.socket = null;
         CmdChan.sequence = 1;
 
-        CmdChan.connect = (function(host) {
+        CmdChan.connect = (function(host, message) {
             if ('WebSocket' in window) {
                 CmdChan.socket = new WebSocket(host);
             } else if ('MozWebSocket' in window) {
@@ -46,15 +46,10 @@
 
             CmdChan.socket.onopen = function () {
                 Console.log('Info: WebSocket connection opened.');
-                document.getElementById('cmd-chan').onkeydown = function(event) {
-                    if (event.keyCode == 13) {
-                        CmdChan.sendMessage();
-                    }
-                };
+                CmdChan.socket.send(JSON.stringify(message));
             };
 
             CmdChan.socket.onclose = function () {
-                document.getElementById('cmd-chan').onkeydown = null;
                 Console.log('Info: WebSocket closed.');
             };
 
@@ -62,36 +57,41 @@
                 Console.log(message.data);
                 CmdChan.socket.close();
                 CmdChan.socket = null;
-                CmdChan.connect( CmdChan.getConnectUrl() );
             };
         });
 
-        CmdChan.getConnectUrl = (function() {
-            return CmdChan.url + CmdChan.sequence++;
-        });
-
         CmdChan.initialize = (function() {
-            if (window.location.protocol == 'http:') {
-                CmdChan.url = 'ws://' + window.location.host + '/commands/v1/actions/dump-heap/systems/foo/agents/testAgent/jvms/abc/sequence/';
-            } else {
-                CmdChan.url = 'wss://' + window.location.host + '/commands/v1/actions/dump-heap/systems/foo/agents/testAgent/jvms/abc/sequence/';
-            }
-            CmdChan.connect( CmdChan.getConnectUrl() );
+            document.getElementById('cmd-chan').onkeydown = function(event) {
+                if (event.keyCode == 13) {
+                    CmdChan.sendMessage();
+                }
+            };
+            document.getElementById('agent').onkeydown = function(event) {
+                if (event.keyCode == 13) {
+                    CmdChan.sendMessage();
+                }
+            };
         });
 
         CmdChan.sendMessage = (function() {
-            var message = document.getElementById('cmd-chan').value;
-            if (message != '') {
+            var action = document.getElementById('cmd-chan').value;
+            var agent = document.getElementById('agent').value;
+            if (action != '' && agent != '') {
                 var clientRequest = {};
+                var url = null;
                 clientRequest.type = 2;
                 clientRequest.payload = {};
-                clientRequest.payload.receiver = message;
-                CmdChan.socket.send(JSON.stringify(clientRequest));
+                if (window.location.protocol == 'http:') {
+                  url = 'ws://' + window.location.host;
+                } else {
+                  url = 'wss://' + window.location.host;
+                } 
+                url = url + '/commands/v1/actions/' + action + '/systems/ignored_system/agents/' + agent + '/jvms/ignored_jvm/sequence/' + CmdChan.sequence++;
+                CmdChan.connect( url, clientRequest );
                 document.getElementById('cmd-chan').value = '';
             }
         });
 
-        CmdChan.initialize();
 
         var Console = {};
 
@@ -126,11 +126,15 @@
     Thermostat Commands: HTML Client
     </p>
     <p>
-        <input type="text" placeholder="type the receiver name and press enter in order to send a message to the agent and trigger the receiver." id="cmd-chan" />
+        <input type="text" placeholder="type the action and press enter in order to send a message to the agent below." id="cmd-chan" />
+        <input type="text" placeholder="The agent to send messages to. Eg. 'testAgent' or 'otherAgent'" id="agent" />
     </p>
     <div id="console-container">
         <div id="console"/>
     </div>
+    <script>
+        CmdChan.initialize();
+    </script>
 </div>
 </body>
 </html>
--- a/services/commands/src/test/java/com/redhat/thermostat/gateway/service/commands/channel/coders/AgentRequestEncoderTest.java	Fri Jul 28 18:48:35 2017 +0200
+++ b/services/commands/src/test/java/com/redhat/thermostat/gateway/service/commands/channel/coders/AgentRequestEncoderTest.java	Thu Jul 27 18:33:26 2017 +0200
@@ -43,22 +43,37 @@
 
 import javax.websocket.EncodeException;
 
+import org.junit.Before;
 import org.junit.Test;
 
 import com.redhat.thermostat.gateway.service.commands.channel.model.AgentRequest;
 
 public class AgentRequestEncoderTest {
 
+    private String action;
+    private String jvmId;
+    private String systemId;
+
+    @Before
+    public void setup() {
+        action = "dump_heap";
+        jvmId = "foo-id";
+        systemId = "system-id";
+    }
+
     @Test
     public void canEncodeAgentRequest() throws EncodeException {
         SortedMap<String, String> params = new TreeMap<>();
         params.put("foo", "bar");
         params.put("zoo", "loo");
-        AgentRequest request = new AgentRequest(2130,params);
+        AgentRequest request = new AgentRequest(2130, action, systemId, jvmId, params);
 
         String expected = "{" +
                           "\"type\":" + request.getMessageType().intValue() + "," +
                           "\"sequence\":" + request.getSequenceId() + "," +
+                          "\"action\":\"" + request.getAction() + "\"," +
+                          "\"jvmId\":\"" + request.getJvmId() + "\"," +
+                          "\"systemId\":\"" + request.getSystemId() + "\"," +
                           "\"payload\":{" +
                           "\"foo\":\"bar\"," +
                           "\"zoo\":\"loo\"" +
@@ -73,11 +88,14 @@
         SortedMap<String, String> params = new TreeMap<>();
         params.put("foo", null);
         params.put("zoo", "loo");
-        AgentRequest request = new AgentRequest(2130,params);
+        AgentRequest request = new AgentRequest(2130, action, systemId, jvmId, params);
 
         String expected = "{" +
                           "\"type\":" + request.getMessageType().intValue() + "," +
                           "\"sequence\":" + request.getSequenceId() + "," +
+                          "\"action\":\"" + request.getAction() + "\"," +
+                          "\"jvmId\":\"" + request.getJvmId() + "\"," +
+                          "\"systemId\":\"" + request.getSystemId() + "\"," +
                           "\"payload\":{" +
                           "\"foo\":null," +
                           "\"zoo\":\"loo\"" +
@@ -89,11 +107,14 @@
 
     @Test
     public void canEncodeAgentRequestEmptyPayloadVal() throws EncodeException {
-        AgentRequest request = new AgentRequest(2130, new TreeMap<String, String>());
+        AgentRequest request = new AgentRequest(2130, action, systemId, jvmId, new TreeMap<String, String>());
 
         String expected = "{" +
                           "\"type\":" + request.getMessageType().intValue() + "," +
                           "\"sequence\":" + request.getSequenceId() + "," +
+                          "\"action\":\"" + request.getAction() + "\"," +
+                          "\"jvmId\":\"" + request.getJvmId() + "\"," +
+                          "\"systemId\":\"" + request.getSystemId() + "\"," +
                           "\"payload\":{}" +
                           "}";
         String encoded = new AgentRequestEncoder().encode(request);
--- a/services/commands/src/test/java/com/redhat/thermostat/gateway/service/commands/channel/coders/EncodeDecodeTest.java	Fri Jul 28 18:48:35 2017 +0200
+++ b/services/commands/src/test/java/com/redhat/thermostat/gateway/service/commands/channel/coders/EncodeDecodeTest.java	Thu Jul 27 18:33:26 2017 +0200
@@ -60,9 +60,12 @@
     @Test
     public void canEncodeDecodeAgentRequest() throws Exception {
         long sequence = 392;
+        String jvmId = "some-jvm-id";
+        String action = "dump_heap";
+        String systemId = "system-id";
         SortedMap<String, String> params = new TreeMap<>();
         params.put("foo", "bar");
-        AgentRequest request = new AgentRequest(sequence, params);
+        AgentRequest request = new AgentRequest(sequence, action, systemId, jvmId, params);
         String encoded = new AgentRequestEncoder().encode(request);
         Message result = new MessageDecoder().decode(encoded);
         assertNotNull(result);
@@ -70,6 +73,9 @@
         AgentRequest actual = (AgentRequest)result;
         assertEquals(sequence, actual.getSequenceId());
         assertEquals(params, actual.getParams());
+        assertEquals(action, actual.getAction());
+        assertEquals(jvmId, actual.getJvmId());
+        assertEquals(systemId, actual.getSystemId());
     }
 
     @Test
--- a/services/commands/src/test/java/com/redhat/thermostat/gateway/service/commands/channel/coders/MessageDecoderTest.java	Fri Jul 28 18:48:35 2017 +0200
+++ b/services/commands/src/test/java/com/redhat/thermostat/gateway/service/commands/channel/coders/MessageDecoderTest.java	Thu Jul 27 18:33:26 2017 +0200
@@ -60,7 +60,10 @@
                       "   \"payload\": {" +
                       "       \"param1\": \"value1\"," +
                       "       \"param2\": \"value2\"" +
-                      "   }" +
+                      "   }," +
+                      "   \"action\": \"dump_heap\"," +
+                      "   \"jvmId\": \"jvm_id\"," +
+                      "   \"systemId\": \"system_id\"" +
                       "}";
         MessageDecoder decoder = new MessageDecoder();
         Message actual = decoder.decode(json);
@@ -70,6 +73,9 @@
         assertEquals(2312, request.getSequenceId());
         assertEquals("value1", request.getParam("param1"));
         assertEquals("value2", request.getParam("param2"));
+        assertEquals("dump_heap", request.getAction());
+        assertEquals("jvm_id", request.getJvmId());
+        assertEquals("system_id", request.getSystemId());
     }
 
     @Test
@@ -80,7 +86,10 @@
                       "   \"payload\": {" +
                       "       \"param1\": \"value1 } {, RULE foo\nparam()\"," +
                       "       \"param2\": \"value2\"" +
-                      "   }" +
+                      "   }," +
+                      "   \"action\": \"dump_heap\"," +
+                      "   \"jvmId\": \"jvm_id\"," +
+                      "   \"systemId\": \"system_id\"" +
                       "}";
         MessageDecoder decoder = new MessageDecoder();
         Message actual = decoder.decode(json);
@@ -100,7 +109,10 @@
                       "   \"payload\": {" +
                       "       \"param1\": null," +
                       "       \"param2\": \"value2\"" +
-                      "   }" +
+                      "   }," +
+                      "   \"action\": \"dump_heap\"," +
+                      "   \"jvmId\": \"jvm_id\"," +
+                      "   \"systemId\": \"system_id\"" +
                       "}";
         MessageDecoder decoder = new MessageDecoder();
         Message actual = decoder.decode(json);
--- a/services/commands/src/test/java/com/redhat/thermostat/gateway/service/commands/channel/model/AgentRequestTest.java	Fri Jul 28 18:48:35 2017 +0200
+++ b/services/commands/src/test/java/com/redhat/thermostat/gateway/service/commands/channel/model/AgentRequestTest.java	Thu Jul 27 18:33:26 2017 +0200
@@ -50,7 +50,7 @@
         long sequence = 3321l;
         SortedMap<String, String> params = new TreeMap<>();
         params.put("foo-bar", "bar-baz");
-        AgentRequest request = new AgentRequest(sequence, params);
+        AgentRequest request = new AgentRequest(sequence, "some-action", "system-id", "jvm-id", params);
         assertEquals("sanity", sequence, request.getSequenceId());
         assertEquals("bar-baz", request.getParam("foo-bar"));
     }
--- a/services/commands/src/test/java/com/redhat/thermostat/gateway/service/commands/http/handlers/AuthBasicCoreServerTest.java	Fri Jul 28 18:48:35 2017 +0200
+++ b/services/commands/src/test/java/com/redhat/thermostat/gateway/service/commands/http/handlers/AuthBasicCoreServerTest.java	Thu Jul 27 18:33:26 2017 +0200
@@ -146,10 +146,10 @@
 
     protected static Map<String, String> getUserConfig() {
         Map<String, String> userConfig = new HashMap<>();
-        userConfig.put("foo-agent-user", "agent-pwd,thermostat-commands-provider-testAgent");
-        userConfig.put("bar-client-user", "client-pwd,thermostat-commands-grant-dump-heap,thermostat-commands-grant-jvm-abc");
+        userConfig.put("foo-agent-user", "agent-pwd,thermostat-commands-receiver-provider");
+        userConfig.put("bar-client-user", "client-pwd,thermostat-commands-grant-dump-heap");
         userConfig.put("insufficient-roles-agent", "agent-pwd");
-        userConfig.put("insufficient-roles-client", "client-pwd,thermostat-commands-grant-dump-heap");
+        userConfig.put("insufficient-roles-client", "client-pwd");
         return userConfig;
     };