# HG changeset patch # User Severin Gehwolf # Date 1504621778 -7200 # Node ID 1428b14f642ff92654cf8f2c416c2dc3dad8222f # Parent ed634d83e742e3eeb43951bf2aae6ad8accd9b0b [commands] Implement Keycloak support. Reviewed-by: jkang Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-September/024853.html diff -r ed634d83e742 -r 1428b14f642f services/commands/src/main/java/com/redhat/thermostat/gateway/service/commands/channel/endpoints/RealmAuthorizerConfigurator.java --- 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 + * 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 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 map = serviceConfig.asMap(); return Boolean.parseBoolean((String)map.get(configKey.name())); diff -r ed634d83e742 -r 1428b14f642f services/commands/src/main/resources/agent.html --- 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. + * + * 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 roles; + private Map 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 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.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 setupKeycloakPrincipal() { + @SuppressWarnings("unchecked") + KeycloakPrincipal 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 realmPingActions = new HashSet<>(); + realmPingActions.add("commands"); + assertEquals(realmPingActions, authorizer.getRealmsWithAction("ping")); + Set readableRealms = new HashSet<>(); + readableRealms.add("foo-realm"); + assertEquals(readableRealms, authorizer.getReadableRealms()); + } + + private void setupConfig(ServiceConfiguration.ConfigurationKey key, Boolean value) { + Map configImpl = new HashMap<>(); + configImpl.put(key.name(), value.toString()); + when(serviceConfig.asMap()).thenReturn(configImpl); + } + + private void setupPrincipal(Principal p) { + when(request.getUserPrincipal()).thenReturn(p); + } +}