Make script providers working on JDK 17 (#11322)

Closes #9945
This commit is contained in:
Marek Posolda 2022-05-27 12:28:50 +02:00 committed by GitHub
parent 27650ab816
commit eed944292b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 310 additions and 36 deletions

View file

@ -25,7 +25,7 @@ import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
import org.hibernate.jpa.boot.internal.PersistenceXmlParser; import org.hibernate.jpa.boot.internal.PersistenceXmlParser;
import org.hibernate.jpa.boot.spi.Bootstrap; import org.hibernate.jpa.boot.spi.Bootstrap;
import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider; import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
import org.keycloak.connections.jpa.entityprovider.ProxyClassLoader; import org.keycloak.utils.ProxyClassLoader;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;

View file

@ -118,6 +118,7 @@
<xmlsec.version>2.2.3</xmlsec.version> <xmlsec.version>2.2.3</xmlsec.version>
<glassfish.json.version>1.1.6</glassfish.json.version> <glassfish.json.version>1.1.6</glassfish.json.version>
<wildfly.common.version>1.6.0.Final</wildfly.common.version> <wildfly.common.version>1.6.0.Final</wildfly.common.version>
<nashorn.version>15.3</nashorn.version>
<ua-parser.version>1.5.2</ua-parser.version> <ua-parser.version>1.5.2</ua-parser.version>
<picketbox.version>5.0.3.Final</picketbox.version> <picketbox.version>5.0.3.Final</picketbox.version>
<google.guava.version>30.1-jre</google.guava.version> <google.guava.version>30.1-jre</google.guava.version>
@ -592,6 +593,11 @@
<artifactId>jakarta.json</artifactId> <artifactId>jakarta.json</artifactId>
<version>${glassfish.json.version}</version> <version>${glassfish.json.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.openjdk.nashorn</groupId>
<artifactId>nashorn-core</artifactId>
<version>${nashorn.version}</version>
</dependency>
<!-- Twitter --> <!-- Twitter -->
<dependency> <dependency>

View file

@ -19,13 +19,20 @@ package org.keycloak.quarkus.runtime.integration;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.platform.Platform; import org.keycloak.platform.Platform;
import org.keycloak.platform.PlatformProvider; import org.keycloak.platform.PlatformProvider;
import org.keycloak.quarkus.runtime.InitializationException; import org.keycloak.quarkus.runtime.InitializationException;
@ -159,4 +166,10 @@ public class QuarkusPlatform implements PlatformProvider {
private void reset() { private void reset() {
deferredExceptions.clear(); deferredExceptions.clear();
} }
@Override
public ClassLoader getScriptEngineClassLoader(Config.Scope scriptProviderConfig) {
// It is fine to return null assuming that nashorn and it's dependencies are included on the classpath (usually "providers" directory)
return null;
}
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright 2016 Red Hat, Inc. and/or its affiliates * Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags. * and other contributors as indicated by the @author tags.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,13 +13,18 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package org.keycloak.connections.jpa.entityprovider; package org.keycloak.utils;
import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set; import java.util.Set;
/** /**
@ -29,7 +34,8 @@ import java.util.Set;
* Effectively it forms a proxy to one or more other classloaders. * Effectively it forms a proxy to one or more other classloaders.
* *
* The way it works: * The way it works:
* - Get all (unique) classloaders from all provided classes * - Get list of classloaders, which will be used as "delegates" when loaded classes or resources.
* - Can be retrived from provided classloaders or alternatively from the provided classes where the "delegate classloaders" will be determined from the classloaders of given classes
* - For each class or resource that is 'requested': * - For each class or resource that is 'requested':
* - First try all provided classloaders and if we have a match, return that * - First try all provided classloaders and if we have a match, return that
* - If no match was found: proceed with 'normal' classloading in 'current classpath' scope * - If no match was found: proceed with 'normal' classloading in 'current classpath' scope
@ -41,17 +47,28 @@ public class ProxyClassLoader extends ClassLoader {
private Set<ClassLoader> classloaders; private Set<ClassLoader> classloaders;
public ProxyClassLoader(Collection<Class<?>> classes, ClassLoader parentClassLoader) { /**
super(parentClassLoader); * Init classloader with the list of given delegates
init(classes); * @param delegateClassLoaders
*/
public ProxyClassLoader(ClassLoader... delegateClassLoaders) {
if (delegateClassLoaders == null || delegateClassLoaders.length == 0) {
throw new IllegalStateException("At least one classloader to delegate must be provided");
}
classloaders = new LinkedHashSet<>();
classloaders.addAll(Arrays.asList(delegateClassLoaders));
} }
/**
* Get all unique classloaders from the provided classes to be used as "Delegate classloaders"
* @param classes
*/
public ProxyClassLoader(Collection<Class<?>> classes) { public ProxyClassLoader(Collection<Class<?>> classes) {
init(classes); init(classes);
} }
private void init(Collection<Class<?>> classes) { private void init(Collection<Class<?>> classes) {
classloaders = new HashSet<>(); classloaders = new LinkedHashSet<>();
for (Class<?> clazz : classes) { for (Class<?> clazz : classes) {
classloaders.add(clazz.getClassLoader()); classloaders.add(clazz.getClassLoader());
} }
@ -84,4 +101,33 @@ public class ProxyClassLoader extends ClassLoader {
return super.getResource(name); return super.getResource(name);
} }
@Override
public Enumeration<URL> getResources(String name) throws IOException {
final LinkedHashSet<URL> resourceUrls = new LinkedHashSet();
for (ClassLoader classloader : classloaders) {
Enumeration<URL> child = classloader.getResources(name);
while (child.hasMoreElements()) {
resourceUrls.add(child.nextElement());
}
}
return new Enumeration<URL>() {
final Iterator<URL> resourceUrlIterator = resourceUrls.iterator();
public boolean hasMoreElements() {
return this.resourceUrlIterator.hasNext();
}
public URL nextElement() {
return (URL)this.resourceUrlIterator.next();
}
};
}
@Override
public String toString() {
return "ProxyClassLoader: Delegates: " + classloaders;
}
} }

View file

@ -19,6 +19,8 @@ package org.keycloak.platform;
import java.io.File; import java.io.File;
import org.keycloak.Config;
public interface PlatformProvider { public interface PlatformProvider {
void onStartup(Runnable runnable); void onStartup(Runnable runnable);
@ -33,4 +35,17 @@ public interface PlatformProvider {
*/ */
File getTmpDirectory(); File getTmpDirectory();
/**
* Returns classloader to load script engine. Classloader should contain the implementation of {@link javax.script.ScriptEngineFactory}
* and it's definition inside META-INF/services of the jar file(s), which will be provided by this classloader.
*
* This method can return null and in that case, the default Keycloak services classloader will be used for load script engine. Note that java versions earlier than 15 always contain
* the "nashorn" script engine by default on the classpath (it is part of the Java platform itself) and hence for them it is always fine to return null (unless you want to override default engine)
*
* @param scriptProviderConfig Configuration scope of the "default" provider of "scripting" SPI. It can contain some config properties for the classloader (EG. file path)
* @return classloader or null
*/
ClassLoader getScriptEngineClassLoader(Config.Scope scriptProviderConfig);
} }

View file

@ -24,7 +24,9 @@ import javax.script.ScriptEngineManager;
import javax.script.ScriptException; import javax.script.ScriptException;
import org.keycloak.models.ScriptModel; import org.keycloak.models.ScriptModel;
import org.keycloak.platform.Platform;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.utils.ProxyClassLoader;
/** /**
* A {@link ScriptingProvider} that uses a {@link ScriptEngineManager} to evaluate scripts with a {@link ScriptEngine}. * A {@link ScriptingProvider} that uses a {@link ScriptEngineManager} to evaluate scripts with a {@link ScriptEngine}.
@ -125,8 +127,17 @@ public class DefaultScriptingProvider implements ScriptingProvider {
private ScriptEngine lookupScriptEngineFor(ScriptModel script) { private ScriptEngine lookupScriptEngineFor(ScriptModel script) {
ClassLoader cl = Thread.currentThread().getContextClassLoader(); ClassLoader cl = Thread.currentThread().getContextClassLoader();
try { try {
Thread.currentThread().setContextClassLoader(DefaultScriptingProvider.class.getClassLoader()); ClassLoader scriptClassLoader = Platform.getPlatform().getScriptEngineClassLoader(factory.getConfig());
return factory.getScriptEngineManager().getEngineByMimeType(script.getMimeType());
// Also need to use classloader of keycloak services itself to be able to use keycloak classes in the scripts
if (scriptClassLoader != null) {
scriptClassLoader = new ProxyClassLoader(scriptClassLoader, DefaultScriptingProvider.class.getClassLoader());
} else {
scriptClassLoader = DefaultScriptingProvider.class.getClassLoader();
}
Thread.currentThread().setContextClassLoader(scriptClassLoader);
return new ScriptEngineManager().getEngineByMimeType(script.getMimeType());
} }
finally { finally {
Thread.currentThread().setContextClassLoader(cl); Thread.currentThread().setContextClassLoader(cl);

View file

@ -23,10 +23,8 @@ import org.jboss.logging.Logger;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.protocol.oidc.OIDCWellKnownProviderFactory;
import javax.script.ScriptEngine; import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
/** /**
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a> * @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
@ -35,30 +33,28 @@ public class DefaultScriptingProviderFactory implements ScriptingProviderFactory
private static final Logger logger = Logger.getLogger(DefaultScriptingProviderFactory.class); private static final Logger logger = Logger.getLogger(DefaultScriptingProviderFactory.class);
static final String ID = "script-based-auth"; static final String ID = "default";
private ScriptEngineManager scriptEngineManager;
private boolean enableScriptEngineCache; private boolean enableScriptEngineCache;
// Key is mime-type. Value is engine for the particular mime-type. Cache can be used when the scriptEngine can be shared across multiple threads / requests (which is the case for nashorn) // Key is mime-type. Value is engine for the particular mime-type. Cache can be used when the scriptEngine can be shared across multiple threads / requests (which is the case for nashorn)
private Map<String, ScriptEngine> scriptEngineCache; private Map<String, ScriptEngine> scriptEngineCache;
private Config.Scope config;
@Override @Override
public ScriptingProvider create(KeycloakSession session) { public ScriptingProvider create(KeycloakSession session) {
lazyInit();
return new DefaultScriptingProvider(this); return new DefaultScriptingProvider(this);
} }
@Override @Override
public void init(Config.Scope config) { public void init(Config.Scope config) {
this.config = config;
this.enableScriptEngineCache = config.getBoolean("enable-script-engine-cache", true); this.enableScriptEngineCache = config.getBoolean("enable-script-engine-cache", true);
logger.debugf("Enable script engine cache: %b", this.enableScriptEngineCache); logger.debugf("Enable script engine cache: %b", this.enableScriptEngineCache);
if (enableScriptEngineCache) {
scriptEngineCache = new ConcurrentHashMap<>();
} }
ScriptEngineManager getScriptEngineManager() {
return scriptEngineManager;
} }
boolean isEnableScriptEngineCache() { boolean isEnableScriptEngineCache() {
@ -69,6 +65,10 @@ public class DefaultScriptingProviderFactory implements ScriptingProviderFactory
return scriptEngineCache; return scriptEngineCache;
} }
Config.Scope getConfig() {
return config;
}
@Override @Override
public void postInit(KeycloakSessionFactory factory) { public void postInit(KeycloakSessionFactory factory) {
//NOOP //NOOP
@ -84,17 +84,4 @@ public class DefaultScriptingProviderFactory implements ScriptingProviderFactory
return ID; return ID;
} }
private void lazyInit() {
if (scriptEngineManager == null) {
synchronized (this) {
if (scriptEngineManager == null) {
scriptEngineManager = new ScriptEngineManager();
if (enableScriptEngineCache) {
scriptEngineCache = new ConcurrentHashMap<>();
}
}
}
}
}
} }

View file

@ -257,4 +257,18 @@
</filterset> </filterset>
</copy> </copy>
</target> </target>
<!-- Needed on Java 15 and later -->
<target name="deploy-nashorn-module">
<copy todir="${cli.tmp.dir}">
<resources>
<file file="${common.resources}/jboss-cli/deploy-nashorn-module.cli"/>
</resources>
<filterset>
<filter token="NASHORN_JAR" value="${project.build.directory}/nashorn/nashorn-core-${nashorn.version}.jar"/>
</filterset>
</copy>
<echo>Nashorn module deployed</echo>
</target>
</project> </project>

View file

@ -0,0 +1,5 @@
echo *** Installing nashorn-core module ***
module add --module-root-dir=../modules/system/layers/keycloak/ \
--name=org.openjdk.nashorn.nashorn-core \
--resources=@NASHORN_JAR@ \
--dependencies=asm.asm,jdk.dynalink

View file

@ -732,5 +732,62 @@
</properties> </properties>
</profile> </profile>
<!-- Nashorn script engine needs to be manually added for the new Java versions as it is not part of the JDK anymore -->
<profile>
<id>jdk15</id>
<activation>
<jdk>[15,)</jdk>
</activation>
<build>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-nashorn-module</id>
<phase>generate-resources</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.openjdk.nashorn</groupId>
<artifactId>nashorn-core</artifactId>
<version>${nashorn.version}</version>
<type>jar</type>
</artifactItem>
</artifactItems>
<outputDirectory>${project.build.directory}/nashorn</outputDirectory>
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<id>deploy-nashorn-module</id>
<phase>generate-resources</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<ant antfile="${common.resources}/ant/configure.xml" target="deploy-nashorn-module" />
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
</profile>
</profiles> </profiles>
</project> </project>

View file

@ -262,5 +262,43 @@
<auth.server.quarkus.cluster.config>ha</auth.server.quarkus.cluster.config> <auth.server.quarkus.cluster.config>ha</auth.server.quarkus.cluster.config>
</properties> </properties>
</profile> </profile>
<!-- Nashorn script engine needs to be manually added for the new Java versions as it is not part of the JDK anymore -->
<profile>
<id>jdk15</id>
<activation>
<jdk>[15,)</jdk>
</activation>
<dependencies>
<dependency>
<groupId>org.openjdk.nashorn</groupId>
<artifactId>nashorn-core</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies-quarkus</id>
<phase>generate-resources</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${auth.server.home}/providers</outputDirectory>
<includeArtifactIds>nashorn-core,asm,asm-util,asm-commons</includeArtifactIds>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles> </profiles>
</project> </project>

View file

@ -254,6 +254,12 @@
<artifactId>mssql-jdbc</artifactId> <artifactId>mssql-jdbc</artifactId>
<version>${mssql.driver.version}</version> <version>${mssql.driver.version}</version>
</dependency> </dependency>
<!-- Nashorn -->
<dependency>
<groupId>org.openjdk.nashorn</groupId>
<artifactId>nashorn-core</artifactId>
</dependency>
</dependencies> </dependencies>

View file

@ -22,6 +22,7 @@ import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.platform.PlatformProvider; import org.keycloak.platform.PlatformProvider;
public class TestPlatform implements PlatformProvider { public class TestPlatform implements PlatformProvider {
@ -70,4 +71,10 @@ public class TestPlatform implements PlatformProvider {
} }
return tmpDir; return tmpDir;
} }
@Override
public ClassLoader getScriptEngineClassLoader(Config.Scope scriptProviderConfig) {
// It is fine to return null as nashorn should be automatically included on the classpath of testsuite utils
return null;
}
} }

View file

@ -20,6 +20,11 @@ package org.keycloak.provider.wildfly;
import java.io.File; import java.io.File;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.modules.Module;
import org.jboss.modules.ModuleIdentifier;
import org.jboss.modules.ModuleLoadException;
import org.keycloak.Config;
import org.keycloak.common.util.Environment;
import org.keycloak.platform.PlatformProvider; import org.keycloak.platform.PlatformProvider;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
@ -27,6 +32,9 @@ public class WildflyPlatform implements PlatformProvider {
private static final Logger log = Logger.getLogger(WildflyPlatform.class); private static final Logger log = Logger.getLogger(WildflyPlatform.class);
// In this module, the attempt to load script engine will be done by default
private static final String DEFAULT_SCRIPT_ENGINE_MODULE = "org.openjdk.nashorn.nashorn-core";
Runnable shutdownHook; Runnable shutdownHook;
private File tmpDir; private File tmpDir;
@ -74,4 +82,25 @@ public class WildflyPlatform implements PlatformProvider {
} }
return tmpDir; return tmpDir;
} }
@Override
public ClassLoader getScriptEngineClassLoader(Config.Scope scriptProviderConfig) {
String engineModule = scriptProviderConfig.get("script-engine-module");
if (engineModule == null) {
engineModule = DEFAULT_SCRIPT_ENGINE_MODULE;
}
try {
Module module = Module.getContextModuleLoader().loadModule(ModuleIdentifier.fromString(engineModule));
log.infof("Found script engine module '%s'", engineModule);
return module.getClassLoader();
} catch (ModuleLoadException mle) {
if (WildflyUtil.getJavaVersion() >= 15) {
log.warnf("Cannot find script engine in the JBoss module '%s'. Please add JavaScript engine to the specified JBoss Module or make sure it is available on the classpath", engineModule);
} else {
log.debugf("Cannot find script engine in the JBoss module '%s'. Will fallback to the default script engine", engineModule);
}
return null;
}
}
} }

View file

@ -0,0 +1,40 @@
/*
* Copyright 2022 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.provider.wildfly;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class WildflyUtil {
/**
* @return major version of java runtime like 8, 11 or 17
*/
public static int getJavaVersion() {
String version = System.getProperty("java.version");
if (version.startsWith("1.")) {
version = version.substring(2, 3);
} else {
int dot = version.indexOf(".");
if (dot != -1) version = version.substring(0, dot);
}
return Integer.parseInt(version);
}
}