changeset 2755:d717e6133812

Add Keycloak authentication support in commands plugin. Reviewed-by: jkang Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-September/024887.html
author Severin Gehwolf <sgehwolf@redhat.com>
date Thu, 07 Sep 2017 18:56:23 +0200
parents 6018bc5f92b3
children d844cfb19af1
files agent/core/src/main/java/com/redhat/thermostat/agent/config/AgentStartupConfiguration.java agent/core/src/main/java/com/redhat/thermostat/agent/config/AuthenticationProviderConfig.java agent/core/src/main/java/com/redhat/thermostat/agent/internal/AgentApplication.java plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/CommandsBackend.java plugins/commands/agent/src/test/java/com/redhat/thermostat/commands/agent/internal/CommandsBackendTest.java
diffstat 5 files changed, 139 insertions(+), 33 deletions(-) [+]
line wrap: on
line diff
--- a/agent/core/src/main/java/com/redhat/thermostat/agent/config/AgentStartupConfiguration.java	Tue Sep 12 12:19:00 2017 +0200
+++ b/agent/core/src/main/java/com/redhat/thermostat/agent/config/AgentStartupConfiguration.java	Thu Sep 07 18:56:23 2017 +0200
@@ -36,7 +36,7 @@
 
 package com.redhat.thermostat.agent.config;
 
-public class AgentStartupConfiguration {
+public class AgentStartupConfiguration implements AuthenticationProviderConfig {
 
     private boolean purge;
     private long startTime;
@@ -46,15 +46,15 @@
     private String keycloakRealm;
     private String keycloakClient;
     private boolean basicAuthEnabled;
-    
+
     AgentStartupConfiguration() {
     }
-    
+
     // TODO: that should be a friend, we only want the Service to set this value
     public void setStartTime(long startTime) {
         this.startTime = startTime;
     }
-    
+
     public long getStartTime() {
         return startTime;
     }
@@ -62,7 +62,7 @@
     void setPurge(boolean purge) {
         this.purge = purge;
     }
-    
+
     public boolean purge() {
         return purge;
     }
@@ -83,6 +83,7 @@
         this.keycloakClient = keycloakClient;
     }
 
+    @Override
     public boolean isKeycloakEnabled() {
         return keycloakEnabled;
     }
@@ -99,6 +100,7 @@
         this.keycloakRealm = keycloakRealm;
     }
 
