KEYCLOAK-17693 add config for loading custom IdMapper class

This commit is contained in:
Yang Xie 2020-04-07 14:37:43 +02:00 committed by Marek Posolda
parent a0b01b6ef4
commit d8cb279bc4
6 changed files with 218 additions and 56 deletions

View file

@ -43,6 +43,9 @@ import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -58,6 +61,8 @@ public class KeycloakOIDCFilter implements Filter {
public static final String SKIP_PATTERN_PARAM = "keycloak.config.skipPattern";
public static final String ID_MAPPER_PARAM = "keycloak.config.idMapper";
public static final String CONFIG_RESOLVER_PARAM = "keycloak.config.resolver";
public static final String CONFIG_FILE_PARAM = "keycloak.config.file";
@ -94,6 +99,28 @@ public class KeycloakOIDCFilter implements Filter {
skipPattern = Pattern.compile(skipPatternDefinition, Pattern.DOTALL);
}
String idMapperClassName = filterConfig.getInitParameter(ID_MAPPER_PARAM);
if (idMapperClassName != null) {
try {
final Class<?> idMapperClass = getClass().getClassLoader().loadClass(idMapperClassName);
final Constructor<?> idMapperConstructor = idMapperClass.getDeclaredConstructor();
Object idMapperInstance = null;
// for KEYCLOAK-13745 test
if (idMapperConstructor.getModifiers() == Modifier.PRIVATE) {
idMapperInstance = idMapperClass.getMethod("getInstance").invoke(null);
} else {
idMapperInstance = idMapperConstructor.newInstance();
}
if(idMapperInstance instanceof SessionIdMapper) {
this.idMapper = (SessionIdMapper) idMapperInstance;
} else {
log.log(Level.WARNING, "SessionIdMapper class {0} is not instance of org.keycloak.adapters.spi.SessionIdMapper", idMapperClassName);
}
} catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
log.log(Level.WARNING, "SessionIdMapper class could not be instanced", e);
}
}
if (definedconfigResolver != null) {
deploymentContext = new AdapterDeploymentContext(definedconfigResolver);
log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", definedconfigResolver.getClass());
@ -160,25 +187,7 @@ public class KeycloakOIDCFilter implements Filter {
return;
}
PreAuthActionsHandler preActions = new PreAuthActionsHandler(new UserSessionManagement() {
@Override
public void logoutAll() {
if (idMapper != null) {
idMapper.clear();
}
}
@Override
public void logoutHttpSessions(List<String> ids) {
log.fine("**************** logoutHttpSessions");
//System.err.println("**************** logoutHttpSessions");
for (String id : ids) {
log.finest("removed idMapper: " + id);
idMapper.removeSession(id);
}
}
}, deploymentContext, facade);
PreAuthActionsHandler preActions = new PreAuthActionsHandler(new IdMapperUserSessionManagement(), deploymentContext, facade);
if (preActions.handleRequest()) {
//System.err.println("**************** preActions.handleRequest happened!");
@ -241,4 +250,24 @@ public class KeycloakOIDCFilter implements Filter {
public void destroy() {
}
private class IdMapperUserSessionManagement implements UserSessionManagement {
@Override
public void logoutAll() {
if (idMapper != null) {
idMapper.clear();
}
}
@Override
public void logoutHttpSessions(List<String> ids) {
log.fine("**************** logoutHttpSessions");
//System.err.println("**************** logoutHttpSessions");
for (String id : ids) {
log.finest("removed idMapper: " + id);
idMapper.removeSession(id);
}
}
}
}

View file

@ -0,0 +1,56 @@
package org.keycloak.testsuite.adapter.spi;
import org.keycloak.adapters.spi.SessionIdMapper;
import java.util.HashSet;
import java.util.Set;
public class TestSessionIdMapper implements SessionIdMapper {
private static final TestSessionIdMapper SINGLETON = new TestSessionIdMapper();
private static Set<String> whoCalled = new HashSet<>();
private TestSessionIdMapper() {
}
public boolean isCalledBy(String className) {
return whoCalled.contains(className);
}
public static TestSessionIdMapper getInstance() {
StackTraceElement[] ste = (new Throwable()).getStackTrace();
for (int i = 0; i < ste.length; i++) {
whoCalled.add(ste[i].getClassName());
}
return SINGLETON;
}
@Override
public boolean hasSession(String id) {
return false;
}
@Override
public void clear() {
whoCalled.clear();
}
@Override
public Set<String> getUserSessions(String principal) {
return null;
}
@Override
public String getSessionFromSSO(String sso) {
return null;
}
@Override
public void map(String sso, String principal, String session) {
}
@Override
public void removeSession(String session) {
}
}

View file

@ -250,23 +250,8 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor {
}
appendChildInDocument(webXmlDoc, "web-app", filter);
// Limitation that all deployments of annotated class use same skipPattern. Refactor if something more flexible is needed (would require more tricky web.xml parsing though...)
String skipPattern = testClass.getAnnotation(UseServletFilter.class).skipPattern();
if (skipPattern != null && !skipPattern.isEmpty()) {
Element initParam = webXmlDoc.createElement("init-param");
Element paramName = webXmlDoc.createElement("param-name");
paramName.setTextContent(KeycloakOIDCFilter.SKIP_PATTERN_PARAM);
Element paramValue = webXmlDoc.createElement("param-value");
paramValue.setTextContent(skipPattern);
initParam.appendChild(paramName);
initParam.appendChild(paramValue);
filter.appendChild(initParam);
}
addInitParam(webXmlDoc, filter, KeycloakOIDCFilter.SKIP_PATTERN_PARAM, testClass.getAnnotation(UseServletFilter.class).skipPattern());
addInitParam(webXmlDoc, filter, KeycloakOIDCFilter.ID_MAPPER_PARAM, testClass.getAnnotation(UseServletFilter.class).idMapper());
appendChildInDocument(webXmlDoc, "web-app", filter);
@ -298,7 +283,25 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor {
archive.add(new StringAsset((documentToString(webXmlDoc))), WEBXML_PATH);
}
private void addInitParam(Document webXmlDoc, Element filter, String initParamName, String initParamValue) {
// Limitation that all deployments of annotated class use same skipPattern. Refactor if something more flexible is needed (would require more tricky web.xml parsing though...)
if (initParamValue != null && !initParamValue.isEmpty()) {
Element initParam = webXmlDoc.createElement("init-param");
Element paramName = webXmlDoc.createElement("param-name");
paramName.setTextContent(initParamName);
Element paramValue = webXmlDoc.createElement("param-value");
paramValue.setTextContent(initParamValue);
initParam.appendChild(paramName);
initParam.appendChild(paramValue);
filter.appendChild(initParam);
}
}
private String getKeycloakResolverClass(Document doc) {
try {
XPathFactory factory = XPathFactory.newInstance();

View file

@ -0,0 +1,67 @@
/*
* Copyright 2018 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.adapter.servlet;
import org.junit.Test;
import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
import org.keycloak.testsuite.adapter.page.CustomerPortal;
import org.keycloak.testsuite.adapter.spi.TestSessionIdMapper;
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
import org.keycloak.testsuite.auth.page.login.OIDCLogin;
import org.keycloak.testsuite.util.JavascriptBrowser;
import org.keycloak.testsuite.utils.annotation.UseServletFilter;
import org.keycloak.testsuite.utils.arquillian.ContainerConstants;
import org.openqa.selenium.WebDriver;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.jboss.arquillian.graphene.page.Page;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import static org.junit.Assert.assertTrue;
@AppServerContainer(ContainerConstants.APP_SERVER_UNDERTOW)
@AppServerContainer(ContainerConstants.APP_SERVER_WILDFLY)
@AppServerContainer(ContainerConstants.APP_SERVER_WILDFLY_DEPRECATED)
@AppServerContainer(ContainerConstants.APP_SERVER_EAP)
@AppServerContainer(ContainerConstants.APP_SERVER_EAP6)
@AppServerContainer(ContainerConstants.APP_SERVER_EAP71)
@UseServletFilter(filterName = "oidc-filter", filterClass = "org.keycloak.adapters.servlet.KeycloakOIDCFilter",
filterDependency = "org.keycloak:keycloak-servlet-filter-adapter", skipPattern = "/error.html",
idMapper = "org.keycloak.testsuite.adapter.spi.TestSessionIdMapper")
public class DemoFilterServletAdapterTestForCustomizedIdMapper extends AbstractServletsAdapterTest {
@Drone
@JavascriptBrowser
protected WebDriver jsDriver;
@Page
protected CustomerPortal customerPortal;
@Deployment(name = CustomerPortal.DEPLOYMENT_NAME)
protected static WebArchive customerPortal() {
return servletDeployment(CustomerPortal.DEPLOYMENT_NAME, CustomerServlet.class, ErrorServlet.class, ServletTestUtils.class);
}
// KEYCLOAK-13745
@Test
public void testCustomizedSessionIdMapper() {
customerPortal.navigateTo();
TestSessionIdMapper singleton = TestSessionIdMapper.getInstance();
assertTrue(singleton.isCalledBy(getClass().getAnnotation(UseServletFilter.class).filterClass()));
singleton.clear();
}
}

View file

@ -23,4 +23,5 @@ public @interface UseServletFilter {
String filterPattern() default "/*";
String dispatcherType() default "";
String skipPattern() default "";
String idMapper() default "";
}

View file

@ -91,8 +91,8 @@ public class DeploymentArchiveProcessorUtils {
}
//We need to add filter declaration to web.xml
log.info("Adding filter to " + testClass.getAnnotation(UseServletFilter.class).filterClass() +
" with mapping " + testClass.getAnnotation(UseServletFilter.class).filterPattern() +
log.info("Adding filter to " + testClass.getAnnotation(UseServletFilter.class).filterClass() +
" with mapping " + testClass.getAnnotation(UseServletFilter.class).filterPattern() +
" for " + archive.getName());
Element filter = webXmlDoc.createElement("filter");
@ -124,21 +124,9 @@ public class DeploymentArchiveProcessorUtils {
// Limitation that all deployments of annotated class use same skipPattern. Refactor if
// something more flexible is needed (would require more tricky web.xml parsing though...)
String skipPattern = testClass.getAnnotation(UseServletFilter.class).skipPattern();
if (skipPattern != null && !skipPattern.isEmpty()) {
Element initParam = webXmlDoc.createElement("init-param");
addInitParam(webXmlDoc, filter, KeycloakOIDCFilter.SKIP_PATTERN_PARAM, testClass.getAnnotation(UseServletFilter.class).skipPattern());
addInitParam(webXmlDoc, filter, KeycloakOIDCFilter.ID_MAPPER_PARAM, testClass.getAnnotation(UseServletFilter.class).idMapper());
Element paramName = webXmlDoc.createElement("param-name");
paramName.setTextContent(KeycloakOIDCFilter.SKIP_PATTERN_PARAM);
Element paramValue = webXmlDoc.createElement("param-value");
paramValue.setTextContent(skipPattern);
initParam.appendChild(paramName);
initParam.appendChild(paramValue);
filter.appendChild(initParam);
}
IOUtil.appendChildInDocument(webXmlDoc, "web-app", filter);
@ -165,10 +153,28 @@ public class DeploymentArchiveProcessorUtils {
IOUtil.removeElementsFromDoc(webXmlDoc, "web-app", "security-constraint");
IOUtil.removeElementsFromDoc(webXmlDoc, "web-app", "login-config");
IOUtil.removeElementsFromDoc(webXmlDoc, "web-app", "security-role");
archive.add(new StringAsset((IOUtil.documentToString(webXmlDoc))), WEBXML_PATH);
}
private static void addInitParam(Document webXmlDoc, Element filter, String initParamName, String initParamValue) {
// Limitation that all deployments of annotated class use same skipPattern. Refactor if something more flexible is needed (would require more tricky web.xml parsing though...)
if (initParamValue != null && !initParamValue.isEmpty()) {
Element initParam = webXmlDoc.createElement("init-param");
Element paramName = webXmlDoc.createElement("param-name");
paramName.setTextContent(initParamName);
Element paramValue = webXmlDoc.createElement("param-value");
paramValue.setTextContent(initParamValue);
initParam.appendChild(paramName);
initParam.appendChild(paramValue);
filter.appendChild(initParam);
}
}
public static String getKeycloakResolverClass(Document doc) {
try {
XPathFactory factory = XPathFactory.newInstance();
@ -188,7 +194,7 @@ public class DeploymentArchiveProcessorUtils {
public static void addFilterDependencies(Archive<?> archive, TestClass testClass) {
log.info("Adding filter dependencies to " + archive.getName());
String dependency = testClass.getAnnotation(UseServletFilter.class).filterDependency();
((WebArchive) archive).addAsLibraries(KeycloakDependenciesResolver.resolveDependencies((dependency + ":" + System.getProperty("project.version"))));