keycloak-aplcache

Secured RemoteCache availability

3/2/2018 6:49:20 AM

Details

diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProvider.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProvider.java
index 5513777..c1f8914 100644
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProvider.java
@@ -18,6 +18,7 @@
 package org.keycloak.connections.infinispan;
 
 import org.infinispan.Cache;
+import org.infinispan.client.hotrod.RemoteCache;
 import org.infinispan.manager.EmbeddedCacheManager;
 
 /**
@@ -26,11 +27,13 @@ import org.infinispan.manager.EmbeddedCacheManager;
 public class DefaultInfinispanConnectionProvider implements InfinispanConnectionProvider {
 
     private final EmbeddedCacheManager cacheManager;
+    private final RemoteCacheProvider remoteCacheProvider;
     private final String siteName;
     private final String nodeName;
 
-    public DefaultInfinispanConnectionProvider(EmbeddedCacheManager cacheManager, String nodeName, String siteName) {
+    public DefaultInfinispanConnectionProvider(EmbeddedCacheManager cacheManager, RemoteCacheProvider remoteCacheProvider, String nodeName, String siteName) {
         this.cacheManager = cacheManager;
+        this.remoteCacheProvider = remoteCacheProvider;
         this.nodeName = nodeName;
         this.siteName = siteName;
     }
@@ -41,6 +44,11 @@ public class DefaultInfinispanConnectionProvider implements InfinispanConnection
     }
 
     @Override
+    public <K, V> RemoteCache<K, V> getRemoteCache(String cacheName) {
+        return remoteCacheProvider.getRemoteCache(cacheName);
+    }
+
+    @Override
     public String getNodeName() {
         return nodeName;
     }
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
index 7493cae..0053791 100755
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
@@ -56,6 +56,8 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
 
     protected EmbeddedCacheManager cacheManager;
 
+    protected RemoteCacheProvider remoteCacheProvider;
+
     protected boolean containerManaged;
 
     private String nodeName;
@@ -66,7 +68,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
     public InfinispanConnectionProvider create(KeycloakSession session) {
         lazyInit();
 
-        return new DefaultInfinispanConnectionProvider(cacheManager, nodeName, siteName);
+        return new DefaultInfinispanConnectionProvider(cacheManager, remoteCacheProvider, nodeName, siteName);
     }
 
     @Override
@@ -74,6 +76,9 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
         if (cacheManager != null && !containerManaged) {
             cacheManager.stop();
         }
+        if (remoteCacheProvider != null) {
+            remoteCacheProvider.stop();
+        }
         cacheManager = null;
     }
 
@@ -104,6 +109,8 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
                     }
 
                     logger.infof("Node name: %s, Site name: %s", nodeName, siteName);
+
+                    remoteCacheProvider = new RemoteCacheProvider(config, cacheManager);
                 }
             }
         }
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
index 00f60a7..cb23b6f 100755
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
@@ -18,6 +18,7 @@
 package org.keycloak.connections.infinispan;
 
 import org.infinispan.Cache;
+import org.infinispan.client.hotrod.RemoteCache;
 import org.keycloak.provider.Provider;
 
 /**
@@ -68,6 +69,12 @@ public interface InfinispanConnectionProvider extends Provider {
     <K, V> Cache<K, V> getCache(String name);
 
     /**
+     * Get remote cache of given name. Could just retrieve the remote cache from the remoteStore configured in given infinispan cache and/or
+     * alternatively return the secured remoteCache (remoteCache corresponding to secured hotrod endpoint)
+     */
+    <K, V> RemoteCache<K, V> getRemoteCache(String name);
+
+    /**
      * @return Address of current node in cluster. In non-cluster environment, it returns some other non-null value (eg. hostname with some random value like "host-123456" )
      */
     String getNodeName();
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/RemoteCacheProvider.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/RemoteCacheProvider.java
new file mode 100644
index 0000000..ea1ba9f
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/RemoteCacheProvider.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.connections.infinispan;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.sasl.RealmCallback;
+
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.client.hotrod.RemoteCacheManager;
+import org.infinispan.client.hotrod.configuration.Configuration;
+import org.infinispan.client.hotrod.configuration.ConfigurationBuilder;
+import org.infinispan.manager.EmbeddedCacheManager;
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.common.util.reflections.Reflections;
+import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
+
+/**
+ * Get either just remoteCache associated with remoteStore associated with infinispan cache of given name. If security is enabled, then
+ * return secured remoteCache based on the template provided by remoteStore configuration but with added "authentication" configuration
+ * of secured hotrod endpoint (RemoteStore doesn't yet allow to configure "security" of hotrod endpoints)
+ *
+ * TODO: Remove this class once we upgrade to infinispan version, which allows to configure security for remoteStore itself
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RemoteCacheProvider {
+
+    protected static final Logger logger = Logger.getLogger(RemoteCacheProvider.class);
+
+    private final Config.Scope config;
+    private final EmbeddedCacheManager cacheManager;
+
+    private final Map<String, RemoteCache> availableCaches = new HashMap<>();
+
+    // Enlist secured managers, which are managed by us and should be shutdown on stop
+    private final List<RemoteCacheManager> managedManagers = new LinkedList<>();
+
+    public RemoteCacheProvider(Config.Scope config, EmbeddedCacheManager cacheManager) {
+        this.config = config;
+        this.cacheManager = cacheManager;
+    }
+
+    public RemoteCache getRemoteCache(String cacheName) {
+        if (availableCaches.get(cacheName) == null) {
+            synchronized (this) {
+                if (availableCaches.get(cacheName) == null) {
+                    RemoteCache remoteCache = loadRemoteCache(cacheName);
+                    availableCaches.put(cacheName, remoteCache);
+                }
+            }
+        }
+
+        return availableCaches.get(cacheName);
+    }
+
+    public void stop() {
+        // TODO:mposolda
+        logger.infof("Shutdown %d registered secured remoteCache managers", managedManagers.size());
+
+        for (RemoteCacheManager mgr : managedManagers) {
+            mgr.stop();
+        }
+    }
+
+
+    protected RemoteCache loadRemoteCache(String cacheName) {
+        RemoteCache remoteCache = InfinispanUtil.getRemoteCache(cacheManager.getCache(cacheName));
+
+        if (config.getBoolean("remoteStoreSecurityEnabled", false)) {
+            // TODO:mposolda
+            logger.info("Remote store security is enabled");
+            RemoteCacheManager securedMgr = createSecuredRemoteCacheManager(config, remoteCache.getRemoteCacheManager());
+            managedManagers.add(securedMgr);
+            return securedMgr.getCache(remoteCache.getName());
+        } else {
+            // TODO:mposolda
+            logger.info("Remote store security is disabled");
+            return remoteCache;
+        }
+    }
+
+
+    protected RemoteCacheManager createSecuredRemoteCacheManager(Config.Scope config, RemoteCacheManager origManager) {
+        String serverName = config.get("remoteStoreSecurityServerName", "keycloak-server");
+        String realm = config.get("remoteStoreSecurityRealm", "ApplicationRealm");
+
+        String securedHotRodEndpoint = config.get("remoteStoreSecurityHotRodEndpoint");
+        String username = config.get("remoteStoreSecurityUsername");
+        String password = config.get("remoteStoreSecurityPassword");
+
+        // TODO:mposolda
+        logger.infof("Server: '%s', Realm: '%s', Username: '%s', Secured HotRod endpoint: '%s'", serverName, realm, username, securedHotRodEndpoint);
+
+        // Create configuration template from the original configuration provided at remoteStore level
+        Configuration origConfig = origManager.getConfiguration();
+
+        ConfigurationBuilder cfgBuilder = new ConfigurationBuilder()
+                .read(origConfig);
+
+        // Workaround as I need a way to override servers and it's not possible to remove existing :/
+        try {
+            Field serversField = cfgBuilder.getClass().getDeclaredField("servers");
+            Reflections.setAccessible(serversField);
+            List origServers = Reflections.getFieldValue(serversField, cfgBuilder, List.class);
+            origServers.clear();
+        } catch (NoSuchFieldException nsfe) {
+            throw new RuntimeException(nsfe);
+        }
+
+        // Create configuration based on the configuration template from remoteStore. Just add security and override secured endpoint
+        Configuration newConfig = cfgBuilder
+                .addServers(securedHotRodEndpoint)
+                .security()
+                  .authentication()
+                    .serverName(serverName) //define server name, should be specified in XML configuration on JDG side
+                    .saslMechanism("DIGEST-MD5") // define SASL mechanism, in this example we use DIGEST with MD5 hash
+                    .callbackHandler(new LoginHandler(username, password.toCharArray(), realm)) // define login handler, implementation defined
+                    .enable()
+                .build();
+
+        return new RemoteCacheManager(newConfig);
+    }
+
+
+    private static class LoginHandler implements CallbackHandler {
+        final private String login;
+        final private char[] password;
+        final private String realm;
+
+        private LoginHandler(String login, char[] password, String realm) {
+            this.login = login;
+            this.password = password;
+            this.realm = realm;
+        }
+
+        @Override
+        public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+            for (Callback callback : callbacks) {
+                if (callback instanceof NameCallback) {
+                    ((NameCallback) callback).setName(login);
+                } else if (callback instanceof PasswordCallback) {
+                    ((PasswordCallback) callback).setPassword(password);
+                } else if (callback instanceof RealmCallback) {
+                    ((RealmCallback) callback).setText(realm);
+                } else {
+                    throw new UnsupportedCallbackException(callback);
+                }
+            }
+        }
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java
index 4ecd445..ba13171 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java
@@ -32,7 +32,6 @@ import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
 import org.keycloak.models.sessions.infinispan.initializer.BaseCacheInitializer;
 import org.keycloak.models.sessions.infinispan.initializer.OfflinePersistentUserSessionLoader;
 import org.keycloak.models.sessions.infinispan.initializer.SessionLoader;
-import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -87,7 +86,7 @@ public class RemoteCacheSessionsLoader implements SessionLoader {
 
     @Override
     public void init(KeycloakSession session) {
-        RemoteCache remoteCache = InfinispanUtil.getRemoteCache(getCache(session));
+        RemoteCache remoteCache = getRemoteCache(session);
 
         RemoteCache<String, String> scriptCache = remoteCache.getRemoteCacheManager().getCache("___script_cache");
 
@@ -100,7 +99,7 @@ public class RemoteCacheSessionsLoader implements SessionLoader {
 
     @Override
     public int getSessionsCount(KeycloakSession session) {
-        RemoteCache remoteCache = InfinispanUtil.getRemoteCache(getCache(session));
+        RemoteCache remoteCache = getRemoteCache(session);
         return remoteCache.size();
     }
 
@@ -109,7 +108,7 @@ public class RemoteCacheSessionsLoader implements SessionLoader {
         Cache cache = getCache(session);
         Cache decoratedCache = cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE, Flag.IGNORE_RETURN_VALUES);
 
-        RemoteCache<?, ?> remoteCache = InfinispanUtil.getRemoteCache(cache);
+        RemoteCache<?, ?> remoteCache = getRemoteCache(session);
 
         log.debugf("Will do bulk load of sessions from remote cache '%s' . First: %d, max: %d", cache.getName(), first, max);
 
@@ -167,4 +166,10 @@ public class RemoteCacheSessionsLoader implements SessionLoader {
     public void afterAllSessionsLoaded(BaseCacheInitializer initializer) {
 
     }
+
+    // Get remoteCache, which may be secured
+    private RemoteCache getRemoteCache(KeycloakSession session) {
+        InfinispanConnectionProvider ispn = session.getProvider(InfinispanConnectionProvider.class);
+        return ispn.getRemoteCache(cacheName);
+    }
 }
diff --git a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json
index 12ffb33..eb58977 100755
--- a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json
+++ b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json
@@ -94,7 +94,13 @@
             "l1Lifespan": "${keycloak.connectionsInfinispan.l1Lifespan:600000}",
             "remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:false}",
             "remoteStoreHost": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}",
-            "remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}"
+            "remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}",
+            "remoteStoreSecurityEnabled": "${keycloak.connectionsInfinispan.remoteStoreSecurityEnabled:false}",
+            "remoteStoreSecurityServerName": "${keycloak.connectionsInfinispan.remoteStoreSecurityServerName:keycloak-server}",
+            "remoteStoreSecurityRealm": "${keycloak.connectionsInfinispan.remoteStoreSecurityRealm:ApplicationRealm}",
+            "remoteStoreSecurityHotRodEndpoint": "${keycloak.connectionsInfinispan.remoteStoreSecurityHotRodEndpoint}",
+            "remoteStoreSecurityUsername": "${keycloak.connectionsInfinispan.remoteStoreSecurityUsername}",
+            "remoteStoreSecurityPassword": "${keycloak.connectionsInfinispan.remoteStoreSecurityPassword}"
         }
     },