Skip to content

Commit 3e59772

Browse files
authored
Add cache for IdentityProviderStorageProvider.getForLogin (#32918)
Closes #32573 Signed-off-by: Stefan Guilhen <[email protected]>
1 parent 5ff9e91 commit 3e59772

File tree

4 files changed

+268
-3
lines changed

4 files changed

+268
-3
lines changed

model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/idp/InfinispanIdentityProviderStorageProvider.java

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.HashSet;
2020
import java.util.Map;
21+
import java.util.Objects;
2122
import java.util.Optional;
2223
import java.util.Set;
2324
import java.util.stream.Collectors;
@@ -36,11 +37,14 @@
3637
import org.keycloak.models.cache.infinispan.RealmCacheSession;
3738
import org.keycloak.organization.OrganizationProvider;
3839

40+
import static org.keycloak.models.IdentityProviderStorageProvider.LoginFilter.getLoginPredicate;
41+
3942
public class InfinispanIdentityProviderStorageProvider implements IdentityProviderStorageProvider {
4043

4144
private static final String IDP_COUNT_KEY_SUFFIX = ".idp.count";
4245
private static final String IDP_ALIAS_KEY_SUFFIX = ".idp.alias";
4346
private static final String IDP_ORG_ID_KEY_SUFFIX = ".idp.orgId";
47+
private static final String IDP_LOGIN_SUFFIX = ".idp.login";
4448

4549
private final KeycloakSession session;
4650
private final IdentityProviderStorageProvider idpDelegate;
@@ -70,9 +74,14 @@ public static String cacheKeyOrgId(RealmModel realm, String orgId) {
7074
return realm.getId() + "." + orgId + IDP_ORG_ID_KEY_SUFFIX;
7175
}
7276

77+
public static String cacheKeyForLogin(RealmModel realm, FetchMode fetchMode) {
78+
return realm.getId() + IDP_LOGIN_SUFFIX + "." + fetchMode;
79+
}
80+
7381
@Override
7482
public IdentityProviderModel create(IdentityProviderModel model) {
7583
registerCountInvalidation();
84+
registerIDPLoginInvalidation(model);
7685
return idpDelegate.create(model);
7786
}
7887

@@ -81,22 +90,25 @@ public void update(IdentityProviderModel model) {
8190
// for cases the alias is being updated, it is needed to lookup the idp by id to obtain the original alias
8291
IdentityProviderModel idpById = getById(model.getInternalId());
8392
registerIDPInvalidation(idpById);
93+
registerIDPLoginInvalidationOnUpdate(idpById, model);
8494
idpDelegate.update(model);
8595
}
8696

8797
@Override
8898
public boolean remove(String alias) {
8999
String cacheKey = cacheKeyIdpAlias(getRealm(), alias);
100+
IdentityProviderModel storedIdp = idpDelegate.getByAlias(alias);
90101
if (isInvalid(cacheKey)) {
91102
//lookup idp by alias in cache to be able to invalidate its internalId
92-
registerIDPInvalidation(idpDelegate.getByAlias(alias));
103+
registerIDPInvalidation(storedIdp);
93104
} else {
94105
CachedIdentityProvider cached = realmCache.getCache().get(cacheKey, CachedIdentityProvider.class);
95106
if (cached != null) {
96107
registerIDPInvalidation(cached.getIdentityProvider());
97108
}
98109
}
99110
registerCountInvalidation();
111+
registerIDPLoginInvalidation(storedIdp);
100112
return idpDelegate.remove(alias);
101113
}
102114

@@ -198,6 +210,50 @@ public Stream<IdentityProviderModel> getByOrganization(String orgId, Integer fir
198210
return identityProviders.stream();
199211
}
200212

213+
@Override
214+
public Stream<IdentityProviderModel> getForLogin(FetchMode mode, String organizationId) {
215+
String cacheKey = cacheKeyForLogin(getRealm(), mode);
216+
217+
if (isInvalid(cacheKey)) {
218+
return idpDelegate.getForLogin(mode, organizationId).map(this::createOrganizationAwareIdentityProviderModel);
219+
}
220+
221+
RealmCacheManager cache = realmCache.getCache();
222+
IdentityProviderListQuery query = cache.get(cacheKey, IdentityProviderListQuery.class);
223+
String searchKey = organizationId != null ? organizationId : "";
224+
Set<String> cached;
225+
226+
if (query == null) {
227+
// not cached yet
228+
Long loaded = cache.getCurrentRevision(cacheKey);
229+
cached = idpDelegate.getForLogin(mode, organizationId).map(IdentityProviderModel::getInternalId).collect(Collectors.toSet());
230+
query = new IdentityProviderListQuery(loaded, cacheKey, getRealm(), searchKey, cached);
231+
cache.addRevisioned(query, startupRevision);
232+
} else {
233+
cached = query.getIDPs(searchKey);
234+
if (cached == null) {
235+
// there is a cache entry, but the current search is not yet cached
236+
cache.invalidateObject(cacheKey);
237+
Long loaded = cache.getCurrentRevision(cacheKey);
238+
cached = idpDelegate.getForLogin(mode, organizationId).map(IdentityProviderModel::getInternalId).collect(Collectors.toSet());
239+
query = new IdentityProviderListQuery(loaded, cacheKey, getRealm(), searchKey, cached, query);
240+
cache.addRevisioned(query, cache.getCurrentCounter());
241+
}
242+
}
243+
244+
Set<IdentityProviderModel> identityProviders = new HashSet<>();
245+
for (String id : cached) {
246+
IdentityProviderModel idp = session.identityProviders().getById(id);
247+
if (idp == null) {
248+
realmCache.registerInvalidation(cacheKey);
249+
return idpDelegate.getForLogin(mode, organizationId).map(this::createOrganizationAwareIdentityProviderModel);
250+
}
251+
identityProviders.add(idp);
252+
}
253+
254+
return identityProviders.stream();
255+
}
256+
201257
@Override
202258
public Stream<String> getByFlow(String flowId, String search, Integer first, Integer max) {
203259
return idpDelegate.getByFlow(flowId, search, first, max);
@@ -323,6 +379,44 @@ private void registerIDPMapperInvalidation(IdentityProviderMapperModel mapper) {
323379
realmCache.registerInvalidation(cacheKeyIdpMapperAliasName(getRealm(), mapper.getIdentityProviderAlias(), mapper.getName()));
324380
}
325381

382+
private void registerIDPLoginInvalidation(IdentityProviderModel idp) {
383+
// only invalidate login caches if the IDP qualifies as a login IDP.
384+
if (getLoginPredicate().test(idp)) {
385+
for (FetchMode mode : FetchMode.values()) {
386+
realmCache.registerInvalidation(cacheKeyForLogin(getRealm(), mode));
387+
}
388+
}
389+
}
390+
391+
/**
392+
* Registers invalidations for the caches that hold the IDPs available for login when an IDP is updated. The caches
393+
* are <strong>NOT</strong> invalidated if:
394+
* <ul>
395+
* <li>IDP is currently NOT a login IDP, and the update hasn't changed that (i.e. it continues to be unavailable for login);</li>
396+
* <li>IDP is currently a login IDP, and the update hasn't changed that. This includes the organization link not being updated as well</li>
397+
* </ul>
398+
* In all other scenarios, the caches must be invalidated.
399+
*
400+
* @param original the identity provider's current model
401+
* @param updated the identity provider's updated model
402+
*/
403+
private void registerIDPLoginInvalidationOnUpdate(IdentityProviderModel original, IdentityProviderModel updated) {
404+
// IDP isn't currently available for login and update preserves that - no need to invalidate.
405+
if (!getLoginPredicate().test(original) && !getLoginPredicate().test(updated)) {
406+
return;
407+
}
408+
// IDP is currently available for login and update preserves that, including organization link - no need to invalidate.
409+
if (getLoginPredicate().test(original) && getLoginPredicate().test(updated)
410+
&& Objects.equals(original.getOrganizationId(), updated.getOrganizationId())) {
411+
return;
412+
}
413+
414+
// all other scenarios should invalidate the login caches.
415+
for (FetchMode mode : FetchMode.values()) {
416+
realmCache.registerInvalidation(cacheKeyForLogin(getRealm(), mode));
417+
}
418+
}
419+
326420
private RealmModel getRealm() {
327421
RealmModel realm = session.getContext().getRealm();
328422
if (realm == null) {

server-spi/src/main/java/org/keycloak/models/IdentityProviderStorageProvider.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ public static Map<String, String> getLoginSearchOptions() {
251251

252252
public static Predicate<IdentityProviderModel> getLoginPredicate() {
253253
return ((Predicate<IdentityProviderModel>) Objects::nonNull)
254+
.and(idp -> idp.getOrganizationId() == null || Boolean.parseBoolean(idp.getConfig().get(OrganizationModel.BROKER_PUBLIC)))
254255
.and(Stream.of(values()).map(LoginFilter::getFilter).reduce(Predicate::and).get());
255256
}
256257
}

services/src/main/java/org/keycloak/organization/forms/login/freemarker/model/OrganizationAwareIdentityProviderBean.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,12 @@ protected List<IdentityProvider> searchForIdentityProviders(String existingIDP)
7272
}
7373
// we don't have a specific organization - fetch public enabled IDPs linked to any org.
7474
return session.identityProviders().getForLogin(ORG_ONLY, null)
75-
.filter(idp -> !Objects.equals(existingIDP, idp.getAlias()))
75+
.filter(idp -> idp.isEnabled() && !Objects.equals(existingIDP, idp.getAlias())) // re-check isEnabled as idp might have been wrapped.
7676
.map(idp -> createIdentityProvider(this.realm, this.baseURI, idp))
7777
.sorted(IDP_COMPARATOR_INSTANCE).toList();
7878
}
7979
return session.identityProviders().getForLogin(ALL, this.organization != null ? this.organization.getId() : null)
80-
.filter(idp -> !Objects.equals(existingIDP, idp.getAlias()))
80+
.filter(idp -> idp.isEnabled() && !Objects.equals(existingIDP, idp.getAlias())) // re-check isEnabled as idp might have been wrapped.
8181
.map(idp -> createIdentityProvider(this.realm, this.baseURI, idp))
8282
.sorted(IDP_COMPARATOR_INSTANCE).toList();
8383
}

0 commit comments

Comments
 (0)