Skip to content

Commit

Permalink
Merge pull request #39665 from jonasjensen-brolstark/ldap-cache
Browse files Browse the repository at this point in the history
Add Elytron ldap cache
  • Loading branch information
geoand authored Mar 29, 2024
2 parents 27a7bad + 4dc83f9 commit 4f28004
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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}
*
Expand All @@ -47,7 +54,19 @@ public RuntimeValue<SecurityRealm> 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<DirContext, NamingException> createDirContextSupplier(DirContextConfig dirContext) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ public interface LdapSecurityRealmRuntimeConfig {
*/
DirContextConfig dirContext();

/**
* The LDAP cache configuration
*/
CacheConfig cache();

/**
* The config which we use to map an identity
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -21,6 +28,7 @@ void anonymous() {
}

@Test
@Order(2)
void standard_role_not_authenticated() {
RestAssured.given()
.redirects().follow(false)
Expand All @@ -31,6 +39,7 @@ void standard_role_not_authenticated() {
}

@Test
@Order(3)
void standard_role_authenticated() {
RestAssured.given()
.redirects().follow(false)
Expand All @@ -42,6 +51,7 @@ void standard_role_authenticated() {
}

@Test
@Order(4)
void standard_role_not_authorized() {
RestAssured.given()
.redirects().follow(false)
Expand All @@ -53,6 +63,7 @@ void standard_role_not_authorized() {
}

@Test
@Order(5)
void admin_role_authorized() {
RestAssured.given()
.when()
Expand All @@ -63,6 +74,7 @@ void admin_role_authorized() {
}

@Test
@Order(6)
void admin_role_not_authenticated() {
RestAssured.given()
.redirects().follow(false)
Expand All @@ -73,6 +85,7 @@ void admin_role_not_authenticated() {
}

@Test
@Order(7)
void admin_role_not_authorized() {
RestAssured.given()
.redirects().follow(false)
Expand All @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,9 @@ public synchronized void stop() {
ldapServer = null;
}
}

@Override
public void inject(TestInjector testInjector) {
testInjector.injectIntoFields(ldapServer, new TestInjector.MatchesType(InMemoryDirectoryServer.class));
}
}

0 comments on commit 4f28004

Please sign in to comment.