diff --git a/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/CacheTest.java b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/CacheTest.java new file mode 100644 index 0000000000000..317f3b7485a5a --- /dev/null +++ b/extensions/elytron-security-ldap/deployment/src/test/java/io/quarkus/elytron/security/ldap/CacheTest.java @@ -0,0 +1,30 @@ +package io.quarkus.elytron.security.ldap; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.elytron.security.ldap.rest.SingleRoleSecuredServlet; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.ldap.LdapServerTestResource; +import io.restassured.RestAssured; + +@QuarkusTestResource(LdapServerTestResource.class) +public class CacheTest { + protected static Class[] testClasses = { + SingleRoleSecuredServlet.class + }; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(testClasses) + .addAsResource("cache/application.properties", "application.properties")); + + @Test() + public void testNoCacheFailure() { + RestAssured.given().auth().preemptive().basic("standardUser", "standardUserPassword") + .when().get("/servlet-secured").then() + .statusCode(200); + } +} diff --git a/extensions/elytron-security-ldap/deployment/src/test/resources/cache/application.properties b/extensions/elytron-security-ldap/deployment/src/test/resources/cache/application.properties new file mode 100644 index 0000000000000..c2efe52af4cc0 --- /dev/null +++ b/extensions/elytron-security-ldap/deployment/src/test/resources/cache/application.properties @@ -0,0 +1,15 @@ +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 + +quarkus.security.ldap.cache.enabled=true +quarkus.security.ldap.cache.max-age=60s +quarkus.security.ldap.cache.size=10 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 index 71aaec3f65fdd..3659109fe97f8 100644 --- 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 @@ -6,11 +6,16 @@ import javax.naming.NamingException; import javax.naming.directory.DirContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.wildfly.common.function.ExceptionSupplier; +import org.wildfly.security.auth.realm.CacheableSecurityRealm; +import org.wildfly.security.auth.realm.CachingSecurityRealm; 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 org.wildfly.security.cache.LRURealmIdentityCache; import io.quarkus.elytron.security.ldap.config.AttributeMappingConfig; import io.quarkus.elytron.security.ldap.config.DirContextConfig; @@ -22,6 +27,8 @@ @Recorder public class LdapRecorder { + private static final Logger log = LoggerFactory.getLogger(LdapRecorder.class); + /** * Create a runtime value for a {@linkplain LdapSecurityRealm} * @@ -47,7 +54,19 @@ public RuntimeValue createRealm(LdapSecurityRealmRuntimeConfig ru ldapSecurityRealmBuilder.addDirectEvidenceVerification(false); } - return new RuntimeValue<>(ldapSecurityRealmBuilder.build()); + SecurityRealm ldapRealm = ldapSecurityRealmBuilder.build(); + + if (runtimeConfig.cache().enabled()) { + if (ldapRealm instanceof CacheableSecurityRealm) { + ldapRealm = new CachingSecurityRealm(ldapRealm, + new LRURealmIdentityCache(runtimeConfig.cache().size(), runtimeConfig.cache().maxAge().toMillis())); + } else { + log.warn( + "Created LDAP realm is not cacheable. Caching of the 'SecurityRealm' won't be available. Please, report this issue."); + } + } + + return new RuntimeValue<>(ldapRealm); } private static ExceptionSupplier createDirContextSupplier(DirContextConfig dirContext) { diff --git a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/CacheConfig.java b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/CacheConfig.java new file mode 100644 index 0000000000000..b55328c77937c --- /dev/null +++ b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/CacheConfig.java @@ -0,0 +1,30 @@ +package io.quarkus.elytron.security.ldap.config; + +import java.time.Duration; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.smallrye.config.WithDefault; + +@ConfigGroup +public interface CacheConfig { + + /** + * If set to true, request to the LDAP server are cached + */ + @WithDefault("false") + boolean enabled(); + + /** + * The duration that an entry can stay in the cache + */ + @WithDefault("60s") + Duration maxAge(); + + /** + * The maximum number of entries to keep in the cache + */ + @WithDefault("100") + int size(); + + String toString(); +} diff --git a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/LdapSecurityRealmRuntimeConfig.java b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/LdapSecurityRealmRuntimeConfig.java index b49eebd8b0bae..4ce264288cf06 100644 --- a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/LdapSecurityRealmRuntimeConfig.java +++ b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/config/LdapSecurityRealmRuntimeConfig.java @@ -24,6 +24,11 @@ public interface LdapSecurityRealmRuntimeConfig { */ DirContextConfig dirContext(); + /** + * The LDAP cache configuration + */ + CacheConfig cache(); + /** * The config which we use to map an identity */ diff --git a/integration-tests/elytron-security-ldap/src/main/resources/application.properties b/integration-tests/elytron-security-ldap/src/main/resources/application.properties index e9282033bed8b..4454fc4de0f62 100644 --- a/integration-tests/elytron-security-ldap/src/main/resources/application.properties +++ b/integration-tests/elytron-security-ldap/src/main/resources/application.properties @@ -9,4 +9,8 @@ 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 +quarkus.security.ldap.identity-mapping.attribute-mappings."0".filter-base-dn=ou=Roles,dc=quarkus,dc=io + +quarkus.security.ldap.cache.enabled=true +quarkus.security.ldap.cache.max-age=60s +quarkus.security.ldap.cache.size=10 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 index 7c16318bfc8c6..e2e107bdb26e6 100644 --- 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 @@ -2,15 +2,22 @@ import static org.hamcrest.Matchers.containsString; +import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import com.unboundid.ldap.listener.InMemoryDirectoryServer; +import com.unboundid.ldap.sdk.LDAPException; + import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; @QuarkusTest class ElytronSecurityLdapTest { + InMemoryDirectoryServer ldapServer; + @Test + @Order(1) void anonymous() { RestAssured.given() .when() @@ -21,6 +28,7 @@ void anonymous() { } @Test + @Order(2) void standard_role_not_authenticated() { RestAssured.given() .redirects().follow(false) @@ -31,6 +39,7 @@ void standard_role_not_authenticated() { } @Test + @Order(3) void standard_role_authenticated() { RestAssured.given() .redirects().follow(false) @@ -42,6 +51,7 @@ void standard_role_authenticated() { } @Test + @Order(4) void standard_role_not_authorized() { RestAssured.given() .redirects().follow(false) @@ -53,6 +63,7 @@ void standard_role_not_authorized() { } @Test + @Order(5) void admin_role_authorized() { RestAssured.given() .when() @@ -63,6 +74,7 @@ void admin_role_authorized() { } @Test + @Order(6) void admin_role_not_authenticated() { RestAssured.given() .redirects().follow(false) @@ -73,6 +85,7 @@ void admin_role_not_authenticated() { } @Test + @Order(7) void admin_role_not_authorized() { RestAssured.given() .redirects().follow(false) @@ -82,4 +95,27 @@ void admin_role_not_authorized() { .then() .statusCode(403); } + + @Test() + @Order(8) + void standard_role_authenticated_cached() throws LDAPException { + RestAssured.given() + .redirects().follow(false) + .when() + .auth().preemptive().basic("standardUser", "standardUserPassword") + .get("/api/requiresStandardRole") + .then() + .statusCode(200); + + ldapServer.shutDown(false); + + RestAssured.given() + .redirects().follow(false) + .when() + .auth().preemptive().basic("standardUser", "standardUserPassword") + .get("/api/requiresStandardRole") + .then() + .statusCode(200); + } + } diff --git a/test-framework/ldap/src/main/java/io/quarkus/test/ldap/LdapServerTestResource.java b/test-framework/ldap/src/main/java/io/quarkus/test/ldap/LdapServerTestResource.java index c8aa6fe9541c5..1a71f6fac1e9d 100644 --- a/test-framework/ldap/src/main/java/io/quarkus/test/ldap/LdapServerTestResource.java +++ b/test-framework/ldap/src/main/java/io/quarkus/test/ldap/LdapServerTestResource.java @@ -49,4 +49,9 @@ public synchronized void stop() { ldapServer = null; } } + + @Override + public void inject(TestInjector testInjector) { + testInjector.injectIntoFields(ldapServer, new TestInjector.MatchesType(InMemoryDirectoryServer.class)); + } }