Merge pull request #1492 from mposolda/master
Simplify embedded server bootstrap in LDAP and Kerberos examples
This commit is contained in:
commit
1c7dd0a533
16 changed files with 123 additions and 320 deletions
|
@ -43,21 +43,13 @@ is in your `/etc/hosts` before other records for the 127.0.0.1 host to avoid iss
|
|||
for credential delegation example, as application needs to forward Kerberos ticket and authenticate with it against LDAP server.
|
||||
See [this file](https://github.com/keycloak/keycloak/blob/master/testsuite/integration/src/test/resources/kerberos/test-krb5.conf) for inspiration.
|
||||
|
||||
**6)** Run ApacheDS based Kerberos server . The [LDAP Example](../ldap) contains the embedded server, which you can run for example
|
||||
with these commands (assuming you're in `kerberos` directory with this example)
|
||||
**6)** Run ApacheDS based LDAP server. You can run the command like this (assuming you're in the "kerberos" directory with this example):
|
||||
|
||||
```
|
||||
cd ../ldap
|
||||
mvn clean install
|
||||
cd ..
|
||||
java -jar ldap/embedded-ldap/target/embedded-ldap.jar kerberos
|
||||
mvn exec:java -Pkerberos
|
||||
```
|
||||
|
||||
This will also automatically import the LDIF from `users.ldif` of kerberos example into the LDAP server. If you want to import your own LDIF file,
|
||||
you can add the system property `ldap.ldif` with the path of the LDIF file to the command. For example:
|
||||
```
|
||||
java -jar -Dldap.ldif=/tmp/my-users.ldif ldap/embedded-ldap/target/embedded-ldap.jar kerberos
|
||||
```
|
||||
This will also automatically import the LDIF from `kerberos-example-users.ldif` of kerberos example into the LDAP server. Replace with your own LDIF file if you want different users.
|
||||
|
||||
A bit more details about embedded Kerberos server in [testsuite README](https://github.com/keycloak/keycloak/blob/master/misc/Testsuite.md#kerberos-server).
|
||||
|
||||
|
|
|
@ -40,6 +40,11 @@
|
|||
<artifactId>keycloak-adapter-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-util-embedded-ldap</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
@ -62,4 +67,28 @@
|
|||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>kerberos</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<mainClass>org.keycloak.util.ldap.KerberosEmbeddedServer</mainClass>
|
||||
<classpathScope>test</classpathScope>
|
||||
<systemProperties>
|
||||
<systemProperty>
|
||||
<key>ldap.ldif</key>
|
||||
<value>kerberos-example-users.ldif</value>
|
||||
</systemProperty>
|
||||
</systemProperties>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
</project>
|
||||
|
|
|
@ -13,24 +13,20 @@ The example application is then showing all the basic claims of current user tog
|
|||
|
||||
Detailed steps how to make the example working:
|
||||
|
||||
**1)** Build and deploy this sample's WAR file in `ldap-app/target/ldap-portal.war` . For this example, deploy on the same server that is running the Keycloak Server,
|
||||
**1)** Build and deploy this sample's WAR file in `target/ldap-portal.war` . For this example, deploy on the same server that is running the Keycloak Server,
|
||||
although this is not required for real world scenarios.
|
||||
|
||||
|
||||
**2)** Run ApacheDS based LDAP server. You can run the command like this (assuming you're in the "ldap" directory with this example):
|
||||
|
||||
```
|
||||
java -jar embedded-ldap/target/embedded-ldap.jar ldap
|
||||
mvn exec:java -Pldap
|
||||
```
|
||||
|
||||
This will also automatically import the LDIF from `ldap-app/users.ldif` into the LDAP server. If you want to import your own LDIF file,
|
||||
you can add the system property `ldap.ldif` with the path of the LDIF file to the command. For example:
|
||||
```
|
||||
java -jar -Dldap.ldif=/tmp/my-users.ldif embedded-ldap/target/embedded-ldap.jar ldap
|
||||
```
|
||||
This will also automatically import the LDIF from `ldap-example-users.ldif` into the LDAP server. Replace with your own LDIF file if you want different users.
|
||||
|
||||
|
||||
**3)** Run Keycloak server and import `ldap-app/ldaprealm.json` into it through admin console. This contains the realm with preconfigured LDAP federation provider and LDAP mappers
|
||||
**3)** Run Keycloak server and import `ldaprealm.json` into it through admin console. This contains the realm with preconfigured LDAP federation provider and LDAP mappers
|
||||
and protocol mappers. Note that there are not any roles or users in this file. All of users, roles and role mappings data will be imported automatically from LDAP.
|
||||
|
||||
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
<assembly>
|
||||
<id>embedded-ldap</id>
|
||||
|
||||
<formats>
|
||||
<format>dir</format>
|
||||
</formats>
|
||||
|
||||
<includeBaseDirectory>false</includeBaseDirectory>
|
||||
|
||||
<dependencySets>
|
||||
<dependencySet>
|
||||
<unpack>false</unpack>
|
||||
<useTransitiveDependencies>true</useTransitiveDependencies>
|
||||
<useTransitiveFiltering>true</useTransitiveFiltering>
|
||||
<includes>
|
||||
<include>org.keycloak:keycloak-util-embedded-ldap</include>
|
||||
<include>org.slf4j:slf4j-log4j12</include>
|
||||
</includes>
|
||||
</dependencySet>
|
||||
</dependencySets>
|
||||
</assembly>
|
|
@ -1,77 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-examples-ldap-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.4.0.Final-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.keycloak.example.demo</groupId>
|
||||
<artifactId>keycloak-examples-embedded-ldap</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>LDAP Demo Application</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-util-embedded-ldap</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>embedded-ldap</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>assemble</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<descriptors>
|
||||
<descriptor>assembly.xml</descriptor>
|
||||
</descriptors>
|
||||
<outputDirectory>
|
||||
target
|
||||
</outputDirectory>
|
||||
<workDirectory>
|
||||
target/assembly/work
|
||||
</workDirectory>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>org.keycloak.example.ldap.embedded.EmbeddedLDAPLauncher</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>${maven.compiler.source}</source>
|
||||
<target>${maven.compiler.target}</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
|
@ -1,129 +0,0 @@
|
|||
package org.keycloak.example.ldap.embedded;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Allows to run embedded ApacheDS LDAP or Kerberos server
|
||||
*
|
||||
* It is supposed to be executed from JAR file. For example:
|
||||
* java -jar target/embedded-ldap.jar ldap
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class EmbeddedLDAPLauncher {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
String arg = args.length == 0 ? null : args[0];
|
||||
if (arg == null) {
|
||||
System.err.println("Missing argument: either 'kerberos', 'ldap' or 'keytabCreator' must be passed as argument");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
String clazz = null;
|
||||
File home = getHome();
|
||||
Properties defaultProperties = new Properties();
|
||||
if (arg.equalsIgnoreCase("ldap")) {
|
||||
|
||||
clazz = "org.keycloak.util.ldap.LDAPEmbeddedServer";
|
||||
File ldapLdif = file(home, "..", "ldap-app", "users.ldif");
|
||||
defaultProperties.put("ldap.ldif", ldapLdif.getAbsolutePath());
|
||||
} else if (arg.equalsIgnoreCase("kerberos")) {
|
||||
|
||||
clazz = "org.keycloak.util.ldap.KerberosEmbeddedServer";
|
||||
File kerberosLdif = file(home, "..", "..", "kerberos", "users.ldif");
|
||||
defaultProperties.put("ldap.ldif", kerberosLdif.getAbsolutePath());
|
||||
} else if (arg.equalsIgnoreCase("keytabCreator")) {
|
||||
|
||||
clazz = "org.keycloak.util.ldap.KerberosKeytabCreator";
|
||||
} else {
|
||||
|
||||
System.err.println("Invalid argument: '" + arg + "' . Either 'kerberos', 'ldap' or 'keytabCreator' must be passed as argument");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
// Remove first argument
|
||||
String[] newArgs = new String[args.length - 1];
|
||||
for (int i=0 ; i<(args.length - 1) ; i++) {
|
||||
newArgs[i] = args[i + 1];
|
||||
}
|
||||
|
||||
System.out.println("Executing " + clazz);
|
||||
runClass(clazz, newArgs, defaultProperties);
|
||||
}
|
||||
|
||||
|
||||
private static void runClass(String className, String[] args, Properties defaultProperties) throws Exception {
|
||||
File home = getHome();
|
||||
File lib = file(home, "target", "embedded-ldap");
|
||||
|
||||
if (!lib.exists()) {
|
||||
System.err.println("Could not find lib directory: " + lib.toString());
|
||||
System.exit(1);
|
||||
} else {
|
||||
System.out.println("Found directory to load jars: " + lib.getAbsolutePath());
|
||||
}
|
||||
|
||||
List<URL> jars = new ArrayList<URL>();
|
||||
for (File file : lib.listFiles()) {
|
||||
jars.add(file.toURI().toURL());
|
||||
}
|
||||
URL[] urls = jars.toArray(new URL[jars.size()]);
|
||||
URLClassLoader loader = new URLClassLoader(urls, EmbeddedLDAPLauncher.class.getClassLoader());
|
||||
|
||||
Class mainClass = loader.loadClass(className);
|
||||
Method executeMethod = null;
|
||||
for (Method m : mainClass.getMethods()) if (m.getName().equals("execute")) { executeMethod = m; break; }
|
||||
Object obj = args;
|
||||
executeMethod.invoke(null, obj, defaultProperties);
|
||||
}
|
||||
|
||||
|
||||
private static File getHome() {
|
||||
String launcherPath = EmbeddedLDAPLauncher.class.getName().replace('.', '/') + ".class";
|
||||
URL jarfile = EmbeddedLDAPLauncher.class.getClassLoader().getResource(launcherPath);
|
||||
if (jarfile != null) {
|
||||
Matcher m = Pattern.compile("jar:(file:.*)!/" + launcherPath).matcher(jarfile.toString());
|
||||
if (m.matches()) {
|
||||
try {
|
||||
File jarPath = new File(new URI(m.group(1)));
|
||||
File libPath = jarPath.getParentFile().getParentFile();
|
||||
System.out.println("Home directory: " + libPath.toString());
|
||||
if (!libPath.exists()) {
|
||||
System.exit(1);
|
||||
|
||||
}
|
||||
return libPath;
|
||||
} catch (URISyntaxException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
System.err.println("jar file null: " + launcherPath);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static File file(File home, String... pathItems) {
|
||||
File current = home;
|
||||
|
||||
for (String item : pathItems) {
|
||||
if (item.equals("..")) {
|
||||
current = current.getParentFile();
|
||||
} else {
|
||||
current = new File(current, item);
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-examples-ldap-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.4.0.Final-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.keycloak.example.demo</groupId>
|
||||
<artifactId>keycloak-examples-ldap-app</artifactId>
|
||||
<packaging>war</packaging>
|
||||
<name>LDAP Demo Application</name>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>jboss</id>
|
||||
<name>jboss repo</name>
|
||||
<url>http://repository.jboss.org/nexus/content/groups/public/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jboss.spec.javax.servlet</groupId>
|
||||
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>ldap-portal</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.jboss.as.plugins</groupId>
|
||||
<artifactId>jboss-as-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.wildfly.plugins</groupId>
|
||||
<artifactId>wildfly-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
|
@ -64,4 +64,3 @@ objectclass: top
|
|||
objectclass: groupOfNames
|
||||
cn: accountant
|
||||
member: uid=bwilson,ou=People,dc=keycloak,dc=org
|
||||
|
|
@ -1,20 +1,87 @@
|
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-examples-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.4.0.Final-SNAPSHOT</version>
|
||||
</parent>
|
||||
<name>Keycloak LDAP Examples - Parent</name>
|
||||
<description/>
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.keycloak.example.demo</groupId>
|
||||
<artifactId>keycloak-examples-ldap</artifactId>
|
||||
<packaging>war</packaging>
|
||||
<name>LDAP Demo Application</name>
|
||||
|
||||
<artifactId>keycloak-examples-ldap-parent</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>jboss</id>
|
||||
<name>jboss repo</name>
|
||||
<url>http://repository.jboss.org/nexus/content/groups/public/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<modules>
|
||||
<module>embedded-ldap</module>
|
||||
<module>ldap-app</module>
|
||||
</modules>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jboss.spec.javax.servlet</groupId>
|
||||
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-util-embedded-ldap</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>ldap-portal</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.jboss.as.plugins</groupId>
|
||||
<artifactId>jboss-as-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.wildfly.plugins</groupId>
|
||||
<artifactId>wildfly-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>ldap</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<mainClass>org.keycloak.util.ldap.LDAPEmbeddedServer</mainClass>
|
||||
<classpathScope>test</classpathScope>
|
||||
<systemProperties>
|
||||
<systemProperty>
|
||||
<key>ldap.ldif</key>
|
||||
<value>ldap-example-users.ldif</value>
|
||||
</systemProperty>
|
||||
</systemProperties>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
</project>
|
|
@ -56,7 +56,7 @@ public class FederationProvidersIntegrationTest {
|
|||
|
||||
@Override
|
||||
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
|
||||
FederationTestUtils.addLocalUser(manager.getSession(), appRealm, "mary", "mary@test.com", "password-app");
|
||||
FederationTestUtils.addLocalUser(manager.getSession(), appRealm, "marykeycloak", "mary@test.com", "password-app");
|
||||
|
||||
Map<String,String> ldapConfig = ldapRule.getConfig();
|
||||
ldapConfig.put(LDAPConstants.SYNC_REGISTRATIONS, "true");
|
||||
|
@ -225,7 +225,7 @@ public class FederationProvidersIntegrationTest {
|
|||
@Test
|
||||
public void loginClassic() {
|
||||
loginPage.open();
|
||||
loginPage.login("mary", "password-app");
|
||||
loginPage.login("marykeycloak", "password-app");
|
||||
|
||||
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||
|
@ -488,7 +488,7 @@ public class FederationProvidersIntegrationTest {
|
|||
@Override
|
||||
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
|
||||
LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
||||
FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "mary", "Mary1", "Kelly1", "mary1@email.org", null, "123");
|
||||
FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "marykeycloak", "Mary1", "Kelly1", "mary1@email.org", null, "123");
|
||||
FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "mary-duplicatemail", "Mary2", "Kelly2", "mary@test.com", null, "123");
|
||||
}
|
||||
|
||||
|
|
|
@ -28,8 +28,13 @@
|
|||
<artifactId>log4j</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-log4j12</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
|
Loading…
Reference in a new issue