diff --git a/.github/workflows/ci-actions.yml b/.github/workflows/ci-actions.yml
index 6908ff81f80c3..77e6b5414a4ad 100644
--- a/.github/workflows/ci-actions.yml
+++ b/.github/workflows/ci-actions.yml
@@ -355,13 +355,14 @@ jobs:
kafka
kafka-streams
- category: Security1
- timeout: 30
+ timeout: 40
keycloak: "true"
test-modules: >
elytron-security-oauth2
elytron-security
elytron-security-jdbc
- elytron-undertow
+ elytron-undertow
+ elytron-security-ldap
- category: Security2
timeout: 45
keycloak: "true"
diff --git a/bom/deployment/pom.xml b/bom/deployment/pom.xml
index d734353960eb1..05d90016fe75b 100644
--- a/bom/deployment/pom.xml
+++ b/bom/deployment/pom.xml
@@ -760,6 +760,11 @@
quarkus-security-jpa-deployment
${project.version}
+
+ io.quarkus
+ quarkus-elytron-security-ldap-deployment
+ ${project.version}
+
io.quarkus
quarkus-security-test-utils
diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml
index 8aa72a7d053d6..6ecae25b2c0d8 100644
--- a/bom/runtime/pom.xml
+++ b/bom/runtime/pom.xml
@@ -860,6 +860,11 @@
quarkus-security-jpa
${project.version}
+
+ io.quarkus
+ quarkus-elytron-security-ldap
+ ${project.version}
+
io.quarkus
quarkus-vault
@@ -2082,6 +2087,11 @@
wildfly-elytron-realm-jdbc
${wildfly-elytron.version}
+
+ org.wildfly.security
+ wildfly-elytron-realm-ldap
+ ${wildfly-elytron.version}
+
org.wildfly.security
wildfly-elytron-ssl
diff --git a/docs/src/main/asciidoc/security-ldap.adoc b/docs/src/main/asciidoc/security-ldap.adoc
new file mode 100644
index 0000000000000..5937520e12763
--- /dev/null
+++ b/docs/src/main/asciidoc/security-ldap.adoc
@@ -0,0 +1,226 @@
+////
+This guide is maintained in the main Quarkus repository
+and pull requests should be submitted there:
+https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc
+////
+= Quarkus - Using Security with an LDAP Realm
+
+include::./attributes.adoc[]
+
+This guide demonstrates how your Quarkus application can use an LDAP server to authenticate and authorize your user identities.
+
+
+== Prerequisites
+
+To complete this guide, you need:
+
+* less than 15 minutes
+* an IDE
+* JDK 1.8+ installed with `JAVA_HOME` configured appropriately
+* Apache Maven {maven-version}
+
+== Architecture
+
+In this example, we build a very simple microservice which offers three endpoints:
+
+* `/api/public`
+* `/api/users/me`
+* `/api/admin`
+
+The `/api/public` endpoint can be accessed anonymously.
+The `/api/admin` endpoint is protected with RBAC (Role-Based Access Control) where only users granted with the `adminRole` role can access. At this endpoint, we use the `@RolesAllowed` annotation to declaratively enforce the access constraint.
+The `/api/users/me` endpoint is also protected with RBAC (Role-Based Access Control) where only users granted with the `standardRole` role can access. As a response, it returns a JSON document with details about the user.
+
+== Solution
+
+We recommend that you follow the instructions in the next sections and create the application step by step.
+However, you can go right to the completed example.
+
+Clone the Git repository: `git clone {quickstarts-clone-url}`, or download an {quickstarts-archive-url}[archive].
+
+The solution is located in the `security-ldap-quickstart` {quickstarts-tree-url}/security-ldap-quickstart[directory].
+
+== Creating the Maven Project
+
+First, we need a new project. Create a new project with the following command:
+
+[source, subs=attributes+]
+----
+mvn io.quarkus:quarkus-maven-plugin:{quarkus-version}:create \
+ -DprojectGroupId=org.acme \
+ -DprojectArtifactId=security-ldap-quickstart \
+ -Dextensions="elytron-security-ldap, resteasy"
+cd security-ldap-quickstart
+----
+
+This command generates a Maven project, importing the `elytron-security-ldap` extension
+which is a https://docs.wildfly.org/19/WildFly_Elytron_Security.html#ldap-security-realm[`wildfly-elytron-realm-ldap`] adapter for Quarkus applications.
+
+== Writing the application
+
+Let's start by implementing the `/api/public` endpoint. As you can see from the source code below, it is just a regular JAX-RS resource:
+
+[source,java]
+----
+package org.acme.elytron.security.ldap;
+
+import javax.annotation.security.PermitAll;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+@Path("/api/public")
+public class PublicResource {
+
+ @GET
+ @PermitAll
+ @Produces(MediaType.TEXT_PLAIN)
+ public String publicResource() {
+ return "public";
+ }
+}
+----
+
+The source code for the `/api/admin` endpoint is also very simple. The main difference here is that we are using a `@RolesAllowed` annotation to make sure that only users granted with the `adminRole` role can access the endpoint:
+
+
+[source,java]
+----
+package org.acme.elytron.security.ldap;
+
+import javax.annotation.security.RolesAllowed;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+@Path("/api/admin")
+public class AdminResource {
+
+ @GET
+ @RolesAllowed("adminRole")
+ @Produces(MediaType.TEXT_PLAIN)
+ public String adminResource() {
+ return "admin";
+ }
+}
+----
+
+Finally, let's consider the `/api/users/me` endpoint. As you can see from the source code below, we are trusting only users with the `standardRole` role.
+We are using `SecurityContext` to get access to the current authenticated Principal and we return the user's name. This information is loaded from the LDAP server.
+
+[source,java]
+----
+package org.acme.elytron.security.ldap;
+
+import javax.annotation.security.RolesAllowed;
+import javax.inject.Inject;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.SecurityContext;
+
+@Path("/api/users")
+public class UserResource {
+
+ @GET
+ @RolesAllowed("standardRole")
+ @Path("/me")
+ @Produces(MediaType.APPLICATION_JSON)
+ public String me(@Context SecurityContext securityContext) {
+ return securityContext.getUserPrincipal().getName();
+ }
+}
+----
+
+=== Configuring the Application
+
+[source,properties]
+----
+quarkus.security.ldap.enabled=true
+
+quarkus.security.ldap.dir-context.principal=uid=tool,ou=accounts,o=YourCompany,c=DE
+quarkus.security.ldap.dir-context.url=ldaps://ldap.server.local
+quarkus.security.ldap.dir-context.password=PASSWORD
+
+quarkus.security.ldap.identity-mapping.rdn-identifier=uid
+quarkus.security.ldap.identity-mapping.search-base-dn=ou=users,ou=tool,o=YourCompany,c=DE
+
+quarkus.security.ldap.identity-mapping.attribute-mappings."0".from=cn
+quarkus.security.ldap.identity-mapping.attribute-mappings."0".to=groups
+quarkus.security.ldap.identity-mapping.attribute-mappings."0".filter=(member=uid={0})
+quarkus.security.ldap.identity-mapping.attribute-mappings."0".filter-base-dn=ou=roles,ou=tool,o=YourCompany,c=DE
+----
+
+The `elytron-security-ldap` extension requires a dir-context and an identity-mapping with at least one attribute-mapping to authenticate the user and its identity.
+
+== Testing the Application
+
+The application is now protected and the identities are provided by our LDAP server.
+The very first thing to check is to ensure the anonymous access works.
+
+[source,shell]
+----
+$ curl -i -X GET http://localhost:8080/api/public
+HTTP/1.1 200 OK
+Content-Length: 6
+Content-Type: text/plain;charset=UTF-8
+
+public%
+----
+
+Now, let's try a to hit a protected resource anonymously.
+
+[source,shell]
+----
+$ curl -i -X GET http://localhost:8080/api/admin
+HTTP/1.1 401 Unauthorized
+Content-Length: 14
+Content-Type: text/html;charset=UTF-8
+
+Not authorized%
+----
+
+So far so good, now let's try with an allowed user.
+
+[source,shell]
+----
+$ curl -i -X GET -u adminUser:adminUserPassword http://localhost:8080/api/admin
+HTTP/1.1 200 OK
+Content-Length: 5
+Content-Type: text/plain;charset=UTF-8
+
+admin%
+----
+By providing the `adminUser:adminUserPassword` credentials, the extension authenticated the user and loaded their roles.
+The `adminUser` user is authorized to access to the protected resources.
+
+The user `adminUser` should be forbidden to access a resource protected with `@RolesAllowed("standardRole")` because it doesn't have this role.
+[source,shell]
+----
+$ curl -i -X GET -u adminUser:adminUserPassword http://localhost:8080/api/users/me
+HTTP/1.1 403 Forbidden
+Content-Length: 34
+Content-Type: text/html;charset=UTF-8
+
+Forbidden%
+----
+
+Finally, using the user `standardUser` works and the security context contains the principal details (username for instance).
+[source,shell]
+----
+curl -i -X GET -u standardUser:standardUserPassword http://localhost:8080/api/users/me
+HTTP/1.1 200 OK
+Content-Length: 4
+Content-Type: text/plain;charset=UTF-8
+
+user%
+----
+
+[[configuration-reference]]
+== Configuration Reference
+
+include::{generated-dir}/config/quarkus-elytron-security-ldap.adoc[opts=optional, leveloffset=+1]
diff --git a/extensions/elytron-security-ldap/deployment/pom.xml b/extensions/elytron-security-ldap/deployment/pom.xml
new file mode 100644
index 0000000000000..3cf64668dd879
--- /dev/null
+++ b/extensions/elytron-security-ldap/deployment/pom.xml
@@ -0,0 +1,85 @@
+
+
+ 4.0.0
+
+ io.quarkus
+ quarkus-elytron-security-ldap-parent
+ 999-SNAPSHOT
+ ../pom.xml
+
+
+ quarkus-elytron-security-ldap-deployment
+ Quarkus - Elytron Security LDAP - Deployment
+
+
+
+ io.quarkus
+ quarkus-elytron-security-ldap
+
+
+
+ io.quarkus
+ quarkus-core-deployment
+
+
+ io.quarkus
+ quarkus-arc-deployment
+
+
+ io.quarkus
+ quarkus-elytron-security-deployment
+
+
+
+ io.quarkus
+ quarkus-undertow
+ test
+
+
+ io.quarkus
+ quarkus-undertow-deployment
+ test
+
+
+ io.quarkus
+ quarkus-resteasy-deployment
+ test
+
+
+ io.quarkus
+ quarkus-junit5-internal
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+ io.quarkus
+ quarkus-test-ldap
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${project.version}
+
+
+
+
+
+
+
+
diff --git a/extensions/elytron-security-ldap/deployment/src/main/java/io/quarkus/elytron/security/ldap/deployment/ElytronSecurityLdapProcessor.java b/extensions/elytron-security-ldap/deployment/src/main/java/io/quarkus/elytron/security/ldap/deployment/ElytronSecurityLdapProcessor.java
new file mode 100644
index 0000000000000..acf5da21ed6d1
--- /dev/null
+++ b/extensions/elytron-security-ldap/deployment/src/main/java/io/quarkus/elytron/security/ldap/deployment/ElytronSecurityLdapProcessor.java
@@ -0,0 +1,67 @@
+package io.quarkus.elytron.security.ldap.deployment;
+
+import org.wildfly.security.auth.server.SecurityRealm;
+
+import io.quarkus.arc.deployment.BeanContainerBuildItem;
+import io.quarkus.deployment.Capabilities;
+import io.quarkus.deployment.annotations.BuildProducer;
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.annotations.ExecutionTime;
+import io.quarkus.deployment.annotations.Record;
+import io.quarkus.deployment.builditem.CapabilityBuildItem;
+import io.quarkus.deployment.builditem.FeatureBuildItem;
+import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
+import io.quarkus.elytron.security.deployment.ElytronPasswordMarkerBuildItem;
+import io.quarkus.elytron.security.deployment.SecurityRealmBuildItem;
+import io.quarkus.elytron.security.ldap.LdapRecorder;
+import io.quarkus.elytron.security.ldap.QuarkusDirContextFactory;
+import io.quarkus.elytron.security.ldap.config.LdapSecurityRealmConfig;
+import io.quarkus.runtime.RuntimeValue;
+
+class ElytronSecurityLdapProcessor {
+
+ LdapSecurityRealmConfig ldap;
+
+ @BuildStep
+ CapabilityBuildItem capability() {
+ return new CapabilityBuildItem(Capabilities.SECURITY_ELYTRON_LDAP);
+ }
+
+ @BuildStep()
+ FeatureBuildItem feature() {
+ return new FeatureBuildItem(FeatureBuildItem.SECURITY_LDAP);
+ }
+
+ /**
+ * Check to see if a LdapRealmConfig was specified and enabled and create a
+ * {@linkplain org.wildfly.security.auth.realm.ldap.LdapSecurityRealm}
+ *
+ * @param recorder - runtime security recorder
+ * @param securityRealm - the producer factory for the SecurityRealmBuildItem
+ * @throws Exception - on any failure
+ */
+ @BuildStep
+ @Record(ExecutionTime.RUNTIME_INIT)
+ void configureLdapRealmAuthConfig(LdapRecorder recorder,
+ BuildProducer securityRealm,
+ BeanContainerBuildItem beanContainerBuildItem //we need this to make sure ArC is initialized
+ ) throws Exception {
+ if (ldap.enabled) {
+ RuntimeValue realm = recorder.createRealm(ldap);
+ securityRealm.produce(new SecurityRealmBuildItem(realm, ldap.realmName, null));
+ }
+ }
+
+ @BuildStep
+ ElytronPasswordMarkerBuildItem marker() {
+ if (ldap.enabled) {
+ return new ElytronPasswordMarkerBuildItem();
+ }
+ return null;
+ }
+
+ @BuildStep
+ ReflectiveClassBuildItem enableReflection() {
+ return new ReflectiveClassBuildItem(true, true, QuarkusDirContextFactory.INITIAL_CONTEXT_FACTORY);
+ }
+}
diff --git a/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/CustomRoleDecoder.java b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/CustomRoleDecoder.java
new file mode 100644
index 0000000000000..9697832847407
--- /dev/null
+++ b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/CustomRoleDecoder.java
@@ -0,0 +1,28 @@
+package io.quarkus.elytron.security.ldap;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.StreamSupport;
+
+import javax.enterprise.context.ApplicationScoped;
+
+import org.wildfly.security.authz.Attributes;
+import org.wildfly.security.authz.AuthorizationIdentity;
+import org.wildfly.security.authz.RoleDecoder;
+import org.wildfly.security.authz.Roles;
+
+@ApplicationScoped
+public class CustomRoleDecoder implements RoleDecoder {
+
+ @Override
+ public Roles decodeRoles(AuthorizationIdentity authorizationIdentity) {
+ Attributes.Entry groupsEntry = authorizationIdentity.getAttributes().get("Roles");
+ Set roles = new HashSet<>();
+ StreamSupport.stream(groupsEntry.spliterator(), false).forEach(groups -> {
+ for (String role : groups.split(",")) {
+ roles.add(role.trim());
+ }
+ });
+ return Roles.fromSet(roles);
+ }
+}
diff --git a/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/CustomRoleDecoderTest.java b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/CustomRoleDecoderTest.java
new file mode 100644
index 0000000000000..e4be68b51a431
--- /dev/null
+++ b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/CustomRoleDecoderTest.java
@@ -0,0 +1,24 @@
+package io.quarkus.elytron.security.ldap;
+
+import java.util.Arrays;
+import java.util.stream.Stream;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+
+public class CustomRoleDecoderTest extends LdapSecurityRealmTest {
+
+ static Class[] testClassesWithCustomRoleDecoder = Stream.concat(
+ Arrays.stream(testClasses),
+ Arrays.stream(new Class[] { CustomRoleDecoder.class })).toArray(Class[]::new);
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest()
+ .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
+ .addClasses(testClassesWithCustomRoleDecoder)
+ .addAsResource("custom-role-decoder/application.properties", "application.properties"));
+
+}
diff --git a/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/LdapSecurityRealmTest.java b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/LdapSecurityRealmTest.java
new file mode 100644
index 0000000000000..ed589a1af7aed
--- /dev/null
+++ b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/LdapSecurityRealmTest.java
@@ -0,0 +1,152 @@
+package io.quarkus.elytron.security.ldap;
+
+import static org.hamcrest.Matchers.equalTo;
+
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.elytron.security.ldap.rest.ParametrizedPathsResource;
+import io.quarkus.elytron.security.ldap.rest.RolesEndpointClassLevel;
+import io.quarkus.elytron.security.ldap.rest.SingleRoleSecuredServlet;
+import io.quarkus.elytron.security.ldap.rest.SubjectExposingResource;
+import io.quarkus.elytron.security.ldap.rest.TestApplication;
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.ldap.LdapServerTestResource;
+import io.restassured.RestAssured;
+
+/**
+ * Tests of BASIC authentication mechanism with the minimal config required
+ */
+
+@QuarkusTestResource(LdapServerTestResource.class)
+public abstract class LdapSecurityRealmTest {
+
+ protected static Class[] testClasses = {
+ SingleRoleSecuredServlet.class, TestApplication.class, RolesEndpointClassLevel.class,
+ ParametrizedPathsResource.class, SubjectExposingResource.class
+ };
+
+ // Basic @ServletSecurity tests
+ @Test()
+ public void testSecureAccessFailure() {
+ RestAssured.when().get("/servlet-secured").then()
+ .statusCode(401);
+ }
+
+ @Test()
+ public void testSecureRoleFailure() {
+ RestAssured.given().auth().preemptive().basic("noRoleUser", "noRoleUserPassword")
+ .when().get("/servlet-secured").then()
+ .statusCode(403);
+ }
+
+ @Test()
+ public void testSecureAccessSuccess() {
+ RestAssured.given().auth().preemptive().basic("standardUser", "standardUserPassword")
+ .when().get("/servlet-secured").then()
+ .statusCode(200);
+ }
+
+ /**
+ * Test access a secured jaxrs resource without any authentication. should see 401 error code.
+ */
+ @Test
+ public void testJaxrsGetFailure() {
+ RestAssured.when().get("/jaxrs-secured/roles-class").then()
+ .statusCode(401);
+ }
+
+ /**
+ * Test access a secured jaxrs resource with authentication, but no authorization. should see 403 error code.
+ */
+ @Test
+ public void testJaxrsGetRoleFailure() {
+ RestAssured.given().auth().preemptive().basic("noRoleUser", "noRoleUserPassword")
+ .when().get("/jaxrs-secured/roles-class").then()
+ .statusCode(403);
+ }
+
+ /**
+ * Test access a secured jaxrs resource with authentication, and authorization. should see 200 success code.
+ */
+ @Test
+ public void testJaxrsGetRoleSuccess() {
+ RestAssured.given().auth().preemptive().basic("standardUser", "standardUserPassword")
+ .when().get("/jaxrs-secured/roles-class").then()
+ .statusCode(200);
+ }
+
+ /**
+ * Test access a secured jaxrs resource with authentication, and authorization. should see 200 success code.
+ */
+ @Test
+ public void testJaxrsPathAdminRoleSuccess() {
+ RestAssured.given().auth().preemptive().basic("adminUser", "adminUserPassword")
+ .when().get("/jaxrs-secured/parameterized-paths/my/banking/admin").then()
+ .statusCode(200);
+ }
+
+ @Test
+ public void testJaxrsPathAdminRoleFailure() {
+ RestAssured.given().auth().preemptive().basic("standardUser", "standardUserPassword")
+ .when().get("/jaxrs-secured/parameterized-paths/my/banking/admin").then()
+ .statusCode(403);
+ }
+
+ /**
+ * Test access a secured jaxrs resource with authentication, and authorization. should see 200 success code.
+ */
+ @Test
+ public void testJaxrsPathUserRoleSuccess() {
+ RestAssured.given().auth().preemptive().basic("standardUser", "standardUserPassword")
+ .when().get("/jaxrs-secured/parameterized-paths/my/banking/view").then()
+ .statusCode(200);
+ }
+
+ /**
+ * Test access a secured jaxrs resource with authentication, and authorization. should see 200 success code.
+ */
+ @Test
+ public void testJaxrsUserRoleSuccess() {
+ RestAssured.given().auth().preemptive().basic("standardUser", "standardUserPassword")
+ .when().get("/jaxrs-secured/subject/secured").then()
+ .statusCode(200)
+ .body(equalTo("standardUser"));
+ }
+
+ @Test
+ public void testJaxrsInjectedPrincipalSuccess() {
+ RestAssured.given().auth().preemptive().basic("standardUser", "standardUserPassword")
+ .when().get("/jaxrs-secured/subject/principal-secured").then()
+ .statusCode(200)
+ .body(equalTo("standardUser"));
+ }
+
+ /**
+ * Test access a @PermitAll secured jaxrs resource without any authentication. should see a 200 success code.
+ */
+ @Test
+ public void testJaxrsGetPermitAll() {
+ RestAssured.when().get("/jaxrs-secured/subject/unsecured").then()
+ .statusCode(200)
+ .body(equalTo("anonymous"));
+ }
+
+ /**
+ * Test access a @DenyAll secured jaxrs resource without authentication. should see a 401 success code.
+ */
+ @Test
+ public void testJaxrsGetDenyAllWithoutAuth() {
+ RestAssured.when().get("/jaxrs-secured/subject/denied").then()
+ .statusCode(401);
+ }
+
+ /**
+ * Test access a @DenyAll secured jaxrs resource with authentication. should see a 403 success code.
+ */
+ @Test
+ public void testJaxrsGetDenyAllWithAuth() {
+ RestAssured.given().auth().preemptive().basic("standardUser", "standardUserPassword")
+ .when().get("/jaxrs-secured/subject/denied").then()
+ .statusCode(403);
+ }
+}
diff --git a/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/MinimalConfigurationTest.java b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/MinimalConfigurationTest.java
new file mode 100644
index 0000000000000..6ae33d5b81698
--- /dev/null
+++ b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/MinimalConfigurationTest.java
@@ -0,0 +1,17 @@
+package io.quarkus.elytron.security.ldap;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+
+public class MinimalConfigurationTest extends LdapSecurityRealmTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest()
+ .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
+ .addClasses(testClasses)
+ .addAsResource("minimal-config/application.properties", "application.properties"));
+
+}
diff --git a/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/ParametrizedPathsResource.java b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/ParametrizedPathsResource.java
new file mode 100644
index 0000000000000..33d8be9dfdf78
--- /dev/null
+++ b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/ParametrizedPathsResource.java
@@ -0,0 +1,23 @@
+package io.quarkus.elytron.security.ldap.rest;
+
+import javax.annotation.security.RolesAllowed;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+
+@Path("/parameterized-paths")
+public class ParametrizedPathsResource {
+ @GET
+ @Path("/my/{path}/admin")
+ @RolesAllowed("adminRole")
+ public String admin(@PathParam("path") String path) {
+ return "Admin accessed " + path;
+ }
+
+ @GET
+ @Path("/my/{path}/view")
+ @RolesAllowed("standardRole")
+ public String view(@PathParam("path") String path) {
+ return "View accessed " + path;
+ }
+}
diff --git a/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/RolesEndpointClassLevel.java b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/RolesEndpointClassLevel.java
new file mode 100644
index 0000000000000..fc8e8f8b8ebed
--- /dev/null
+++ b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/RolesEndpointClassLevel.java
@@ -0,0 +1,20 @@
+package io.quarkus.elytron.security.ldap.rest;
+
+import javax.annotation.security.RolesAllowed;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.SecurityContext;
+
+/**
+ * Test JAXRS endpoint with RolesAllowed specified at the class level
+ */
+@Path("/roles-class")
+@RolesAllowed("standardRole")
+public class RolesEndpointClassLevel {
+ @GET
+ public String echo(@Context SecurityContext sec) {
+ return "Hello " + sec.getUserPrincipal().getName();
+ }
+
+}
diff --git a/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/SingleRoleSecuredServlet.java b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/SingleRoleSecuredServlet.java
new file mode 100644
index 0000000000000..a9c6df3421306
--- /dev/null
+++ b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/SingleRoleSecuredServlet.java
@@ -0,0 +1,24 @@
+package io.quarkus.elytron.security.ldap.rest;
+
+import java.io.IOException;
+
+import javax.servlet.annotation.HttpConstraint;
+import javax.servlet.annotation.ServletSecurity;
+import javax.servlet.annotation.WebInitParam;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Basic secured servlet test target
+ */
+@ServletSecurity(@HttpConstraint(rolesAllowed = { "standardRole" }))
+@WebServlet(name = "SingleRoleSecuredServlet", urlPatterns = "/servlet-secured", initParams = {
+ @WebInitParam(name = "message", value = "A secured message") })
+public class SingleRoleSecuredServlet extends HttpServlet {
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ resp.getWriter().write(getInitParameter("message"));
+ }
+}
diff --git a/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/SubjectExposingResource.java b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/SubjectExposingResource.java
new file mode 100644
index 0000000000000..124a0a7f704e7
--- /dev/null
+++ b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/SubjectExposingResource.java
@@ -0,0 +1,57 @@
+package io.quarkus.elytron.security.ldap.rest;
+
+import java.security.Principal;
+
+import javax.annotation.security.DenyAll;
+import javax.annotation.security.PermitAll;
+import javax.annotation.security.RolesAllowed;
+import javax.inject.Inject;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.SecurityContext;
+
+@Path("subject")
+public class SubjectExposingResource {
+
+ @Inject
+ Principal principal;
+
+ @GET
+ @RolesAllowed("standardRole")
+ @Path("secured")
+ public String getSubjectSecured(@Context SecurityContext sec) {
+ Principal user = sec.getUserPrincipal();
+ String name = user != null ? user.getName() : "anonymous";
+ return name;
+ }
+
+ @GET
+ @RolesAllowed("standardRole")
+ @Path("principal-secured")
+ public String getPrincipalSecured(@Context SecurityContext sec) {
+ if (principal == null) {
+ throw new IllegalStateException("No injected principal");
+ }
+ String name = principal.getName();
+ return name;
+ }
+
+ @GET
+ @Path("unsecured")
+ @PermitAll
+ public String getSubjectUnsecured(@Context SecurityContext sec) {
+ Principal user = sec.getUserPrincipal();
+ String name = user != null ? user.getName() : "anonymous";
+ return name;
+ }
+
+ @DenyAll
+ @GET
+ @Path("denied")
+ public String getSubjectDenied(@Context SecurityContext sec) {
+ Principal user = sec.getUserPrincipal();
+ String name = user != null ? user.getName() : "anonymous";
+ return name;
+ }
+}
diff --git a/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/TestApplication.java b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/TestApplication.java
new file mode 100644
index 0000000000000..752efd59d42e6
--- /dev/null
+++ b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/rest/TestApplication.java
@@ -0,0 +1,11 @@
+package io.quarkus.elytron.security.ldap.rest;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.ws.rs.ApplicationPath;
+import javax.ws.rs.core.Application;
+
+@ApplicationScoped
+@ApplicationPath("/jaxrs-secured")
+public class TestApplication extends Application {
+ // intentionally left empty
+}
diff --git a/extensions/elytron-security-ldap/deployment/src/test/resources/custom-role-decoder/application.properties b/extensions/elytron-security-ldap/deployment/src/test/resources/custom-role-decoder/application.properties
new file mode 100644
index 0000000000000..4e5cf424913d3
--- /dev/null
+++ b/extensions/elytron-security-ldap/deployment/src/test/resources/custom-role-decoder/application.properties
@@ -0,0 +1,13 @@
+quarkus.security.ldap.enabled=true
+
+quarkus.security.ldap.dir-context.principal=uid=admin,ou=system
+quarkus.security.ldap.dir-context.url=ldap://127.0.0.1:10389
+quarkus.security.ldap.dir-context.password=secret
+
+quarkus.security.ldap.identity-mapping.rdn-identifier=uid
+quarkus.security.ldap.identity-mapping.search-base-dn=ou=Users,dc=quarkus,dc=io
+
+quarkus.security.ldap.identity-mapping.attribute-mappings."0".from=cn
+quarkus.security.ldap.identity-mapping.attribute-mappings."0".to=Roles
+quarkus.security.ldap.identity-mapping.attribute-mappings."0".filter=(member=uid={0},ou=Users,dc=quarkus,dc=io)
+quarkus.security.ldap.identity-mapping.attribute-mappings."0".filter-base-dn=ou=Roles,dc=quarkus,dc=io
\ No newline at end of file
diff --git a/extensions/elytron-security-ldap/deployment/src/test/resources/minimal-config/application.properties b/extensions/elytron-security-ldap/deployment/src/test/resources/minimal-config/application.properties
new file mode 100644
index 0000000000000..e5d01d25ea966
--- /dev/null
+++ b/extensions/elytron-security-ldap/deployment/src/test/resources/minimal-config/application.properties
@@ -0,0 +1,11 @@
+quarkus.security.ldap.enabled=true
+
+quarkus.security.ldap.dir-context.principal=uid=admin,ou=system
+quarkus.security.ldap.dir-context.url=ldap://127.0.0.1:10389
+quarkus.security.ldap.dir-context.password=secret
+
+quarkus.security.ldap.identity-mapping.search-base-dn=ou=Users,dc=quarkus,dc=io
+
+quarkus.security.ldap.identity-mapping.attribute-mappings."0".from=cn
+quarkus.security.ldap.identity-mapping.attribute-mappings."0".filter=(member=uid={0},ou=Users,dc=quarkus,dc=io)
+quarkus.security.ldap.identity-mapping.attribute-mappings."0".filter-base-dn=ou=Roles,dc=quarkus,dc=io
diff --git a/extensions/elytron-security-ldap/pom.xml b/extensions/elytron-security-ldap/pom.xml
new file mode 100644
index 0000000000000..c7f782508322a
--- /dev/null
+++ b/extensions/elytron-security-ldap/pom.xml
@@ -0,0 +1,21 @@
+
+
+ 4.0.0
+
+ io.quarkus
+ quarkus-extensions-parent
+ 999-SNAPSHOT
+ ../pom.xml
+
+
+ quarkus-elytron-security-ldap-parent
+ Quarkus - Elytron Security LDAP
+
+ pom
+
+ deployment
+ runtime
+
+
diff --git a/extensions/elytron-security-ldap/runtime/pom.xml b/extensions/elytron-security-ldap/runtime/pom.xml
new file mode 100644
index 0000000000000..c97b7bb7c9fbd
--- /dev/null
+++ b/extensions/elytron-security-ldap/runtime/pom.xml
@@ -0,0 +1,56 @@
+
+
+ 4.0.0
+
+ io.quarkus
+ quarkus-elytron-security-ldap-parent
+ 999-SNAPSHOT
+ ../pom.xml
+
+
+ quarkus-elytron-security-ldap
+ Quarkus - Elytron Security LDAP - Runtime
+ Secure your applications with username/password via LDAP
+
+
+ io.quarkus
+ quarkus-core
+
+
+ io.quarkus
+ quarkus-elytron-security
+
+
+ io.quarkus
+ quarkus-arc
+
+
+ org.wildfly.security
+ wildfly-elytron-realm-ldap
+
+
+
+
+
+
+ io.quarkus
+ quarkus-bootstrap-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${project.version}
+
+
+
+
+
+
+
diff --git a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/DelegatingLdapContext.java b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/DelegatingLdapContext.java
new file mode 100644
index 0000000000000..c90a9da278e2c
--- /dev/null
+++ b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/DelegatingLdapContext.java
@@ -0,0 +1,497 @@
+package io.quarkus.elytron.security.ldap;
+
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.Hashtable;
+
+import javax.naming.Binding;
+import javax.naming.Context;
+import javax.naming.Name;
+import javax.naming.NameClassPair;
+import javax.naming.NameParser;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.ReferralException;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.ModificationItem;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+import javax.naming.ldap.Control;
+import javax.naming.ldap.ExtendedRequest;
+import javax.naming.ldap.ExtendedResponse;
+import javax.naming.ldap.InitialLdapContext;
+import javax.naming.ldap.LdapContext;
+import javax.net.SocketFactory;
+
+import org.wildfly.common.Assert;
+import org.wildfly.security.auth.realm.ldap.ThreadLocalSSLSocketFactory;
+import org.wildfly.security.manager.action.SetContextClassLoaderAction;
+
+class DelegatingLdapContext implements LdapContext {
+
+ private final DirContext delegating;
+ private final CloseHandler closeHandler;
+ private final SocketFactory socketFactory;
+
+ interface CloseHandler {
+ void handle(DirContext context) throws NamingException;
+ }
+
+ DelegatingLdapContext(DirContext delegating, CloseHandler closeHandler, SocketFactory socketFactory)
+ throws NamingException {
+ this.delegating = delegating;
+ this.closeHandler = closeHandler;
+ this.socketFactory = socketFactory;
+ }
+
+ // for needs of newInstance()
+ private DelegatingLdapContext(DirContext delegating, SocketFactory socketFactory) throws NamingException {
+ this.delegating = delegating;
+ this.closeHandler = null; // close handler should not be applied to copy
+ this.socketFactory = socketFactory;
+ }
+
+ public LdapContext newInitialLdapContext(Hashtable, ?> environment, Control[] connCtls) throws NamingException {
+ ClassLoader previous = setSocketFactory();
+ try {
+ return new InitialLdapContext(environment, null);
+ } finally {
+ unsetSocketFactory(previous);
+ }
+ }
+
+ @Override
+ public void close() throws NamingException {
+ if (closeHandler == null) {
+ delegating.close();
+ } else {
+ closeHandler.handle(delegating);
+ }
+ }
+
+ // for needs of search()
+ private NamingEnumeration wrap(NamingEnumeration delegating) {
+ return new NamingEnumeration() {
+
+ @Override
+ public boolean hasMoreElements() {
+ ClassLoader previous = setSocketFactory();
+ try {
+ return delegating.hasMoreElements();
+ } finally {
+ unsetSocketFactory(previous);
+ }
+ }
+
+ @Override
+ public SearchResult nextElement() {
+ ClassLoader previous = setSocketFactory();
+ try {
+ return delegating.nextElement();
+ } finally {
+ unsetSocketFactory(previous);
+ }
+ }
+
+ @Override
+ public SearchResult next() throws NamingException {
+ ClassLoader previous = setSocketFactory();
+ try {
+ return delegating.next();
+ } finally {
+ unsetSocketFactory(previous);
+ }
+ }
+
+ @Override
+ public boolean hasMore() throws NamingException {
+ ClassLoader previous = setSocketFactory();
+ try {
+ return delegating.hasMore();
+ } finally {
+ unsetSocketFactory(previous);
+ }
+ }
+
+ @Override
+ public void close() throws NamingException {
+ delegating.close();
+ }
+ };
+ }
+
+ public DelegatingLdapContext wrapReferralContextObtaining(ReferralException e) throws NamingException {
+ ClassLoader previous = setSocketFactory();
+ try {
+ return new DelegatingLdapContext((DirContext) e.getReferralContext(), socketFactory);
+ } finally {
+ unsetSocketFactory(previous);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + "->" + delegating.toString();
+ }
+
+ // LdapContext specific
+
+ @Override
+ public ExtendedResponse extendedOperation(ExtendedRequest request) throws NamingException {
+ if (!(delegating instanceof LdapContext))
+ throw Assert.unsupported();
+ return ((LdapContext) delegating).extendedOperation(request);
+ }
+
+ @Override
+ public LdapContext newInstance(Control[] requestControls) throws NamingException {
+ if (!(delegating instanceof LdapContext))
+ throw Assert.unsupported();
+ LdapContext newContext = ((LdapContext) delegating).newInstance(requestControls);
+ return new DelegatingLdapContext(newContext, socketFactory);
+ }
+
+ @Override
+ public void reconnect(Control[] controls) throws NamingException {
+ if (!(delegating instanceof LdapContext))
+ throw Assert.unsupported();
+ ClassLoader previous = setSocketFactory();
+ try {
+ ((LdapContext) delegating).reconnect(controls);
+ } finally {
+ unsetSocketFactory(previous);
+ }
+ }
+
+ @Override
+ public Control[] getConnectControls() throws NamingException {
+ if (!(delegating instanceof LdapContext))
+ throw Assert.unsupported();
+ return ((LdapContext) delegating).getConnectControls();
+ }
+
+ @Override
+ public void setRequestControls(Control[] requestControls) throws NamingException {
+ if (!(delegating instanceof LdapContext))
+ throw Assert.unsupported();
+ ((LdapContext) delegating).setRequestControls(requestControls);
+ }
+
+ @Override
+ public Control[] getRequestControls() throws NamingException {
+ if (!(delegating instanceof LdapContext))
+ throw Assert.unsupported();
+ return ((LdapContext) delegating).getRequestControls();
+ }
+
+ @Override
+ public Control[] getResponseControls() throws NamingException {
+ if (!(delegating instanceof LdapContext))
+ throw Assert.unsupported();
+ return ((LdapContext) delegating).getResponseControls();
+ }
+
+ // DirContext methods delegates only
+
+ @Override
+ public void bind(String name, Object obj, Attributes attrs) throws NamingException {
+ delegating.bind(name, obj, attrs);
+ }
+
+ @Override
+ public Attributes getAttributes(Name name) throws NamingException {
+ return delegating.getAttributes(name);
+ }
+
+ @Override
+ public Attributes getAttributes(String name) throws NamingException {
+ return delegating.getAttributes(name);
+ }
+
+ @Override
+ public Attributes getAttributes(Name name, String[] attrIds) throws NamingException {
+ return delegating.getAttributes(name, attrIds);
+ }
+
+ @Override
+ public Attributes getAttributes(String name, String[] attrIds) throws NamingException {
+ return delegating.getAttributes(name, attrIds);
+ }
+
+ @Override
+ public void modifyAttributes(Name name, int mod_op, Attributes attrs) throws NamingException {
+ delegating.modifyAttributes(name, mod_op, attrs);
+ }
+
+ @Override
+ public void modifyAttributes(String name, int mod_op, Attributes attrs) throws NamingException {
+ delegating.modifyAttributes(name, mod_op, attrs);
+ }
+
+ @Override
+ public void modifyAttributes(Name name, ModificationItem[] mods) throws NamingException {
+ delegating.modifyAttributes(name, mods);
+ }
+
+ @Override
+ public void modifyAttributes(String name, ModificationItem[] mods) throws NamingException {
+ delegating.modifyAttributes(name, mods);
+ }
+
+ @Override
+ public void bind(Name name, Object obj, Attributes attrs) throws NamingException {
+ delegating.bind(name, obj, attrs);
+ }
+
+ @Override
+ public void rebind(Name name, Object obj, Attributes attrs) throws NamingException {
+ delegating.rebind(name, obj, attrs);
+ }
+
+ @Override
+ public void rebind(String name, Object obj, Attributes attrs) throws NamingException {
+ delegating.rebind(name, obj, attrs);
+ }
+
+ @Override
+ public DirContext createSubcontext(Name name, Attributes attrs) throws NamingException {
+ return delegating.createSubcontext(name, attrs);
+ }
+
+ @Override
+ public DirContext createSubcontext(String name, Attributes attrs) throws NamingException {
+ return delegating.createSubcontext(name, attrs);
+ }
+
+ @Override
+ public DirContext getSchema(Name name) throws NamingException {
+ return delegating.getSchema(name);
+ }
+
+ @Override
+ public DirContext getSchema(String name) throws NamingException {
+ return delegating.getSchema(name);
+ }
+
+ @Override
+ public DirContext getSchemaClassDefinition(Name name) throws NamingException {
+ return delegating.getSchemaClassDefinition(name);
+ }
+
+ @Override
+ public DirContext getSchemaClassDefinition(String name) throws NamingException {
+ return delegating.getSchemaClassDefinition(name);
+ }
+
+ @Override
+ public NamingEnumeration search(Name name, Attributes matchingAttributes, String[] attributesToReturn)
+ throws NamingException {
+ return wrap(delegating.search(name, matchingAttributes, attributesToReturn));
+ }
+
+ @Override
+ public NamingEnumeration search(String name, Attributes matchingAttributes, String[] attributesToReturn)
+ throws NamingException {
+ return wrap(delegating.search(name, matchingAttributes, attributesToReturn));
+ }
+
+ @Override
+ public NamingEnumeration search(Name name, Attributes matchingAttributes) throws NamingException {
+ return wrap(delegating.search(name, matchingAttributes));
+ }
+
+ @Override
+ public NamingEnumeration search(String name, Attributes matchingAttributes) throws NamingException {
+ return wrap(delegating.search(name, matchingAttributes));
+ }
+
+ @Override
+ public NamingEnumeration search(Name name, String filter, SearchControls cons) throws NamingException {
+ return wrap(delegating.search(name, filter, cons));
+ }
+
+ @Override
+ public NamingEnumeration search(String name, String filter, SearchControls cons) throws NamingException {
+ return wrap(delegating.search(name, filter, cons));
+ }
+
+ @Override
+ public NamingEnumeration search(Name name, String filterExpr, Object[] filterArgs, SearchControls cons)
+ throws NamingException {
+ return wrap(delegating.search(name, filterExpr, filterArgs, cons));
+ }
+
+ @Override
+ public NamingEnumeration search(String name, String filterExpr, Object[] filterArgs, SearchControls cons)
+ throws NamingException {
+ return wrap(delegating.search(name, filterExpr, filterArgs, cons));
+ }
+
+ @Override
+ public Object lookup(Name name) throws NamingException {
+ return delegating.lookup(name);
+ }
+
+ @Override
+ public Object lookup(String name) throws NamingException {
+ return delegating.lookup(name);
+ }
+
+ @Override
+ public void bind(Name name, Object obj) throws NamingException {
+ delegating.bind(name, obj);
+ }
+
+ @Override
+ public void bind(String name, Object obj) throws NamingException {
+ delegating.bind(name, obj);
+ }
+
+ @Override
+ public void rebind(Name name, Object obj) throws NamingException {
+ delegating.rebind(name, obj);
+ }
+
+ @Override
+ public void rebind(String name, Object obj) throws NamingException {
+ delegating.rebind(name, obj);
+ }
+
+ @Override
+ public void unbind(Name name) throws NamingException {
+ delegating.unbind(name);
+ }
+
+ @Override
+ public void unbind(String name) throws NamingException {
+ delegating.unbind(name);
+ }
+
+ @Override
+ public void rename(Name oldName, Name newName) throws NamingException {
+ delegating.rename(oldName, newName);
+ }
+
+ @Override
+ public void rename(String oldName, String newName) throws NamingException {
+ delegating.rename(oldName, newName);
+ }
+
+ @Override
+ public NamingEnumeration list(Name name) throws NamingException {
+ return delegating.list(name);
+ }
+
+ @Override
+ public NamingEnumeration list(String name) throws NamingException {
+ return delegating.list(name);
+ }
+
+ @Override
+ public NamingEnumeration listBindings(Name name) throws NamingException {
+ return delegating.listBindings(name);
+ }
+
+ @Override
+ public NamingEnumeration listBindings(String name) throws NamingException {
+ return delegating.listBindings(name);
+ }
+
+ @Override
+ public void destroySubcontext(Name name) throws NamingException {
+ delegating.destroySubcontext(name);
+ }
+
+ @Override
+ public void destroySubcontext(String name) throws NamingException {
+ delegating.destroySubcontext(name);
+ }
+
+ @Override
+ public Context createSubcontext(Name name) throws NamingException {
+ return delegating.createSubcontext(name);
+ }
+
+ @Override
+ public Context createSubcontext(String name) throws NamingException {
+ return delegating.createSubcontext(name);
+ }
+
+ @Override
+ public Object lookupLink(Name name) throws NamingException {
+ return delegating.lookupLink(name);
+ }
+
+ @Override
+ public Object lookupLink(String name) throws NamingException {
+ return delegating.lookupLink(name);
+ }
+
+ @Override
+ public NameParser getNameParser(Name name) throws NamingException {
+ return delegating.getNameParser(name);
+ }
+
+ @Override
+ public NameParser getNameParser(String name) throws NamingException {
+ return delegating.getNameParser(name);
+ }
+
+ @Override
+ public Name composeName(Name name, Name prefix) throws NamingException {
+ return delegating.composeName(name, prefix);
+ }
+
+ @Override
+ public String composeName(String name, String prefix) throws NamingException {
+ return delegating.composeName(name, prefix);
+ }
+
+ @Override
+ public Object addToEnvironment(String propName, Object propVal) throws NamingException {
+ return delegating.addToEnvironment(propName, propVal);
+ }
+
+ @Override
+ public Object removeFromEnvironment(String propName) throws NamingException {
+ return delegating.removeFromEnvironment(propName);
+ }
+
+ @Override
+ public Hashtable, ?> getEnvironment() throws NamingException {
+ return delegating.getEnvironment();
+ }
+
+ @Override
+ public String getNameInNamespace() throws NamingException {
+ return delegating.getNameInNamespace();
+ }
+
+ private ClassLoader setSocketFactory() {
+ if (socketFactory != null) {
+ ThreadLocalSSLSocketFactory.set(socketFactory);
+ return setClassLoaderTo(getSocketFactoryClassLoader());
+ }
+ return null;
+ }
+
+ private void unsetSocketFactory(ClassLoader previous) {
+ if (socketFactory != null) {
+ ThreadLocalSSLSocketFactory.unset();
+ setClassLoaderTo(previous);
+ }
+ }
+
+ private ClassLoader getSocketFactoryClassLoader() {
+ return ThreadLocalSSLSocketFactory.class.getClassLoader();
+ }
+
+ private ClassLoader setClassLoaderTo(final ClassLoader targetClassLoader) {
+ return doPrivileged(new SetContextClassLoaderAction(targetClassLoader));
+ }
+
+ private static T doPrivileged(final PrivilegedAction action) {
+ return System.getSecurityManager() != null ? AccessController.doPrivileged(action) : action.run();
+ }
+}
diff --git a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/LdapRecorder.java b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/LdapRecorder.java
new file mode 100644
index 0000000000000..83dac6927d9e8
--- /dev/null
+++ b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/LdapRecorder.java
@@ -0,0 +1,69 @@
+package io.quarkus.elytron.security.ldap;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.naming.NamingException;
+import javax.naming.directory.DirContext;
+
+import org.wildfly.common.function.ExceptionSupplier;
+import org.wildfly.security.auth.realm.ldap.AttributeMapping;
+import org.wildfly.security.auth.realm.ldap.DirContextFactory;
+import org.wildfly.security.auth.realm.ldap.LdapSecurityRealmBuilder;
+import org.wildfly.security.auth.server.SecurityRealm;
+
+import io.quarkus.elytron.security.ldap.config.AttributeMappingConfig;
+import io.quarkus.elytron.security.ldap.config.DirContextConfig;
+import io.quarkus.elytron.security.ldap.config.IdentityMappingConfig;
+import io.quarkus.elytron.security.ldap.config.LdapSecurityRealmConfig;
+import io.quarkus.runtime.RuntimeValue;
+import io.quarkus.runtime.annotations.Recorder;
+
+@Recorder
+public class LdapRecorder {
+
+ /**
+ * Create a runtime value for a {@linkplain LdapSecurityRealm}
+ *
+ * @param config - the realm config
+ * @return - runtime value wrapper for the SecurityRealm
+ */
+ public RuntimeValue createRealm(LdapSecurityRealmConfig config) {
+ LdapSecurityRealmBuilder builder = LdapSecurityRealmBuilder.builder()
+ .setDirContextSupplier(createDirContextSupplier(config.dirContext))
+ .identityMapping()
+ .map(createAttributeMappings(config.identityMapping))
+ .setRdnIdentifier(config.identityMapping.rdnIdentifier)
+ .setSearchDn(config.identityMapping.searchBaseDn)
+ .build();
+
+ if (config.directVerification) {
+ builder.addDirectEvidenceVerification(false);
+ }
+
+ return new RuntimeValue<>(builder.build());
+ }
+
+ private ExceptionSupplier createDirContextSupplier(DirContextConfig dirContext) {
+ DirContextFactory dirContextFactory = new QuarkusDirContextFactory(
+ dirContext.url,
+ dirContext.principal,
+ dirContext.password);
+ return () -> dirContextFactory.obtainDirContext(DirContextFactory.ReferralMode.IGNORE);
+ }
+
+ private AttributeMapping[] createAttributeMappings(IdentityMappingConfig identityMappingConfig) {
+ List attributeMappings = new ArrayList<>();
+
+ for (AttributeMappingConfig attributeMappingConfig : identityMappingConfig.attributeMappings.values()) {
+ attributeMappings.add(AttributeMapping.fromFilter(attributeMappingConfig.filter)
+ .from(attributeMappingConfig.from)
+ .to(attributeMappingConfig.to)
+ .searchDn(attributeMappingConfig.filterBaseDn)
+ .build());
+ }
+
+ AttributeMapping[] attributeMappingsArray = new AttributeMapping[attributeMappings.size()];
+ return attributeMappings.toArray(attributeMappingsArray);
+ }
+}
diff --git a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/QuarkusDirContextFactory.java b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/QuarkusDirContextFactory.java
new file mode 100644
index 0000000000000..6dde3648e0826
--- /dev/null
+++ b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/QuarkusDirContextFactory.java
@@ -0,0 +1,151 @@
+package io.quarkus.elytron.security.ldap;
+
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.Hashtable;
+
+import javax.naming.NamingException;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.InitialDirContext;
+import javax.naming.ldap.InitialLdapContext;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+
+import org.wildfly.security.auth.realm.ldap.DirContextFactory;
+import org.wildfly.security.manager.action.SetContextClassLoaderAction;
+
+public class QuarkusDirContextFactory implements DirContextFactory {
+ // private static final ElytronMessages log = Logger.getMessageLogger(ElytronMessages.class, "org.wildfly.security");
+
+ private static final String CONNECT_TIMEOUT = "com.sun.jndi.ldap.connect.timeout";
+ private static final String READ_TIMEOUT = "com.sun.jndi.ldap.read.timeout";
+ private static final String SOCKET_FACTORY = "java.naming.ldap.factory.socket";
+ public static final String INITIAL_CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";
+ private static final String SECURITY_AUTHENTICATION = "simple";
+
+ private static final String DEFAULT_CONNECT_TIMEOUT = "5000"; // ms
+ private static final String DEFAULT_READ_TIMEOUT = "60000"; // ms
+ private static final String LDAPS_SCHEME = "ldaps";
+
+ private final String providerUrl;
+ private final String securityPrincipal;
+ private final String securityCredential;
+ private final ClassLoader targetClassLoader;
+
+ public QuarkusDirContextFactory(String providerUrl, String securityPrincipal, String securityCredential) {
+ this.providerUrl = providerUrl;
+ this.securityPrincipal = securityPrincipal;
+ this.securityCredential = securityCredential;
+ this.targetClassLoader = getClass().getClassLoader();
+ }
+
+ @Override
+ public DirContext obtainDirContext(ReferralMode mode) throws NamingException {
+ char[] charPassword = null;
+ if (securityCredential != null) { // password from String
+ charPassword = securityCredential.toCharArray();
+ }
+ return createDirContext(securityPrincipal, charPassword, mode);
+ }
+
+ @Override
+ public DirContext obtainDirContext(CallbackHandler handler, ReferralMode mode) throws NamingException {
+ NameCallback nameCallback = new NameCallback("Principal Name");
+ PasswordCallback passwordCallback = new PasswordCallback("Password", false);
+
+ try {
+ handler.handle(new Callback[] { nameCallback, passwordCallback });
+ } catch (Exception e) {
+ throw new RuntimeException("Could not obtain credential", e);
+ // throw log.couldNotObtainCredentialWithCause(e);
+ }
+
+ String securityPrincipal = nameCallback.getName();
+
+ if (securityPrincipal == null) {
+ throw new RuntimeException("Could not obtain principal");
+ // throw log.couldNotObtainPrincipal();
+ }
+
+ char[] securityCredential = passwordCallback.getPassword();
+
+ if (securityCredential == null) {
+ throw new RuntimeException("Could not obtain credential");
+ // throw log.couldNotObtainCredential();
+ }
+
+ return createDirContext(securityPrincipal, securityCredential, mode);
+ }
+
+ private DirContext createDirContext(String securityPrincipal, char[] securityCredential, ReferralMode mode)
+ throws NamingException {
+ final ClassLoader oldClassLoader = setClassLoaderTo(targetClassLoader);
+ try {
+ Hashtable env = new Hashtable<>();
+
+ env.put(InitialDirContext.INITIAL_CONTEXT_FACTORY, INITIAL_CONTEXT_FACTORY);
+ env.put(InitialDirContext.PROVIDER_URL, providerUrl);
+ env.put(InitialDirContext.SECURITY_AUTHENTICATION, SECURITY_AUTHENTICATION);
+ if (securityPrincipal != null) {
+ env.put(InitialDirContext.SECURITY_PRINCIPAL, securityPrincipal);
+ }
+ if (securityCredential != null) {
+ env.put(InitialDirContext.SECURITY_CREDENTIALS, securityCredential);
+ }
+ env.put(InitialDirContext.REFERRAL, mode == null ? ReferralMode.IGNORE.getValue() : mode.getValue());
+ env.put(CONNECT_TIMEOUT, DEFAULT_CONNECT_TIMEOUT);
+ env.put(READ_TIMEOUT, DEFAULT_READ_TIMEOUT);
+
+ // if (log.isDebugEnabled()) {
+ // log.debugf("Creating [" + InitialDirContext.class + "] with environment:");
+ // env.forEach((key, value) -> log.debugf(" Property [%s] with value [%s]", key,
+ // key != InitialDirContext.SECURITY_CREDENTIALS ? Arrays2.objectToString(value) : "******"));
+ // }
+
+ InitialLdapContext initialContext;
+
+ try {
+ initialContext = new InitialLdapContext(env, null);
+ } catch (NamingException ne) {
+ // log.debugf(ne, "Could not create [%s]. Failed to connect to LDAP server.", InitialLdapContext.class);
+ throw ne;
+ }
+
+ // log.debugf("[%s] successfully created. Connection established to LDAP server.", initialContext);
+
+ return new DelegatingLdapContext(initialContext, this::returnContext, null);
+ } finally {
+ setClassLoaderTo(oldClassLoader);
+ }
+ }
+
+ @Override
+ public void returnContext(DirContext context) {
+
+ if (context == null) {
+ return;
+ }
+
+ if (context instanceof InitialDirContext) {
+ final ClassLoader oldClassLoader = setClassLoaderTo(targetClassLoader);
+ try {
+ context.close();
+ // log.debugf("Context [%s] was closed. Connection closed or just returned to the pool.", context);
+ } catch (NamingException ignored) {
+ } finally {
+ setClassLoaderTo(oldClassLoader);
+ }
+ }
+ }
+
+ private ClassLoader setClassLoaderTo(final ClassLoader targetClassLoader) {
+ return doPrivileged(new SetContextClassLoaderAction(targetClassLoader));
+ }
+
+ private static T doPrivileged(final PrivilegedAction action) {
+ return System.getSecurityManager() != null ? AccessController.doPrivileged(action) : action.run();
+ }
+
+}
diff --git a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/AttributeMappingConfig.java b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/AttributeMappingConfig.java
new file mode 100644
index 0000000000000..5a933bf81e9de
--- /dev/null
+++ b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/AttributeMappingConfig.java
@@ -0,0 +1,35 @@
+package io.quarkus.elytron.security.ldap.config;
+
+import io.quarkus.runtime.annotations.ConfigGroup;
+import io.quarkus.runtime.annotations.ConfigItem;
+
+/**
+ * Configuration information used to populate a {@linkplain org.wildfly.security.auth.realm.ldap.AttributeMapping}
+ */
+@ConfigGroup
+public class AttributeMappingConfig {
+
+ /**
+ * The roleAttributeId from which is mapped (e.g. "cn")
+ */
+ @ConfigItem
+ public String from;
+
+ /**
+ * The identifier whom the attribute is mapped to (in Quarkus: "groups", in WildFly this is "Roles")
+ */
+ @ConfigItem(defaultValue = "groups")
+ public String to;
+
+ /**
+ * The filter (also named "roleFilter")
+ */
+ @ConfigItem
+ public String filter;
+
+ /**
+ * The filter base dn (also named "rolesContextDn")
+ */
+ @ConfigItem
+ public String filterBaseDn;
+}
diff --git a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/DirContextConfig.java b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/DirContextConfig.java
new file mode 100644
index 0000000000000..95476076b255f
--- /dev/null
+++ b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/DirContextConfig.java
@@ -0,0 +1,35 @@
+package io.quarkus.elytron.security.ldap.config;
+
+import io.quarkus.runtime.annotations.ConfigGroup;
+import io.quarkus.runtime.annotations.ConfigItem;
+
+@ConfigGroup
+public class DirContextConfig {
+
+ /**
+ * The url of the ldap server
+ */
+ @ConfigItem
+ public String url;
+
+ /**
+ * The principal: user which is used to connect to ldap server (also named "bindDn")
+ */
+ @ConfigItem
+ public String principal;
+
+ /**
+ * The password which belongs to the principal (also named "bindCredential")
+ */
+ @ConfigItem
+ public String password;
+
+ @Override
+ public String toString() {
+ return "DirContextConfig{" +
+ "url='" + url + '\'' +
+ ", principal='" + principal + '\'' +
+ ", password='" + password + '\'' +
+ '}';
+ }
+}
diff --git a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/IdentityMappingConfig.java b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/IdentityMappingConfig.java
new file mode 100644
index 0000000000000..1846c259df185
--- /dev/null
+++ b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/IdentityMappingConfig.java
@@ -0,0 +1,37 @@
+package io.quarkus.elytron.security.ldap.config;
+
+import java.util.Map;
+
+import io.quarkus.runtime.annotations.ConfigGroup;
+import io.quarkus.runtime.annotations.ConfigItem;
+
+@ConfigGroup
+public class IdentityMappingConfig {
+
+ /**
+ * The identifier which correlates to the provided user (also named "baseFilter")
+ */
+ @ConfigItem(defaultValue = "uid")
+ public String rdnIdentifier;
+
+ /**
+ * The dn where we look for users
+ */
+ @ConfigItem
+ public String searchBaseDn;
+
+ /**
+ * The configs how we get from the attribute to the Role
+ */
+ @ConfigItem
+ public Map attributeMappings;
+
+ @Override
+ public String toString() {
+ return "IdentityMappingConfig{" +
+ "rdnIdentifier='" + rdnIdentifier + '\'' +
+ ", searchBaseDn='" + searchBaseDn + '\'' +
+ ", attributeMappings=" + attributeMappings +
+ '}';
+ }
+}
diff --git a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/LdapSecurityRealmConfig.java b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/LdapSecurityRealmConfig.java
new file mode 100644
index 0000000000000..2e8a1015eaa79
--- /dev/null
+++ b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/LdapSecurityRealmConfig.java
@@ -0,0 +1,54 @@
+package io.quarkus.elytron.security.ldap.config;
+
+import io.quarkus.runtime.annotations.ConfigItem;
+import io.quarkus.runtime.annotations.ConfigPhase;
+import io.quarkus.runtime.annotations.ConfigRoot;
+
+/**
+ * A configuration object for a jdbc based realm configuration,
+ * {@linkplain org.wildfly.security.auth.realm.ldap.LdapSecurityRealm}
+ */
+@ConfigRoot(name = "security.ldap", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED)
+public class LdapSecurityRealmConfig {
+
+ /**
+ * The option to enable the ldap elytron module
+ */
+ @ConfigItem
+ public boolean enabled;
+
+ /**
+ * The elytron realm name
+ */
+ @ConfigItem(defaultValue = "Quarkus")
+ public String realmName;
+
+ /**
+ * Provided credentials are verified against ldap?
+ */
+ @ConfigItem(defaultValue = "true")
+ public boolean directVerification;
+
+ /**
+ * The ldap server configuration
+ */
+ @ConfigItem
+ public DirContextConfig dirContext;
+
+ /**
+ * The config which we use to map an identity
+ */
+ @ConfigItem
+ public IdentityMappingConfig identityMapping;
+
+ @Override
+ public String toString() {
+ return "LdapSecurityRealmConfig{" +
+ "enabled=" + enabled +
+ ", realmName='" + realmName + '\'' +
+ ", directVerification=" + directVerification +
+ ", dirContext=" + dirContext +
+ ", identityMapping=" + identityMapping +
+ '}';
+ }
+}
diff --git a/extensions/elytron-security-ldap/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/elytron-security-ldap/runtime/src/main/resources/META-INF/quarkus-extension.yaml
new file mode 100644
index 0000000000000..4fc9dcd772342
--- /dev/null
+++ b/extensions/elytron-security-ldap/runtime/src/main/resources/META-INF/quarkus-extension.yaml
@@ -0,0 +1,10 @@
+---
+name: "Elytron Security LDAP Realm"
+metadata:
+ keywords:
+ - "security"
+ - "ldap"
+ guide: "https://quarkus.io/guides/security-ldap"
+ categories:
+ - "security"
+ status: "preview"
diff --git a/extensions/pom.xml b/extensions/pom.xml
index 9e4763a2500a7..32fa588d0b59d 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -109,6 +109,7 @@
elytron-security-common
elytron-security
elytron-security-jdbc
+ elytron-security-ldap
elytron-security-properties-file
elytron-security-oauth2
smallrye-jwt
diff --git a/integration-tests/elytron-security-ldap/pom.xml b/integration-tests/elytron-security-ldap/pom.xml
new file mode 100644
index 0000000000000..3df55afc22147
--- /dev/null
+++ b/integration-tests/elytron-security-ldap/pom.xml
@@ -0,0 +1,115 @@
+
+
+ 4.0.0
+
+ io.quarkus
+ quarkus-integration-tests-parent
+ 999-SNAPSHOT
+ ../pom.xml
+
+
+ quarkus-elytron-security-ldap-integration-test
+ Quarkus - Integration Tests - Elytron Security LDAP
+
+
+
+ io.quarkus
+ quarkus-resteasy
+
+
+ io.quarkus
+ quarkus-elytron-security-ldap
+
+
+
+
+ io.quarkus
+ quarkus-test-ldap
+ test
+
+
+ io.quarkus
+ quarkus-junit5
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+
+
+
+
+ io.quarkus
+ quarkus-maven-plugin
+
+
+
+ build
+
+
+
+
+
+
+
+
+
+ native-image
+
+
+ native
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+
+ integration-test
+ verify
+
+
+
+ ${project.build.directory}/${project.build.finalName}-runner
+
+
+
+
+
+
+ io.quarkus
+ quarkus-maven-plugin
+ ${project.version}
+
+
+ native-image
+
+ native-image
+
+
+ false
+ true
+ true
+ false
+ false
+ ${graalvmHome}
+ true
+ false
+
+
+
+
+
+
+
+
+
+
diff --git a/integration-tests/elytron-security-ldap/src/main/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapResource.java b/integration-tests/elytron-security-ldap/src/main/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapResource.java
new file mode 100644
index 0000000000000..1d54f0c2f047f
--- /dev/null
+++ b/integration-tests/elytron-security-ldap/src/main/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapResource.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 io.quarkus.elytron.security.ldap.it;
+
+import javax.annotation.security.RolesAllowed;
+import javax.enterprise.context.ApplicationScoped;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+
+@Path("/api")
+@ApplicationScoped
+public class ElytronSecurityLdapResource {
+
+ @GET
+ @Path("/anonymous")
+ public String anonymous() {
+ return "anonymous";
+ }
+
+ @GET
+ @Path("/requiresStandardRole")
+ @RolesAllowed("standardRole")
+ public String authenticated() {
+ return "authorized";
+ }
+
+ @GET
+ @Path("/requiresAdminRole")
+ @RolesAllowed("adminRole")
+ public String forbidden() {
+ return "authorized";
+ }
+
+}
diff --git a/integration-tests/elytron-security-ldap/src/main/resources/application.properties b/integration-tests/elytron-security-ldap/src/main/resources/application.properties
new file mode 100644
index 0000000000000..e9282033bed8b
--- /dev/null
+++ b/integration-tests/elytron-security-ldap/src/main/resources/application.properties
@@ -0,0 +1,12 @@
+quarkus.security.ldap.enabled=true
+
+quarkus.security.ldap.dir-context.principal=uid=admin,ou=system
+quarkus.security.ldap.dir-context.url=ldap://127.0.0.1:10389
+quarkus.security.ldap.dir-context.password=secret
+
+quarkus.security.ldap.identity-mapping.rdn-identifier=uid
+quarkus.security.ldap.identity-mapping.search-base-dn=ou=Users,dc=quarkus,dc=io
+
+quarkus.security.ldap.identity-mapping.attribute-mappings."0".from=cn
+quarkus.security.ldap.identity-mapping.attribute-mappings."0".filter=(member=uid={0},ou=Users,dc=quarkus,dc=io)
+quarkus.security.ldap.identity-mapping.attribute-mappings."0".filter-base-dn=ou=Roles,dc=quarkus,dc=io
\ No newline at end of file
diff --git a/integration-tests/elytron-security-ldap/src/test/java/io/quarkus/elytron/security/ldap/it/ElytronLdapExtensionTestResources.java b/integration-tests/elytron-security-ldap/src/test/java/io/quarkus/elytron/security/ldap/it/ElytronLdapExtensionTestResources.java
new file mode 100644
index 0000000000000..c0144886d8b05
--- /dev/null
+++ b/integration-tests/elytron-security-ldap/src/test/java/io/quarkus/elytron/security/ldap/it/ElytronLdapExtensionTestResources.java
@@ -0,0 +1,8 @@
+package io.quarkus.elytron.security.ldap.it;
+
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.ldap.LdapServerTestResource;
+
+@QuarkusTestResource(LdapServerTestResource.class)
+public class ElytronLdapExtensionTestResources {
+}
diff --git a/integration-tests/elytron-security-ldap/src/test/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapIT.java b/integration-tests/elytron-security-ldap/src/test/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapIT.java
new file mode 100644
index 0000000000000..f772691da5036
--- /dev/null
+++ b/integration-tests/elytron-security-ldap/src/test/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapIT.java
@@ -0,0 +1,8 @@
+package io.quarkus.elytron.security.ldap.it;
+
+import io.quarkus.test.junit.NativeImageTest;
+
+@NativeImageTest
+class ElytronSecurityLdapIT extends ElytronSecurityLdapTest {
+
+}
diff --git a/integration-tests/elytron-security-ldap/src/test/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapTest.java b/integration-tests/elytron-security-ldap/src/test/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapTest.java
new file mode 100644
index 0000000000000..7c16318bfc8c6
--- /dev/null
+++ b/integration-tests/elytron-security-ldap/src/test/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapTest.java
@@ -0,0 +1,85 @@
+package io.quarkus.elytron.security.ldap.it;
+
+import static org.hamcrest.Matchers.containsString;
+
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.RestAssured;
+
+@QuarkusTest
+class ElytronSecurityLdapTest {
+
+ @Test
+ void anonymous() {
+ RestAssured.given()
+ .when()
+ .get("/api/anonymous")
+ .then()
+ .statusCode(200)
+ .body(containsString("anonymous"));
+ }
+
+ @Test
+ void standard_role_not_authenticated() {
+ RestAssured.given()
+ .redirects().follow(false)
+ .when()
+ .get("/api/requiresStandardRole")
+ .then()
+ .statusCode(401);
+ }
+
+ @Test
+ void standard_role_authenticated() {
+ RestAssured.given()
+ .redirects().follow(false)
+ .when()
+ .auth().preemptive().basic("standardUser", "standardUserPassword")
+ .get("/api/requiresStandardRole")
+ .then()
+ .statusCode(200);
+ }
+
+ @Test
+ void standard_role_not_authorized() {
+ RestAssured.given()
+ .redirects().follow(false)
+ .when()
+ .auth().preemptive().basic("adminUser", "adminUserPassword")
+ .get("/api/requiresStandardRole")
+ .then()
+ .statusCode(403);
+ }
+
+ @Test
+ void admin_role_authorized() {
+ RestAssured.given()
+ .when()
+ .auth().preemptive().basic("adminUser", "adminUserPassword")
+ .get("/api/requiresAdminRole")
+ .then()
+ .statusCode(200);
+ }
+
+ @Test
+ void admin_role_not_authenticated() {
+ RestAssured.given()
+ .redirects().follow(false)
+ .when()
+ .get("/api/requiresAdminRole")
+ .then()
+ .statusCode(401);
+ }
+
+ @Test
+ void admin_role_not_authorized() {
+ RestAssured.given()
+ .redirects().follow(false)
+ .when()
+ .auth().preemptive().basic("standardUser", "standardUserPassword")
+ .get("/api/requiresAdminRole")
+ .then()
+ .statusCode(403);
+ }
+}
diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml
index 12a7627c49295..fd154577b50e2 100644
--- a/integration-tests/pom.xml
+++ b/integration-tests/pom.xml
@@ -92,6 +92,7 @@
narayana-stm
narayana-jta
elytron-security-jdbc
+ elytron-security-ldap
vertx-graphql
jpa-without-entity
quartz