Skip to content

Commit 8d22b45

Browse files
Add caching logic around Certificate retrieval
1 parent 5c643e1 commit 8d22b45

File tree

1 file changed

+129
-8
lines changed

1 file changed

+129
-8
lines changed

src/main/java/com/scality/keycloak/truststore/JpaCertificateTruststoreProvider.java

Lines changed: 129 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@
1010
import java.security.cert.CertificateException;
1111
import java.security.cert.CertificateFactory;
1212
import java.security.cert.X509Certificate;
13+
import java.util.Arrays;
1314
import java.util.Base64;
1415
import java.util.List;
16+
import java.util.concurrent.ConcurrentHashMap;
17+
import java.util.concurrent.atomic.AtomicLong;
18+
import java.util.concurrent.atomic.AtomicReference;
1519

1620
import org.bouncycastle.asn1.x500.RDN;
1721
import org.bouncycastle.asn1.x500.X500Name;
@@ -40,6 +44,12 @@ public class JpaCertificateTruststoreProvider implements CertificateTruststorePr
4044
private final KeycloakSession session;
4145
protected static final Logger logger = Logger.getLogger(JpaCertificateTruststoreProvider.class);
4246

47+
// Cache for certificates to fallback when database is unavailable
48+
private static final AtomicReference<CertificateRepresentation[]> cachedAllCertificates = new AtomicReference<>(null);
49+
private static final ConcurrentHashMap<Boolean, AtomicReference<CertificateRepresentation[]>> cachedFilteredCertificates = new ConcurrentHashMap<>();
50+
private static final AtomicLong cacheTimestamp = new AtomicLong(0);
51+
private static final long CACHE_MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes
52+
4353
public JpaCertificateTruststoreProvider(KeycloakSession session) {
4454
this.session = session;
4555
}
@@ -117,6 +127,45 @@ private boolean isHibernateFlushError(Throwable e) {
117127
return isHibernateFlushError(e.getCause());
118128
}
119129

