Mercurial > hg > thermostat-ng > web-gateway
changeset 252:1428b14f642f
[commands] Implement Keycloak support.
Reviewed-by: jkang
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-September/024853.html
author | Severin Gehwolf <sgehwolf@redhat.com> |
---|---|
date | Tue, 05 Sep 2017 16:29:38 +0200 |
parents | ed634d83e742 |
children | 3df209d1fa6a |
files | services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/channel/endpoints/RealmAuthorizerConfigurator.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/endpoints/RealmAuthorizerConfiguratorTest.java |
diffstat | 5 files changed, 267 insertions(+), 12 deletions(-) [+] |
line wrap: on
line diff
--- a/services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/channel/endpoints/RealmAuthorizerConfigurator.java Thu Sep 07 09:07:07 2017 -0400 +++ b/services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/channel/endpoints/RealmAuthorizerConfigurator.java Tue Sep 05 16:29:38 2017 +0200 @@ -37,35 +37,65 @@ package com.redhat.thermostat.gateway.service.commands.channel.endpoints; import java.util.Map; +import java.util.logging.Logger; import javax.websocket.HandshakeResponse; import javax.websocket.server.HandshakeRequest; import javax.websocket.server.ServerEndpointConfig; import javax.websocket.server.ServerEndpointConfig.Configurator; +import org.keycloak.KeycloakPrincipal; +import org.keycloak.KeycloakSecurityContext; + import com.redhat.thermostat.gateway.common.core.auth.RealmAuthorizer; import com.redhat.thermostat.gateway.common.core.auth.basic.BasicRealmAuthorizer; import com.redhat.thermostat.gateway.common.core.auth.basic.BasicWebUser; +import com.redhat.thermostat.gateway.common.core.auth.keycloak.KeycloakRealmAuthorizer; +import com.redhat.thermostat.gateway.common.core.auth.keycloak.KeycloakRealmAuthorizer.CannotReduceRealmsException; import com.redhat.thermostat.gateway.common.core.config.Configuration; import com.redhat.thermostat.gateway.common.core.config.ServiceConfiguration; import com.redhat.thermostat.gateway.common.core.servlet.GlobalConstants; +import com.redhat.thermostat.gateway.common.util.LoggingUtil; public class RealmAuthorizerConfigurator extends Configurator { + private static final Logger LOGGER = LoggingUtil.getLogger(RealmAuthorizerConfigurator.class); + @Override public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) { Configuration serviceConfig = (Configuration)config.getUserProperties().get(GlobalConstants.SERVICE_CONFIG_KEY); - RealmAuthorizer realmAuthorizer; - if (isBasicAuthEnabled(serviceConfig)) { + RealmAuthorizer realmAuthorizer = RealmAuthorizer.DENY_ALL_AUTHORIZER; // Default to deny all + if (isKeycloakAuthEnabled(serviceConfig)) { + /* The keycloak authentication handler (a.k.a its Jetty adapter) looks for + * credentials/tokens in the following way: + * + * 1. Look at the HTTP headers => Authorization: Bearer <token> + * 2. Look if a query parameter 'access_token' exists and is a valid token + * 3. Use Basic auth credentials so as to get a token, which is then used + * + * In the web-sockets case we expect for the user to have specified the token + * via the query parameter. Thus, once we arrive here we have a valid principal + * which in turn gives us access to the keycloak security context. We use that, + * in turn, to set up the appropriate realm authorizer. + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + KeycloakPrincipal<KeycloakSecurityContext> keycloakPrincipal = (KeycloakPrincipal)request.getUserPrincipal(); + if (keycloakPrincipal != null) { + KeycloakSecurityContext secContext = keycloakPrincipal.getKeycloakSecurityContext(); + try { + realmAuthorizer = new KeycloakRealmAuthorizer(secContext); + LOGGER.config("Using Keycloak realm authorizer"); + } catch (CannotReduceRealmsException e) { + LOGGER.warning(e.getMessage()); + } + } + } else if (isBasicAuthEnabled(serviceConfig)) { BasicWebUser user = (BasicWebUser)request.getUserPrincipal(); - if (user == null) { - realmAuthorizer = RealmAuthorizer.DENY_ALL_AUTHORIZER; - } else { + if (user != null) { realmAuthorizer = new BasicRealmAuthorizer(user); + LOGGER.config("Using BASIC auth realm authorizer"); } - } else { - realmAuthorizer = RealmAuthorizer.DENY_ALL_AUTHORIZER; } config.getUserProperties().put(RealmAuthorizer.class.getName(), realmAuthorizer); } @@ -74,6 +104,10 @@ return isSet(serviceConfig, ServiceConfiguration.ConfigurationKey.SECURITY_BASIC); } + private boolean isKeycloakAuthEnabled(Configuration serviceConfig) { + return isSet(serviceConfig, ServiceConfiguration.ConfigurationKey.SECURITY_KEYCLOAK); + } + private boolean isSet(Configuration serviceConfig, ServiceConfiguration.ConfigurationKey configKey) { Map<String, Object> map = serviceConfig.asMap(); return Boolean.parseBoolean((String)map.get(configKey.name()));
--- a/services/commands/src/main/resources/agent.html Thu Sep 07 09:07:07 2017 -0400 +++ b/services/commands/src/main/resources/agent.html Tue Sep 05 16:29:38 2017 +0200 @@ -51,6 +51,18 @@ CmdChan.socket = null; + CmdChan.getTokenParam = (function(variable) { + var query = window.location.search.substring(1); + var vars = query.split("&"); + for (var i=0;i<vars.length;i++) { + var pair = vars[i].split("="); + if (pair[0] == variable) { + return pair[1]; + } + } + return null; + }); + CmdChan.connect = (function(host) { if ('WebSocket' in window) { CmdChan.socket = new WebSocket(host); @@ -95,10 +107,15 @@ }); CmdChan.initialize = function() { + var query_string = ""; + var token = CmdChan.getTokenParam("access_token"); + if (token != null) { + query_string = "?access_token=" + token; + } if (window.location.protocol == 'http:') { - CmdChan.connect('ws://' + window.location.host + '/commands/v1/systems/ignored_system/agents/testAgent'); + CmdChan.connect('ws://' + window.location.host + '/commands/v1/systems/ignored_system/agents/testAgent' + query_string); } else { - CmdChan.connect('wss://' + window.location.host + '/commands/v1/systems/ignored_system/agents/testAgent'); + CmdChan.connect('wss://' + window.location.host + '/commands/v1/systems/ignored_system/agents/testAgent' + query_string); } };
--- a/services/commands/src/main/resources/agent2.html Thu Sep 07 09:07:07 2017 -0400 +++ b/services/commands/src/main/resources/agent2.html Tue Sep 05 16:29:38 2017 +0200 @@ -51,6 +51,18 @@ CmdChan.socket = null; + CmdChan.getTokenParam = (function(variable) { + var query = window.location.search.substring(1); + var vars = query.split("&"); + for (var i=0;i<vars.length;i++) { + var pair = vars[i].split("="); + if (pair[0] == variable) { + return pair[1]; + } + } + return null; + }); + CmdChan.connect = (function(host) { if ('WebSocket' in window) { CmdChan.socket = new WebSocket(host); @@ -95,10 +107,15 @@ }); CmdChan.initialize = function() { + var query_string = ""; + var token = CmdChan.getTokenParam("access_token"); + if (token != null) { + query_string = "?access_token=" + token; + } if (window.location.protocol == 'http:') { - CmdChan.connect('ws://' + window.location.host + '/commands/v1/systems/ignored_system/agents/otherAgent'); + CmdChan.connect('ws://' + window.location.host + '/commands/v1/systems/ignored_system/agents/otherAgent' + query_string); } else { - CmdChan.connect('wss://' + window.location.host + '/commands/v1/systems/ignored_system/agents/otherAgent'); + CmdChan.connect('wss://' + window.location.host + '/commands/v1/systems/ignored_system/agents/otherAgent' + query_string); } };
--- a/services/commands/src/main/resources/index.html Thu Sep 07 09:07:07 2017 -0400 +++ b/services/commands/src/main/resources/index.html Tue Sep 05 16:29:38 2017 +0200 @@ -34,6 +34,18 @@ CmdChan.socket = null; CmdChan.sequence = 1; + CmdChan.getTokenParam = (function(variable) { + var query = window.location.search.substring(1); + var vars = query.split("&"); + for (var i=0;i<vars.length;i++) { + var pair = vars[i].split("="); + if (pair[0] == variable) { + return pair[1]; + } + } + return null; + }); + CmdChan.connect = (function(host, message) { if ('WebSocket' in window) { CmdChan.socket = new WebSocket(host); @@ -100,7 +112,12 @@ } else { url = 'wss://' + window.location.host; } - url = url + '/commands/v1/actions/' + action + '/systems/ignored_system/agents/' + agent + '/jvms/ignored_jvm/sequence/' + CmdChan.sequence++; + var query_string = ""; + var token = CmdChan.getTokenParam("access_token"); + if (token != null) { + query_string = "?access_token=" + token; + } + url = url + '/commands/v1/actions/' + action + '/systems/ignored_system/agents/' + agent + '/jvms/ignored_jvm/sequence/' + CmdChan.sequence++ + query_string; CmdChan.connect( url, clientRequest ); document.getElementById('cmd-chan').value = ''; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/services/commands/src/test/java/com/redhat/thermostat/gateway/service/commands/channel/endpoints/RealmAuthorizerConfiguratorTest.java Tue Sep 05 16:29:38 2017 +0200 @@ -0,0 +1,170 @@ +/* + * 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.gateway.service.commands.channel.endpoints; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.security.Principal; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.websocket.HandshakeResponse; +import javax.websocket.server.HandshakeRequest; +import javax.websocket.server.ServerEndpointConfig; + +import org.junit.Before; +import org.junit.Test; +import org.keycloak.KeycloakPrincipal; +import org.keycloak.KeycloakSecurityContext; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.AccessToken.Access; + +import com.redhat.thermostat.gateway.common.core.auth.RealmAuthorizer; +import com.redhat.thermostat.gateway.common.core.auth.basic.BasicRealmAuthorizer; +import com.redhat.thermostat.gateway.common.core.auth.basic.BasicWebUser; +import com.redhat.thermostat.gateway.common.core.auth.keycloak.KeycloakRealmAuthorizer; +import com.redhat.thermostat.gateway.common.core.config.Configuration; +import com.redhat.thermostat.gateway.common.core.config.ServiceConfiguration; +import com.redhat.thermostat.gateway.common.core.servlet.GlobalConstants; + +public class RealmAuthorizerConfiguratorTest { + + private ServerEndpointConfig config; + private HandshakeRequest request; + private HandshakeResponse response; + private Configuration serviceConfig; + private Set<String> roles; + private Map<String, Object> userProps; + + @Before + public void setup() { + serviceConfig = mock(Configuration.class); + userProps = new HashMap<>(); + userProps.put(GlobalConstants.SERVICE_CONFIG_KEY, serviceConfig); + config = mock(ServerEndpointConfig.class); + when(config.getUserProperties()).thenReturn(userProps); + request = mock(HandshakeRequest.class); + response = mock(HandshakeResponse.class); + roles = new HashSet<>(); + roles.add("ping-commands"); + roles.add("ignored"); + roles.add("r-foo-realm"); + } + + @Test + public void testBasicAuthAuthorizer() { + setupConfig(ServiceConfiguration.ConfigurationKey.SECURITY_BASIC, Boolean.TRUE); + BasicWebUser userPrincipal = new BasicWebUser("ignored", new char[] {}, roles); + setupPrincipal(userPrincipal); + + RealmAuthorizerConfigurator configurator = new RealmAuthorizerConfigurator(); + configurator.modifyHandshake(config, request, response); + + RealmAuthorizer authorizer = (RealmAuthorizer)userProps.get(RealmAuthorizer.class.getName()); + assertNotNull(authorizer); + assertTrue(authorizer instanceof BasicRealmAuthorizer); + verifyRoles(authorizer); + } + + @Test + public void testKeycloakAuthAuthorizer() { + setupConfig(ServiceConfiguration.ConfigurationKey.SECURITY_KEYCLOAK, Boolean.TRUE); + KeycloakPrincipal<KeycloakSecurityContext> keycloakPrincipal = setupKeycloakPrincipal(); + setupPrincipal(keycloakPrincipal); + + RealmAuthorizerConfigurator configurator = new RealmAuthorizerConfigurator(); + configurator.modifyHandshake(config, request, response); + + RealmAuthorizer authorizer = (RealmAuthorizer)userProps.get(RealmAuthorizer.class.getName()); + assertNotNull(authorizer); + assertTrue("Actual: " + authorizer.getClass(), authorizer instanceof KeycloakRealmAuthorizer); + verifyRoles(authorizer); + } + + @Test + public void testDenyAllForUnknown() { + // empty config + when(serviceConfig.asMap()).thenReturn(Collections.<String, Object>emptyMap()); + + RealmAuthorizerConfigurator configurator = new RealmAuthorizerConfigurator(); + configurator.modifyHandshake(config, request, response); + + RealmAuthorizer authorizer = (RealmAuthorizer)userProps.get(RealmAuthorizer.class.getName()); + assertNotNull(authorizer); + assertSame(RealmAuthorizer.DENY_ALL_AUTHORIZER, authorizer); + } + + private KeycloakPrincipal<KeycloakSecurityContext> setupKeycloakPrincipal() { + @SuppressWarnings("unchecked") + KeycloakPrincipal<KeycloakSecurityContext> p = mock(KeycloakPrincipal.class); + KeycloakSecurityContext secContext = mock(KeycloakSecurityContext.class); + AccessToken accessToken = mock(AccessToken.class); + Access access = mock(Access.class); + when(access.getRoles()).thenReturn(roles); + when(accessToken.getRealmAccess()).thenReturn(access); + when(secContext.getToken()).thenReturn(accessToken); + when(p.getKeycloakSecurityContext()).thenReturn(secContext); + return p; + } + + private void verifyRoles(RealmAuthorizer authorizer) { + Set<String> realmPingActions = new HashSet<>(); + realmPingActions.add("commands"); + assertEquals(realmPingActions, authorizer.getRealmsWithAction("ping")); + Set<String> readableRealms = new HashSet<>(); + readableRealms.add("foo-realm"); + assertEquals(readableRealms, authorizer.getReadableRealms()); + } + + private void setupConfig(ServiceConfiguration.ConfigurationKey key, Boolean value) { + Map<String, Object> configImpl = new HashMap<>(); + configImpl.put(key.name(), value.toString()); + when(serviceConfig.asMap()).thenReturn(configImpl); + } + + private void setupPrincipal(Principal p) { + when(request.getUserPrincipal()).thenReturn(p); + } +}