1818
1919import java .util .HashSet ;
2020import java .util .Map ;
21+ import java .util .Objects ;
2122import java .util .Optional ;
2223import java .util .Set ;
2324import java .util .stream .Collectors ;
3637import org .keycloak .models .cache .infinispan .RealmCacheSession ;
3738import org .keycloak .organization .OrganizationProvider ;
3839
40+ import static org .keycloak .models .IdentityProviderStorageProvider .LoginFilter .getLoginPredicate ;
41+
3942public 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 ) {
0 commit comments