Skip to content

Commit

Permalink
Make script providers working on JDK 17 (keycloak#11322)
Browse files Browse the repository at this point in the history
  • Loading branch information
mposolda authored May 27, 2022
1 parent 27650ab commit eed9442
Show file tree
Hide file tree
Showing 15 changed files with 310 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import org.hibernate.jpa.boot.internal.PersistenceXmlParser;
import org.hibernate.jpa.boot.spi.Bootstrap;
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 javax.persistence.EntityManager;
Expand Down
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
<xmlsec.version>2.2.3</xmlsec.version>
<glassfish.json.version>1.1.6</glassfish.json.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>
<picketbox.version>5.0.3.Final</picketbox.version>
<google.guava.version>30.1-jre</google.guava.version>
Expand Down Expand Up @@ -592,6 +593,11 @@
<artifactId>jakarta.json</artifactId>
<version>${glassfish.json.version}</version>
</dependency>
<dependency>
<groupId>org.openjdk.nashorn</groupId>
<artifactId>nashorn-core</artifactId>
<version>${nashorn.version}</version>
</dependency>

<!-- Twitter -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,20 @@

import java.io.File;
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.Path;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;

import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.platform.Platform;
import org.keycloak.platform.PlatformProvider;
import org.keycloak.quarkus.runtime.InitializationException;
Expand Down Expand Up @@ -159,4 +166,10 @@ public File getTmpDirectory() {
private void reset() {
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;
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* Licensed under the Apache License, Version 2.0 (the "License");
Expand All @@ -13,13 +13,18 @@
* 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.connections.jpa.entityprovider;
package org.keycloak.utils;

import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;

/**
Expand All @@ -29,7 +34,8 @@
* Effectively it forms a proxy to one or more other classloaders.
*
* 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':
* - 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
Expand All @@ -41,17 +47,28 @@ public class ProxyClassLoader extends ClassLoader {

private Set<ClassLoader> classloaders;

public ProxyClassLoader(Collection<Class<?>> classes, ClassLoader parentClassLoader) {
super(parentClassLoader);
init(classes);
/**
* Init classloader with the list of given delegates
* @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) {
init(classes);
}

private void init(Collection<Class<?>> classes) {
classloaders = new HashSet<>();
classloaders = new LinkedHashSet<>();
for (Class<?> clazz : classes) {
classloaders.add(clazz.getClassLoader());
}
Expand Down Expand Up @@ -84,4 +101,33 @@ public URL getResource(String 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;
}
}
15 changes: 15 additions & 0 deletions services/src/main/java/org/keycloak/platform/PlatformProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

import java.io.File;

import org.keycloak.Config;

public interface PlatformProvider {

void onStartup(Runnable runnable);
Expand All @@ -33,4 +35,17 @@ public interface PlatformProvider {
*/
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);

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
import javax.script.ScriptException;

import org.keycloak.models.ScriptModel;
import org.keycloak.platform.Platform;
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}.
Expand Down Expand Up @@ -125,8 +127,17 @@ private ScriptEngine getPreparedScriptEngine(ScriptModel script) {
private ScriptEngine lookupScriptEngineFor(ScriptModel script) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(DefaultScriptingProvider.class.getClassLoader());
return factory.getScriptEngineManager().getEngineByMimeType(script.getMimeType());
ClassLoader scriptClassLoader = Platform.getPlatform().getScriptEngineClassLoader(factory.getConfig());

// 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 {
Thread.currentThread().setContextClassLoader(cl);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,8 @@
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.protocol.oidc.OIDCWellKnownProviderFactory;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

/**
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
Expand All @@ -35,30 +33,28 @@ public class DefaultScriptingProviderFactory implements ScriptingProviderFactory

private static final Logger logger = Logger.getLogger(DefaultScriptingProviderFactory.class);

static final String ID = "script-based-auth";

private ScriptEngineManager scriptEngineManager;
static final String ID = "default";

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)
private Map<String, ScriptEngine> scriptEngineCache;

private Config.Scope config;

@Override
public ScriptingProvider create(KeycloakSession session) {
lazyInit();

return new DefaultScriptingProvider(this);
}

@Override
public void init(Config.Scope config) {
this.config = config;
this.enableScriptEngineCache = config.getBoolean("enable-script-engine-cache", true);
logger.debugf("Enable script engine cache: %b", this.enableScriptEngineCache);
}

ScriptEngineManager getScriptEngineManager() {
return scriptEngineManager;
if (enableScriptEngineCache) {
scriptEngineCache = new ConcurrentHashMap<>();
}
}

boolean isEnableScriptEngineCache() {
Expand All @@ -69,6 +65,10 @@ Map<String, ScriptEngine> getScriptEngineCache() {
return scriptEngineCache;
}

Config.Scope getConfig() {
return config;
}

@Override
public void postInit(KeycloakSessionFactory factory) {
//NOOP
Expand All @@ -84,17 +84,4 @@ public String getId() {
return ID;
}

private void lazyInit() {
if (scriptEngineManager == null) {
synchronized (this) {
if (scriptEngineManager == null) {
scriptEngineManager = new ScriptEngineManager();
if (enableScriptEngineCache) {
scriptEngineCache = new ConcurrentHashMap<>();
}
}
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -257,4 +257,18 @@
</filterset>
</copy>
</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>
Original file line number Diff line number Diff line change
@@ -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
57 changes: 57 additions & 0 deletions testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -732,5 +732,62 @@
</properties>
</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>
</project>
Loading

0 comments on commit eed9442

Please sign in to comment.