130+
/**
131+
* Updates the cache with the provided certificates.
132+
*/
133+
private void updateCache(CertificateRepresentation[] certificates) {
134+
if (certificates != null) {
135+
cachedAllCertificates.set(certificates);
136+
cacheTimestamp.set(System.currentTimeMillis());
137+
logger.debugf("Updated certificate cache with %d certificates", certificates.length);
138+
}
139+
}
140+
141+
/**
142+
* Updates the cache for filtered certificates (by isRootCA).
143+
*/
144+
private void updateFilteredCache(boolean isRootCA, CertificateRepresentation[] certificates) {
145+
if (certificates != null) {
146+
cachedFilteredCertificates.computeIfAbsent(isRootCA, k -> new AtomicReference<>()).set(certificates);
147+
logger.debugf("Updated filtered certificate cache (isRootCA=%s) with %d certificates", isRootCA, certificates.length);
148+
}
149+
}
150+
151+
/**
152+
* Invalidates the cache. Should be called when certificates are added, updated, or removed.
153+
*/
154+
private void invalidateCache() {
155+
cachedAllCertificates.set(null);
156+
cachedFilteredCertificates.clear();
157+
cacheTimestamp.set(0);
158+
logger.debug("Invalidated certificate cache");
159+
}
160+
161+
/**
162+
* Checks if the cache is still valid (not too old).
163+
*/
164+
private boolean isCacheValid() {
165+
long age = System.currentTimeMillis() - cacheTimestamp.get();
166+
return age < CACHE_MAX_AGE_MS && cachedAllCertificates.get() != null;
167+
}
168+
120169
@Override
121170
public void close() {
122171
// nothing to close
@@ -254,6 +303,9 @@ public CertificateRepresentation addCertificate(String alias, String certificate
254303
getEntityManager().flush();
255304
getEntityManager().clear();
256305

306+
// Invalidate cache since we added a new certificate
307+
invalidateCache();
308+
257309
return toCertificateRepresentation(entity);
258310
}
259311

@@ -274,6 +326,9 @@ public CertificateRepresentation updateCertificate(String alias, String certific
274326
getEntityManager().flush();
275327
getEntityManager().clear();
276328

329+
// Invalidate cache since we updated a certificate
330+
invalidateCache();
331+
277332
return toCertificateRepresentation(entity);
278333
}
279334

@@ -286,12 +341,15 @@ public void removeCertificate(String alias) {
286341
getEntityManager().remove(entity);
287342
getEntityManager().flush();
288343
getEntityManager().clear();
344+
345+
// Invalidate cache since we removed a certificate
346+
invalidateCache();
289347
}
290348

291349
@Override
292350
public CertificateRepresentation[] getCertificates() {
293351
try {
294-
return Retry.call((iteration) -> {
352+
CertificateRepresentation[] result = Retry.call((iteration) -> {
295353
try {
296354
EntityManager em = getEntityManager();
297355
// Set flush mode to COMMIT to prevent automatic flushing before query execution
@@ -307,9 +365,12 @@ public CertificateRepresentation[] getCertificates() {
307365
.setHint(AvailableHints.HINT_CACHEABLE, false)
308366
.setHint(AvailableHints.HINT_CACHE_MODE, CacheMode.IGNORE)
309367
.getResultList();
310-
return list.stream()
368+
CertificateRepresentation[] certificates = list.stream()
311369
.map(this::toCertificateRepresentation)
312370
.toArray(CertificateRepresentation[]::new);
371+
// Update cache on successful retrieval
372+
updateCache(certificates);
373+
return certificates;
313374
} finally {
314375
// Restore original flush mode
315376
em.setFlushMode(originalFlushMode);
@@ -325,16 +386,30 @@ public CertificateRepresentation[] getCertificates() {
325386
throw e;
326387
}
327388
}, 3, 50); // 3 attempts with 50ms delay
389+
return result;
328390
} catch (Exception e) {
329-
logger.error("Failed to get certificates after retries", e);
330-
throw new RuntimeException("Failed to get certificates", e);
391+
logger.error("Failed to get certificates after retries, attempting cache fallback", e);
392+
// Fallback to cache if available and valid
393+
CertificateRepresentation[] cached = cachedAllCertificates.get();
394+
if (cached != null && isCacheValid()) {
395+
logger.warnf("Using cached certificates (count: %d) due to database failure. Cache age: %d ms",
396+
cached.length, System.currentTimeMillis() - cacheTimestamp.get());
397+
return cached;
398+
} else if (cached != null) {
399+
logger.warnf("Cache exists but is expired (age: %d ms). Using expired cache as last resort.",
400+
System.currentTimeMillis() - cacheTimestamp.get());
401+
return cached;
402+
} else {
403+
logger.error("No cached certificates available, cannot fallback");
404+
throw new RuntimeException("Failed to get certificates and no cache available", e);
405+
}
331406
}
332407
}
333408

334409
@Override
335410
public CertificateRepresentation[] getCertificates(boolean isRootCA) {
336411
try {
337-
return Retry.call((iteration) -> {
412+
CertificateRepresentation[] result = Retry.call((iteration) -> {
338413
try {
339414
EntityManager em = getEntityManager();
340415
// Set flush mode to COMMIT to prevent automatic flushing before query execution
@@ -343,13 +418,16 @@ public CertificateRepresentation[] getCertificates(boolean isRootCA) {
343418
FlushModeType originalFlushMode = em.getFlushMode();
344419
try {
345420
em.setFlushMode(FlushModeType.COMMIT);
346-
return em
421+
CertificateRepresentation[] certificates = em
347422
.createNamedQuery("findByIsRootCA", TruststoreEntity.class)
348423
.setParameter("isRootCA", isRootCA)
349424
.getResultList()
350425
.stream()
351426
.map(this::toCertificateRepresentation)
352427
.toArray(CertificateRepresentation[]::new);
428+
// Update filtered cache on successful retrieval
429+
updateFilteredCache(isRootCA, certificates);
430+
return certificates;
353431
} finally {
354432
// Restore original flush mode
355433
em.setFlushMode(originalFlushMode);
@@ -365,9 +443,52 @@ public CertificateRepresentation[] getCertificates(boolean isRootCA) {
365443
throw e;
366444
}
367445
}, 3, 50); // 3 attempts with 50ms delay
446+
return result;
368447
} catch (Exception e) {
369-
logger.errorf("Failed to get certificates (isRootCA=%s) after retries", isRootCA, e);
370-
throw new RuntimeException("Failed to get certificates", e);
448+
logger.errorf("Failed to get certificates (isRootCA=%s) after retries, attempting cache fallback", isRootCA, e);
449+
// First try filtered cache
450+
AtomicReference<CertificateRepresentation[]> filteredCache = cachedFilteredCertificates.get(isRootCA);
451+
if (filteredCache != null && filteredCache.get() != null) {
452+
logger.warnf("Using cached filtered certificates (isRootCA=%s, count: %d) due to database failure",
453+
isRootCA, filteredCache.get().length);
454+
return filteredCache.get();
455+
}
456+
// Fallback to full cache and filter it
457+
CertificateRepresentation[] cached = cachedAllCertificates.get();
458+
if (cached != null && isCacheValid()) {
459+
logger.warnf("Using cached certificates and filtering (isRootCA=%s) due to database failure. Cache age: %d ms",
460+
isRootCA, System.currentTimeMillis() - cacheTimestamp.get());
461+
// Filter the cached certificates
462+
return Arrays.stream(cached)
463+
.filter(cert -> {
464+
try {
465+
X509Certificate x509Cert = toX509Certificate(cert.certificate());
466+
return isSelfSigned(x509Cert) == isRootCA;
467+
} catch (Exception ex) {
468+
logger.warnf("Error filtering cached certificate %s: %s", cert.alias(), ex.getMessage());
469+
return false;
470+
}
471+
})
472+
.toArray(CertificateRepresentation[]::new);
473+
} else if (cached != null) {
474+
logger.warnf("Cache exists but is expired (age: %d ms). Using expired cache as last resort.",
475+
System.currentTimeMillis() - cacheTimestamp.get());
476+
// Filter the expired cached certificates
477+
return Arrays.stream(cached)
478+
.filter(cert -> {
479+
try {
480+
X509Certificate x509Cert = toX509Certificate(cert.certificate());
481+
return isSelfSigned(x509Cert) == isRootCA;
482+
} catch (Exception ex) {
483+
logger.warnf("Error filtering cached certificate %s: %s", cert.alias(), ex.getMessage());
484+
return false;
485+
}
486+
})
487+
.toArray(CertificateRepresentation[]::new);
488+
} else {
489+
logger.errorf("No cached certificates available for isRootCA=%s, cannot fallback", isRootCA);
490+
throw new RuntimeException("Failed to get certificates and no cache available", e);
491+
}
371492
}
372493
}
373494

0 commit comments

Comments
 (0)