+    @Override
     public boolean isBasicAuthEnabled() {
         return basicAuthEnabled;
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/agent/core/src/main/java/com/redhat/thermostat/agent/config/AuthenticationProviderConfig.java	Thu Sep 07 18:56:23 2017 +0200
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2012-2017 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.agent.config;
+
+import com.redhat.thermostat.annotations.Service;
+
+/**
+ *
+ * Configuration service informing plugins which authentication
+ * scheme is to be used.
+ *
+ */
+@Service
+public interface AuthenticationProviderConfig {
+
+    /**
+     *
+     * @return {@code true} if and only if Keycloak is being used as the
+     *         authentication provider.
+     */
+    boolean isKeycloakEnabled();
+
+    /**
+     *
+     * @return {@code true} if basic authentication should be used. If
+     *         {@link AuthenticationProviderConfig#isKeycloakEnabled()} also
+     *         returns true Keycloak authentication should be preferred.
+     */
+    boolean isBasicAuthEnabled();
+
+}
--- a/agent/core/src/main/java/com/redhat/thermostat/agent/internal/AgentApplication.java	Tue Sep 12 12:19:00 2017 +0200
+++ b/agent/core/src/main/java/com/redhat/thermostat/agent/internal/AgentApplication.java	Thu Sep 07 18:56:23 2017 +0200
@@ -40,9 +40,6 @@
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
-import com.redhat.thermostat.agent.dao.AgentInfoDAO;
-import com.redhat.thermostat.agent.dao.BackendInfoDAO;
-
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
@@ -54,6 +51,9 @@
 import com.redhat.thermostat.agent.config.AgentConfigsUtils;
 import com.redhat.thermostat.agent.config.AgentOptionParser;
 import com.redhat.thermostat.agent.config.AgentStartupConfiguration;
+import com.redhat.thermostat.agent.config.AuthenticationProviderConfig;
+import com.redhat.thermostat.agent.dao.AgentInfoDAO;
+import com.redhat.thermostat.agent.dao.BackendInfoDAO;
 import com.redhat.thermostat.backend.BackendRegistry;
 import com.redhat.thermostat.backend.BackendService;
 import com.redhat.thermostat.common.ExitStatus;
@@ -70,6 +70,7 @@
 import com.redhat.thermostat.shared.config.CommonPaths;
 import com.redhat.thermostat.shared.config.InvalidConfigurationException;
 import com.redhat.thermostat.storage.core.WriterID;
+
 import sun.misc.Signal;
 import sun.misc.SignalHandler;
 
@@ -110,7 +111,7 @@
     private BackendInfoDAO backendInfoDAO;
     @Reference
     private CommonPaths commonPaths;
-    
+
     private CommandRegistry reg;
 
     private CountDownLatch shutdownLatch;
@@ -174,6 +175,7 @@
     @Override
     public void run(CommandContext ctx) throws CommandException {
         configuration = configurationCreator.create(commonPaths);
+        context.registerService(AuthenticationProviderConfig.class, configuration, null);
 
         parseArguments(ctx.getArguments());
         if (!parser.isHelp()) {
--- a/plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/CommandsBackend.java	Tue Sep 12 12:19:00 2017 +0200
+++ b/plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/CommandsBackend.java	Thu Sep 07 18:56:23 2017 +0200
@@ -52,6 +52,10 @@
 import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
 import org.osgi.framework.BundleContext;
 
+import com.redhat.thermostat.agent.config.AuthenticationProviderConfig;
+import com.redhat.thermostat.agent.http.RequestFailedException;
+import com.redhat.thermostat.agent.keycloak.KeycloakAccessToken;
+import com.redhat.thermostat.agent.keycloak.KeycloakAccessTokenService;
 import com.redhat.thermostat.backend.Backend;
 import com.redhat.thermostat.backend.BaseBackend;
 import com.redhat.thermostat.commands.agent.internal.socket.AgentSocketOnMessageCallback;
@@ -93,7 +97,7 @@
 
     @Reference
     private SystemID systemId;
-    
+
     @Reference
     private WriterID agentId;
 
@@ -102,14 +106,20 @@
 
     @Reference
     private ConfigurationInfoSource commandInfo;
-    
+
     @Reference
     private SSLConfiguration sslConfig;
 
+    @Reference
+    private KeycloakAccessTokenService keycloakTokenService;
+
+    @Reference
+    private AuthenticationProviderConfig authConfig;
+
     public CommandsBackend() {
         this(new WsClientCreator(), new CredentialsCreator(), new ConfigCreator(), new CountDownLatch(1));
     }
-    
+
     // For testing purposes
     CommandsBackend(WsClientCreator creator,
                     CredentialsCreator credsCreator,
@@ -181,7 +191,7 @@
     }
 
     /**
-     * 
+     *
      * @return {@code true} if and only if connection was successfully made
      */
     private boolean connectWsClient() {
@@ -189,15 +199,22 @@
         try {
             URI microserviceURI = config.getGatewayURL();
             String agent = agentId.getWriterID();
-            String sysId = systemId.getSystemID(); 
+            String sysId = systemId.getSystemID();
             String cmdUriPath = String.format(ENDPOINT_FORMAT, sysId, agent);
             URI agentUri = microserviceURI.resolve(cmdUriPath);
             AgentSocketOnMessageCallback onMsgCallback = new AgentSocketOnMessageCallback(receiverReg);
             CmdChannelAgentSocket agentSocket = new CmdChannelAgentSocket(
                     onMsgCallback, socketConnectLatch, agent);
             ClientUpgradeRequest agentRequest = new ClientUpgradeRequest();
-            agentRequest.setHeader(HttpHeader.AUTHORIZATION.asString(),
-                    getBasicAuthHeaderValue());
+            if (authConfig.isKeycloakEnabled()) {
+                agentRequest.setHeader(HttpHeader.AUTHORIZATION.asString(),
+                        getKeycloakAuthHeaderValue());
+            } else if (authConfig.isBasicAuthEnabled()) {
+                agentRequest.setHeader(HttpHeader.AUTHORIZATION.asString(),
+                        getBasicAuthHeaderValue());
+            } else {
+                logger.warning("No authentication scheme configured. Connection will likely fail.");
+            }
             wsClient.connect(agentSocket, agentUri, agentRequest);
             logger.fine("WebSocket connect initiated.");
             expired = !socketConnectLatch.await(10, TimeUnit.SECONDS);
@@ -215,6 +232,17 @@
         }
     }
 
+    private String getKeycloakAuthHeaderValue() {
+        String bearerToken = UNKNOWN_CREDS;
+        try {
+            KeycloakAccessToken keycloakToken = keycloakTokenService.getAccessToken();
+            bearerToken = keycloakToken.getAccessToken();
+        } catch (RequestFailedException e) {
+            logger.warning("Failed to get keycloak access token from auth provider.");
+        }
+        return "Bearer " + bearerToken;
+    }
+
     String getBasicAuthHeaderValue() {
         String username = creds.getUsername();
         char[] pwdChar = creds.getPassword();
@@ -226,40 +254,45 @@
             String pwd = new String(pwdChar);
             userpassword = username + ":" + pwd;
         }
-        
+
         @SuppressWarnings("restriction")
         String encodedAuthorization = new sun.misc.BASE64Encoder()
                 .encode(userpassword.getBytes());
         return "Basic " + encodedAuthorization;
     }
-    
+
     // DS bind method
     protected void bindPaths(CommonPaths paths) {
         this.paths = paths;
     }
-    
+
     // DS bind method
     protected void bindAgentId(WriterID agentId) {
         this.agentId = agentId;
     }
-    
+
     // DS bind method
     protected void bindSystemId(SystemID systemId) {
         this.systemId = systemId;
     }
-    
+
+    // DS bind method
+    protected void bindAuthConfig(AuthenticationProviderConfig authConfig) {
+        this.authConfig = authConfig;
+    }
+
     static class WsClientCreator {
         WebSocketClientFacade createClient(SSLConfiguration sslConfig) {
             return new WebSocketClientFacadeImpl(sslConfig);
         }
     }
-    
+
     static class CredentialsCreator {
         StorageCredentials create(CommonPaths paths) {
             return new FileStorageCredentials(paths.getUserAgentAuthConfigFile());
         }
     }
-    
+
     static class ConfigCreator {
         PluginConfiguration createConfig(ConfigurationInfoSource source) {
             return new PluginConfiguration(source, PLUGIN_ID);
--- a/plugins/commands/agent/src/test/java/com/redhat/thermostat/commands/agent/internal/CommandsBackendTest.java	Tue Sep 12 12:19:00 2017 +0200
+++ b/plugins/commands/agent/src/test/java/com/redhat/thermostat/commands/agent/internal/CommandsBackendTest.java	Thu Sep 07 18:56:23 2017 +0200
@@ -58,6 +58,7 @@
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.Version;
 
+import com.redhat.thermostat.agent.config.AuthenticationProviderConfig;
 import com.redhat.thermostat.commands.agent.internal.CommandsBackend.ConfigCreator;
 import com.redhat.thermostat.commands.agent.internal.CommandsBackend.CredentialsCreator;
 import com.redhat.thermostat.commands.agent.internal.CommandsBackend.WsClientCreator;
@@ -80,7 +81,7 @@
     private BundleContext bundleContext;
     private WebSocketClientFacade client;
     private CountDownLatch socketConnect;
-    
+
     @Before
     public void setup() throws IOException {
         socketConnect = new CountDownLatch(1);
@@ -107,25 +108,28 @@
         when(sId.getSystemID()).thenReturn(SYSTEM_ID);
         backend.bindAgentId(wId);
         backend.bindSystemId(sId);
+        AuthenticationProviderConfig authConfig = mock(AuthenticationProviderConfig.class);
+        when(authConfig.isBasicAuthEnabled()).thenReturn(true);
+        backend.bindAuthConfig(authConfig);
     }
-    
+
     @Test
     public void testGetBasicAuthHeaderValueNoCreds() {
         doUnknownCredsTest(backend);
     }
-    
+
     @Test
     public void testGetBasicAuthHeaderValueWithNoUsername() {
         when(creds.getPassword()).thenReturn(new char[] { 'a', 'b', 'c' });
         doUnknownCredsTest(backend);
     }
-    
+
     @Test
     public void testGetBasicAuthHeaderValueWithNoPassword() {
         when(creds.getUsername()).thenReturn("foo-user");
         doUnknownCredsTest(backend);
     }
-    
+
     @Test
     public void canGetBasicAuthHeaderValueWithUsernamePwd() {
         String password = "foo";
@@ -135,13 +139,13 @@
         String expected = base64EncodedHeader(username + ":" + password);
         assertEquals(expected, backend.getBasicAuthHeaderValue());
     }
-    
+
     @Test
     public void testComponentActivated() {
         // setup invokes it. only do verification here
         verify(client).start();
     }
-    
+
     @Test
     public void testActivateSuccess() throws IOException {
         String password = "foo";
@@ -164,17 +168,17 @@
         assertEquals(expectedHeader, actualHeader);
         assertTrue("Expected backend to be active", backend.isActive());
     }
-    
+
     @Test
     public void testActivateFail() throws IOException {
         // set up for failure
         doThrow(IOException.class).when(client).connect(any(CmdChannelAgentSocket.class), any(URI.class), any(ClientUpgradeRequest.class));
-        
+
         boolean success = backend.activate();
         assertFalse("Expected unsuccessful activation", success);
         assertFalse(backend.isActive());
     }
-    
+
     @Test
     public void testDeactivate() throws IOException {
         // release connect latch