Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Bill Burke 2016-11-21 11:33:52 -05:00
commit 798fd84698
183 changed files with 5208 additions and 1649 deletions

View file

@ -9,7 +9,7 @@ For more information about Keycloak visit [Keycloak homepage](http://keycloak.or
Building
--------
Ensure you have JDK 8 (or newer), Maven 3.2.1 (or newer) and Git installed
Ensure you have JDK 8 (or newer), Maven 3.1.1 (or newer) and Git installed
java -version
mvn -version

View file

@ -142,9 +142,13 @@ public class AuthenticatedActionsHandler {
AuthorizationContext authorizationContext = policyEnforcer.enforce(facade);
RefreshableKeycloakSecurityContext session = (RefreshableKeycloakSecurityContext) facade.getSecurityContext();
session.setAuthorizationContext(authorizationContext);
if (session != null) {
session.setAuthorizationContext(authorizationContext);
return authorizationContext.isGranted();
return authorizationContext.isGranted();
}
return true;
} catch (Exception e) {
throw new RuntimeException("Failed to enforce policy decisions.", e);
}

View file

@ -19,6 +19,7 @@ package org.keycloak.adapters.authorization;
import org.jboss.logging.Logger;
import org.keycloak.AuthorizationContext;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.OIDCHttpFacade;
import org.keycloak.adapters.spi.HttpFacade.Request;
import org.keycloak.adapters.spi.HttpFacade.Response;
@ -66,40 +67,51 @@ public abstract class AbstractPolicyEnforcer {
return createEmptyAuthorizationContext(true);
}
AccessToken accessToken = httpFacade.getSecurityContext().getToken();
Request request = httpFacade.getRequest();
Response response = httpFacade.getResponse();
String pathInfo = URI.create(request.getURI()).getPath().substring(1);
String path = pathInfo.substring(pathInfo.indexOf('/'), pathInfo.length());
PathConfig pathConfig = this.pathMatcher.matches(path, this.paths);
KeycloakSecurityContext securityContext = httpFacade.getSecurityContext();
LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
if (securityContext != null) {
AccessToken accessToken = securityContext.getToken();
if (pathConfig == null) {
if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
return createAuthorizationContext(accessToken);
if (accessToken != null) {
Request request = httpFacade.getRequest();
Response response = httpFacade.getResponse();
String pathInfo = URI.create(request.getURI()).getPath().substring(1);
String path = pathInfo.substring(pathInfo.indexOf('/'), pathInfo.length());
PathConfig pathConfig = this.pathMatcher.matches(path, this.paths);
LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
if (pathConfig == null) {
if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
return createAuthorizationContext(accessToken);
}
LOGGER.debugf("Could not find a configuration for path [%s]", path);
response.sendError(403, "Could not find a configuration for path [" + path + "].");
return createEmptyAuthorizationContext(false);
}
if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
return createEmptyAuthorizationContext(true);
}
PathConfig actualPathConfig = resolvePathConfig(pathConfig, request);
Set<String> requiredScopes = getRequiredScopes(actualPathConfig, request);
if (isAuthorized(actualPathConfig, requiredScopes, accessToken, httpFacade)) {
try {
return createAuthorizationContext(accessToken);
} catch (Exception e) {
throw new RuntimeException("Error processing path [" + actualPathConfig.getPath() + "].", e);
}
}
if (!challenge(actualPathConfig, requiredScopes, httpFacade)) {
LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
response.sendError(403, "Authorization failed.");
}
}
LOGGER.debugf("Could not find a configuration for path [%s]", path);
response.sendError(403, "Could not find a configuration for path [" + path + "].");
return createEmptyAuthorizationContext(false);
}
PathConfig actualPathConfig = resolvePathConfig(pathConfig, request);
Set<String> requiredScopes = getRequiredScopes(actualPathConfig, request);
if (isAuthorized(actualPathConfig, requiredScopes, accessToken, httpFacade)) {
try {
return createAuthorizationContext(accessToken);
} catch (Exception e) {
throw new RuntimeException("Error processing path [" + actualPathConfig.getPath() + "].", e);
}
}
if (!challenge(actualPathConfig, requiredScopes, httpFacade)) {
LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
response.sendError(403, "Authorization failed.");
}
return createEmptyAuthorizationContext(false);
@ -125,14 +137,17 @@ public abstract class AbstractPolicyEnforcer {
}
List<Permission> permissions = authorization.getPermissions();
boolean hasPermission = false;
for (Permission permission : permissions) {
if (permission.getResourceSetId() != null) {
if (isResourcePermission(actualPathConfig, permission)) {
hasPermission = true;
if (actualPathConfig.isInstance() && !matchResourcePermission(actualPathConfig, permission)) {
continue;
}
if (hasResourceScopePermission(requiredScopes, permission, actualPathConfig)) {
LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, permissions);
if (request.getMethod().equalsIgnoreCase("DELETE") && actualPathConfig.isInstance()) {
@ -143,11 +158,16 @@ public abstract class AbstractPolicyEnforcer {
}
} else {
if (hasResourceScopePermission(requiredScopes, permission, actualPathConfig)) {
hasPermission = true;
return true;
}
}
}
if (!hasPermission && EnforcementMode.PERMISSIVE.equals(actualPathConfig.getEnforcementMode())) {
return true;
}
LOGGER.debugf("Authorization FAILED for path [%s]. No enough permissions [%s].", actualPathConfig, permissions);
return false;
@ -218,6 +238,7 @@ public abstract class AbstractPolicyEnforcer {
config.setScopes(originalConfig.getScopes());
config.setMethods(originalConfig.getMethods());
config.setParentConfig(originalConfig);
config.setEnforcementMode(originalConfig.getEnforcementMode());
this.paths.add(config);

View file

@ -105,7 +105,16 @@ public class PolicyEnforcer {
}
private List<PathConfig> configurePaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
if (enforcerConfig.getPaths().isEmpty()) {
boolean loadPathsFromServer = true;
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
if (!PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
loadPathsFromServer = false;
break;
}
}
if (loadPathsFromServer) {
LOGGER.info("No path provided in configuration.");
return configureAllPathsForResourceServer(protectedResource);
} else {

View file

@ -105,7 +105,7 @@ public class SamlPrincipal implements Serializable, Principal {
* @return
*/
public List<String> getFriendlyAttributes(String friendlyName) {
List<String> list = friendlyAttributes.get(name);
List<String> list = friendlyAttributes.get(friendlyName);
if (list != null) {
return Collections.unmodifiableList(list);
} else {

View file

@ -18,6 +18,8 @@
<description>KeyCloak AuthZ: Client API</description>
<properties>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
<keycloak.osgi.export>
org.keycloak.authorization.client.*
</keycloak.osgi.export>
@ -63,6 +65,14 @@
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
<!-- Adding OSGI metadata to the JAR without changing the packaging type. -->
<plugin>
<artifactId>maven-jar-plugin</artifactId>
@ -98,4 +108,4 @@
</plugins>
</build>
</project>
</project>

View file

@ -48,7 +48,7 @@ public class HttpMethod<R> {
private HttpMethodResponse<R> response;
public HttpMethod(Configuration configuration, RequestBuilder builder) {
this(configuration, builder, new HashMap<>(), new HashMap<>());
this(configuration, builder, new HashMap<String, String>(), new HashMap<String, String>());
}
public HttpMethod(Configuration configuration, RequestBuilder builder, HashMap<String, String> params, HashMap<String, String> headers) {
@ -155,4 +155,4 @@ public class HttpMethod<R> {
}
};
}
}
}

View file

@ -41,7 +41,7 @@ public class HttpMethodResponse<R> {
});
}
public HttpMethodResponse<R> json(Class<R> responseType) {
public HttpMethodResponse<R> json(final Class<R> responseType) {
return new HttpMethodResponse<R>(this.method) {
@Override
public R execute() {

View file

@ -45,7 +45,7 @@ public class RolePolicyProviderFactory implements PolicyProviderFactory {
@Override
public String getName() {
return "Role-Based";
return "Role";
}
@Override

View file

@ -43,7 +43,7 @@ public class UserPolicyProviderFactory implements PolicyProviderFactory {
@Override
public String getName() {
return "User-Based";
return "User";
}
@Override

View file

@ -30,7 +30,7 @@ public class DroolsPolicyProviderFactory implements PolicyProviderFactory {
@Override
public String getName() {
return "Drools";
return "Rule";
}
@Override

View file

@ -18,9 +18,11 @@
package org.keycloak;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessToken.Authorization;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.representations.idm.authorization.Permission;
import java.util.Collections;
import java.util.List;
/**
@ -44,7 +46,17 @@ public class AuthorizationContext {
}
public boolean hasPermission(String resourceName, String scopeName) {
for (Permission permission : authzToken.getAuthorization().getPermissions()) {
if (this.authzToken == null) {
return false;
}
Authorization authorization = this.authzToken.getAuthorization();
if (authorization == null) {
return false;
}
for (Permission permission : authorization.getPermissions()) {
for (PathConfig pathHolder : this.paths) {
if (pathHolder.getName().equals(resourceName)) {
if (pathHolder.getId().equals(permission.getResourceSetId())) {
@ -60,7 +72,17 @@ public class AuthorizationContext {
}
public boolean hasResourcePermission(String resourceName) {
for (Permission permission : authzToken.getAuthorization().getPermissions()) {
if (this.authzToken == null) {
return false;
}
Authorization authorization = this.authzToken.getAuthorization();
if (authorization == null) {
return false;
}
for (Permission permission : authorization.getPermissions()) {
for (PathConfig pathHolder : this.paths) {
if (pathHolder.getName().equals(resourceName)) {
if (pathHolder.getId().equals(permission.getResourceSetId())) {
@ -74,7 +96,17 @@ public class AuthorizationContext {
}
public boolean hasScopePermission(String scopeName) {
for (Permission permission : authzToken.getAuthorization().getPermissions()) {
if (this.authzToken == null) {
return false;
}
Authorization authorization = this.authzToken.getAuthorization();
if (authorization == null) {
return false;
}
for (Permission permission : authorization.getPermissions()) {
if (permission.getScopes().contains(scopeName)) {
return true;
}
@ -84,7 +116,17 @@ public class AuthorizationContext {
}
public List<Permission> getPermissions() {
return this.authzToken.getAuthorization().getPermissions();
if (this.authzToken == null) {
return Collections.emptyList();
}
Authorization authorization = this.authzToken.getAuthorization();
if (authorization == null) {
return Collections.emptyList();
}
return Collections.unmodifiableList(authorization.getPermissions());
}
public boolean isGranted() {

View file

@ -122,6 +122,9 @@ public class PolicyEnforcerConfig {
private List<String> scopes = Collections.emptyList();
private String id;
@JsonProperty("enforcement-mode")
private EnforcementMode enforcementMode = EnforcementMode.ENFORCING;
@JsonIgnore
private PathConfig parentConfig;
@ -173,6 +176,14 @@ public class PolicyEnforcerConfig {
return id;
}
public EnforcementMode getEnforcementMode() {
return enforcementMode;
}
public void setEnforcementMode(EnforcementMode enforcementMode) {
this.enforcementMode = enforcementMode;
}
@Override
public String toString() {
return "PathConfig{" +
@ -181,6 +192,7 @@ public class PolicyEnforcerConfig {
", path='" + path + '\'' +
", scopes=" + scopes +
", id='" + id + '\'' +
", enforcerMode='" + enforcementMode + '\'' +
'}';
}

View file

@ -255,6 +255,10 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.sonatype.sisu.inject</groupId>
<artifactId>guice-servlet</artifactId>
</dependency>
<dependency>
<groupId>org.sonatype.plexus</groupId>
<artifactId>plexus-cipher</artifactId>

View file

@ -1,3 +1,4 @@
<!--
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
@ -39,6 +40,9 @@
<include>org/keycloak/keycloak-as7-subsystem/**</include>
<include>org/keycloak/keycloak-adapter-subsystem/**</include>
<include>org/keycloak/keycloak-servlet-oauth-client/**</include>
<!-- Authorization -->
<include>org/keycloak/keycloak-authz-client/**</include>
</includes>
<excludes>
<exclude>**/*.war</exclude>

View file

@ -91,6 +91,10 @@
<maven-resource group="org.keycloak" artifact="keycloak-servlet-oauth-client"/>
</module-def>
<!-- Authorization -->
<module-def name="org.keycloak.keycloak-authz-client">
<maven-resource group="org.keycloak" artifact="keycloak-authz-client"/>
</module-def>
</target>
<target name="clean-target">

View file

@ -102,6 +102,11 @@
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-json-provider</artifactId>
</dependency>
<!-- Authorization -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-client</artifactId>
</dependency>
</dependencies>
<build>

View file

@ -34,6 +34,7 @@
<module name="org.keycloak.keycloak-common"/>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-adapter-spi"/>
<module name="org.keycloak.keycloak-authz-client"/>
</dependencies>
</module>

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ JBoss, Home of Professional Open Source.
~ Copyright 2016 Red Hat, Inc., and individual 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.
-->
<module xmlns="urn:jboss:module:1.1" name="org.keycloak.keycloak-authz-client">
<resources>
<!-- Insert resources here -->
</resources>
<dependencies>
<module name="org.bouncycastle" />
<module name="javax.api"/>
<module name="javax.activation.api"/>
<module name="sun.jdk" optional="true" />
<module name="javax.ws.rs.api"/>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-common"/>
<module name="org.apache.httpcomponents"/>
<module name="com.fasterxml.jackson.core.jackson-core"/>
<module name="com.fasterxml.jackson.core.jackson-annotations"/>
<module name="com.fasterxml.jackson.core.jackson-databind"/>
<module name="com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider"/>
</dependencies>
</module>

View file

@ -39,6 +39,9 @@
<include>org/keycloak/keycloak-as7-subsystem/**</include>
<include>org/keycloak/keycloak-adapter-subsystem/**</include>
<include>org/keycloak/keycloak-servlet-oauth-client/**</include>
<!-- Authorization -->
<include>org/keycloak/keycloak-authz-client/**</include>
</includes>
<excludes>
<exclude>**/*.war</exclude>

View file

@ -22,6 +22,7 @@
<resources>
<artifact name="${org.eclipse.sisu:org.eclipse.sisu.inject}"/>
<artifact name="${org.eclipse.sisu:org.eclipse.sisu.plexus}"/>
<artifact name="${org.sonatype.sisu.inject:guice-servlet}"/>
</resources>
<dependencies>
<module name="javax.api"/>

View file

@ -30,6 +30,9 @@
<module name="org.keycloak.keycloak-server-spi"/>
<module name="org.keycloak.keycloak-server-spi-private"/>
<module name="org.infinispan"/>
<module name="org.infinispan.commons"/>
<module name="org.infinispan.cachestore.remote"/>
<module name="org.infinispan.client.hotrod"/>
<module name="org.jboss.logging"/>
<module name="javax.api"/>
</dependencies>

View file

@ -30,7 +30,7 @@
<module name="org.keycloak.keycloak-js-adapter" services="import"/>
<module name="org.keycloak.keycloak-kerberos-federation" services="import"/>
<module name="org.keycloak.keycloak-ldap-federation" services="import"/>
<module name="org.keycloak.keycloak-sssd-federation" services="import"/>
<module name="org.keycloak.keycloak-sssd-federation" optional="true" services="import"/>
<module name="org.keycloak.keycloak-server-spi" services="import"/>
<module name="org.keycloak.keycloak-server-spi-private" services="import"/>
<module name="org.keycloak.keycloak-model-jpa" services="import"/>

View file

@ -22,6 +22,7 @@
<resources>
<artifact name="${org.keycloak:keycloak-sssd-federation}"/>
<resource-root path="/usr/share/java/jna.jar"/>
</resources>
<dependencies>

View file

@ -2,9 +2,9 @@ embed-server --server-config=standalone-ha.xml
/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",jta=false,driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true)
/subsystem=infinispan/cache-container=keycloak:add(jndi-name="infinispan/Keycloak")
/subsystem=infinispan/cache-container=keycloak/transport=TRANSPORT:add(lock-timeout=60000)
/subsystem=infinispan/cache-container=keycloak/invalidation-cache=realms:add(mode="SYNC")
/subsystem=infinispan/cache-container=keycloak/invalidation-cache=users:add(mode="SYNC")
/subsystem=infinispan/cache-container=keycloak/invalidation-cache=users/eviction=EVICTION:add(max-entries=10000,strategy=LRU)
/subsystem=infinispan/cache-container=keycloak/local-cache=realms:add()
/subsystem=infinispan/cache-container=keycloak/local-cache=users:add()
/subsystem=infinispan/cache-container=keycloak/local-cache=users/eviction=EVICTION:add(max-entries=10000,strategy=LRU)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:add(mode="SYNC",owners="1")
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:add(mode="SYNC",owners="1")
/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:add(mode="SYNC",owners="1")

View file

@ -12,5 +12,5 @@ Then registering the provider by editing `standalone/configuration/standalone.xm
<provider>module:org.keycloak.examples.hello-rest-example</provider>
</providers>
Then start (or restart) the server. Once started open http://localhost:8080/realms/master/hello and you should see the message _Hello master_.
You can also invoke the endpoint for other realms by replacing `master` with the realm name in the above url.
Then start (or restart) the server. Once started open http://localhost:8080/auth/realms/master/hello and you should see the message _Hello master_.
You can also invoke the endpoint for other realms by replacing `master` with the realm name in the above url.

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<jboss-deployment-structure>
<deployment>
<dependencies>
<module name="org.apache.httpcomponents" />
</dependencies>
</deployment>
</jboss-deployment-structure>

View file

@ -49,6 +49,7 @@
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
@ -70,10 +71,6 @@
<artifactId>jboss-logging</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.github.jnr</groupId>
<artifactId>jnr-unixsocket</artifactId>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,46 @@
/*
* Copyright 2016 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 cx.ath.matthew;
/**
* @author <a href="mailto:bruno@abstractj.org">Bruno Oliveira</a>.
*/
public class LibraryLoader {
private static final String[] PATHS = {"/usr/lib/", "/usr/lib64/", "/usr/local/lib/", "/opt/local/lib/"};
private static final String LIBRARY_NAME = "libunix_dbus_java";
private static final String VERSION = "0.0.8";
private static boolean loadSucceeded;
public static LibraryLoader load() {
for (String path : PATHS) {
try {
System.load(String.format("%s/%s.so.%s", path, LIBRARY_NAME, VERSION));
loadSucceeded = true;
break;
} catch (UnsatisfiedLinkError e) {
loadSucceeded = false;
}
}
return new LibraryLoader();
}
public boolean succeed() {
return loadSucceeded;
}
}

View file

@ -26,25 +26,25 @@
*/
package cx.ath.matthew.unix;
import jnr.unixsocket.UnixSocketChannel;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.Channels;
public class USInputStream extends InputStream {
public static final int MSG_DONTWAIT = 0x40;
private UnixSocketChannel channel;
private native int native_recv(int sock, byte[] b, int off, int len, int flags, int timeout) throws IOException;
private int sock;
boolean closed = false;
private byte[] onebuf = new byte[1];
private UnixSocket us;
private boolean blocking = true;
private int flags = 0;
private int timeout = 0;
public USInputStream(UnixSocketChannel channel, UnixSocket us) {
public USInputStream(int sock, UnixSocket us) {
this.sock = sock;
this.us = us;
this.channel = channel;
}
public void close() throws IOException {
@ -65,8 +65,7 @@ public class USInputStream extends InputStream {
public int read(byte[] b, int off, int len) throws IOException {
if (closed) throw new NotConnectedException();
int count = receive(b, off, len);
int count = native_recv(sock, b, off, len, flags, timeout);
/* Yes, I really want to do this. Recv returns 0 for 'connection shut down'.
* read() returns -1 for 'end of stream.
* Recv returns -1 for 'EAGAIN' (all other errors cause an exception to be raised)
@ -92,21 +91,4 @@ public class USInputStream extends InputStream {
public void setSoTimeout(int timeout) {
this.timeout = timeout;
}
/*
* Taken from JRuby with small modifications
* @see <a href="https://github.com/jruby/jruby/blob/master/core/src/main/java/org/jruby/ext/socket/RubyUNIXSocket.java">RubyUNIXSocket.java</a>
*/
private int receive(byte[] dataBytes, int off, int len) {
int recvStatus = -1;
try {
InputStream inputStream = Channels.newInputStream(channel);
recvStatus = inputStream.read(dataBytes, off, len);
} catch (IOException e) {
e.printStackTrace();
}
return recvStatus;
}
}

View file

@ -26,31 +26,22 @@
*/
package cx.ath.matthew.unix;
import jnr.constants.platform.linux.SocketLevel;
import jnr.posix.CmsgHdr;
import jnr.posix.MsgHdr;
import jnr.posix.POSIX;
import jnr.posix.POSIXFactory;
import jnr.unixsocket.UnixSocketChannel;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class USOutputStream extends OutputStream {
private native int native_send(int sock, byte[] b, int off, int len) throws IOException;
private UnixSocketChannel channel;
private native int native_send(int sock, byte[][] b) throws IOException;
private int sock;
boolean closed = false;
private byte[] onebuf = new byte[1];
private UnixSocket us;
public USOutputStream(UnixSocketChannel channel, int sock, UnixSocket us) {
public USOutputStream(int sock, UnixSocket us) {
this.sock = sock;
this.us = us;
this.channel = channel;
}
public void close() throws IOException {
@ -61,9 +52,14 @@ public class USOutputStream extends OutputStream {
public void flush() {
} // no-op, we do not buffer
public void write(byte[][] b) throws IOException {
if (closed) throw new NotConnectedException();
native_send(sock, b);
}
public void write(byte[] b, int off, int len) throws IOException {
if (closed) throw new NotConnectedException();
send(sock, b, off, len);
native_send(sock, b, off, len);
}
public void write(int b) throws IOException {
@ -79,46 +75,4 @@ public class USOutputStream extends OutputStream {
public UnixSocket getSocket() {
return us;
}
/*
* Taken from JRuby with small modifications
* @see <a href="https://github.com/jruby/jruby/blob/master/core/src/main/java/org/jruby/ext/socket/RubyUNIXSocket.java">RubyUNIXSocket.java</a>
*/
private void send(int sock, ByteBuffer[] outIov) {
final POSIX posix = POSIXFactory.getNativePOSIX();
MsgHdr outMessage = posix.allocateMsgHdr();
outMessage.setIov(outIov);
CmsgHdr outControl = outMessage.allocateControl(4);
outControl.setLevel(SocketLevel.SOL_SOCKET.intValue());
outControl.setType(0x01);
ByteBuffer fdBuf = ByteBuffer.allocateDirect(4);
fdBuf.order(ByteOrder.nativeOrder());
fdBuf.putInt(0, channel.getFD());
outControl.setData(fdBuf);
posix.sendmsg(sock, outMessage, 0);
}
private void send(int sock, byte[] dataBytes, int off, int len) {
ByteBuffer[] outIov = new ByteBuffer[1];
outIov[0] = ByteBuffer.allocateDirect(dataBytes.length);
outIov[0].put(dataBytes, off, len);
outIov[0].flip();
send(sock, outIov);
}
protected void send(int sock, byte[] dataBytes) {
ByteBuffer[] outIov = new ByteBuffer[1];
outIov[0] = ByteBuffer.allocateDirect(dataBytes.length);
outIov[0].put(dataBytes);
outIov[0].flip();
send(sock, outIov);
}
}

View file

@ -0,0 +1,43 @@
/*
* Java Unix Sockets Library
*
* Copyright (c) Matthew Johnson 2004
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* To Contact the author, please email src@matthew.ath.cx
*
*/
package cx.ath.matthew.unix;
import java.io.IOException;
/**
* An IO Exception which occurred during UNIX Socket IO
*/
public class UnixIOException extends IOException {
private int no;
private String message;
public UnixIOException(int no, String message) {
super(message);
this.message = message;
this.no = no;
}
}

View file

@ -26,11 +26,9 @@
*/
package cx.ath.matthew.unix;
import cx.ath.matthew.LibraryLoader;
import cx.ath.matthew.debug.Debug;
import jnr.unixsocket.UnixSocketAddress;
import jnr.unixsocket.UnixSocketChannel;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@ -39,8 +37,25 @@ import java.io.OutputStream;
* Represents a UnixSocket.
*/
public class UnixSocket {
static {
LibraryLoader.load();
}
private UnixSocketChannel channel;
private native void native_set_pass_cred(int sock, boolean passcred) throws IOException;
private native int native_connect(String address, boolean abs) throws IOException;
private native void native_close(int sock) throws IOException;
private native int native_getPID(int sock);
private native int native_getUID(int sock);
private native int native_getGID(int sock);
private native void native_send_creds(int sock, byte data) throws IOException;
private native byte native_recv_creds(int sock, int[] creds) throws IOException;
private UnixSocketAddress address = null;
private USOutputStream os = null;
@ -58,8 +73,8 @@ public class UnixSocket {
this.sock = sock;
this.address = address;
this.connected = true;
this.os = new USOutputStream(channel, sock, this);
this.is = new USInputStream(channel, this);
this.os = new USOutputStream(sock, this);
this.is = new USInputStream(sock, this);
}
/**
@ -83,7 +98,7 @@ public class UnixSocket {
* @param address The Unix Socket address to connect to
*/
public UnixSocket(String address) throws IOException {
this(new UnixSocketAddress(new File(address)));
this(new UnixSocketAddress(address));
}
/**
@ -93,11 +108,9 @@ public class UnixSocket {
*/
public void connect(UnixSocketAddress address) throws IOException {
if (connected) close();
this.channel = UnixSocketChannel.open(address);
this.channel = UnixSocketChannel.open(address);
this.sock = channel.getFD();
this.os = new USOutputStream(channel, sock, this);
this.is = new USInputStream(channel, this);
this.sock = native_connect(address.path, address.abs);
this.os = new USOutputStream(this.sock, this);
this.is = new USInputStream(this.sock, this);
this.address = address;
this.connected = true;
this.closed = false;
@ -110,7 +123,7 @@ public class UnixSocket {
* @param address The Unix Socket address to connect to
*/
public void connect(String address) throws IOException {
connect(new UnixSocketAddress(new File(address)));
connect(new UnixSocketAddress(address));
}
public void finalize() {
@ -125,7 +138,7 @@ public class UnixSocket {
*/
public synchronized void close() throws IOException {
if (Debug.debug) Debug.print(Debug.INFO, "Closing socket");
channel.close();
native_close(sock);
sock = 0;
this.closed = true;
this.connected = false;
@ -169,7 +182,91 @@ public class UnixSocket {
*/
public void sendCredentialByte(byte data) throws IOException {
if (!connected) throw new NotConnectedException();
os.send(channel.getFD(), new byte[]{ data });
native_send_creds(sock, data);
}
/**
* Receive a single byte of data, with credentials.
* (Works on BSDs)
*
* @param data The byte of data to send.
* @see getPeerUID
* @see getPeerPID
* @see getPeerGID
*/
public byte recvCredentialByte() throws IOException {
if (!connected) throw new NotConnectedException();
int[] creds = new int[]{-1, -1, -1};
byte data = native_recv_creds(sock, creds);
pid = creds[0];
uid = creds[1];
gid = creds[2];
return data;
}
/**
* Get the credential passing status.
* (only effective on linux)
*
* @return The current status of credential passing.
* @see setPassCred
*/
public boolean getPassCred() {
return passcred;
}
/**
* Return the uid of the remote process.
* Some data must have been received on the socket to do this.
* Either setPassCred must be called on Linux first, or recvCredentialByte
* on BSD.
*
* @return the UID or -1 if it is not available
*/
public int getPeerUID() {
if (-1 == uid)
uid = native_getUID(sock);
return uid;
}
/**
* Return the gid of the remote process.
* Some data must have been received on the socket to do this.
* Either setPassCred must be called on Linux first, or recvCredentialByte
* on BSD.
*
* @return the GID or -1 if it is not available
*/
public int getPeerGID() {
if (-1 == gid)
gid = native_getGID(sock);
return gid;
}
/**
* Return the pid of the remote process.
* Some data must have been received on the socket to do this.
* Either setPassCred must be called on Linux first, or recvCredentialByte
* on BSD.
*
* @return the PID or -1 if it is not available
*/
public int getPeerPID() {
if (-1 == pid)
pid = native_getPID(sock);
return pid;
}
/**
* Set the credential passing status.
* (Only does anything on linux, for other OS, you need
* to use send/recv credentials)
*
* @param enable Set to true for credentials to be passed.
*/
public void setPassCred(boolean enable) throws IOException {
native_set_pass_cred(sock, enable);
passcred = enable;
}
/**

View file

@ -0,0 +1,86 @@
/*
* Java Unix Sockets Library
*
* Copyright (c) Matthew Johnson 2004
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* To Contact the author, please email src@matthew.ath.cx
*
*/
package cx.ath.matthew.unix;
/**
* Represents an address for a Unix Socket
*/
public class UnixSocketAddress {
String path;
boolean abs;
/**
* Create the address.
*
* @param path The path to the Unix Socket.
* @param abs True if this should be an abstract socket.
*/
public UnixSocketAddress(String path, boolean abs) {
this.path = path;
this.abs = abs;
}
/**
* Create the address.
*
* @param path The path to the Unix Socket.
*/
public UnixSocketAddress(String path) {
this.path = path;
this.abs = false;
}
/**
* Return the path.
*/
public String getPath() {
return path;
}
/**
* Returns true if this an address for an abstract socket.
*/
public boolean isAbstract() {
return abs;
}
/**
* Return the Address as a String.
*/
public String toString() {
return "unix" + (abs ? ":abstract" : "") + ":path=" + path;
}
public boolean equals(Object o) {
if (!(o instanceof UnixSocketAddress)) return false;
return ((UnixSocketAddress) o).path.equals(this.path);
}
public int hashCode() {
return path.hashCode();
}
}

View file

@ -43,12 +43,20 @@ public class MessageWriter {
if (Debug.debug) Debug.print(Debug.WARN, "Message " + m + " wire-data was null!");
return;
}
for (byte[] buf : m.getWireData()) {
if (Debug.debug)
Debug.print(Debug.VERBOSE, "(" + buf + "):" + (null == buf ? "" : Hexdump.format(buf)));
if (null == buf) break;
out.write(buf);
}
if (isunix) {
if (Debug.debug) {
Debug.print(Debug.DEBUG, "Writing all " + m.getWireData().length + " buffers simultaneously to Unix Socket");
for (byte[] buf : m.getWireData())
Debug.print(Debug.VERBOSE, "(" + buf + "):" + (null == buf ? "" : Hexdump.format(buf)));
}
((USOutputStream) out).write(m.getWireData());
} else
for (byte[] buf : m.getWireData()) {
if (Debug.debug)
Debug.print(Debug.VERBOSE, "(" + buf + "):" + (null == buf ? "" : Hexdump.format(buf)));
if (null == buf) break;
out.write(buf);
}
out.flush();
}

View file

@ -12,6 +12,7 @@ package org.freedesktop.dbus;
import cx.ath.matthew.debug.Debug;
import cx.ath.matthew.unix.UnixSocket;
import cx.ath.matthew.unix.UnixSocketAddress;
import cx.ath.matthew.utils.Hexdump;
import java.io.BufferedReader;
@ -25,6 +26,7 @@ import java.io.OutputStream;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@ -255,8 +257,10 @@ public class Transport {
return new String(res);
}
public static final int MODE_SERVER = 1;
public static final int MODE_CLIENT = 2;
public static final int AUTH_NONE = 0;
public static final int AUTH_EXTERNAL = 1;
public static final int AUTH_SHA = 2;
public static final int AUTH_ANON = 4;
@ -273,12 +277,15 @@ public class Transport {
public static final int WAIT_DATA = 1;
public static final int WAIT_OK = 2;
public static final int WAIT_REJECT = 3;
public static final int WAIT_AUTH = 4;
public static final int WAIT_BEGIN = 5;
public static final int AUTHENTICATED = 6;
public static final int FAILED = 7;
public static final int OK = 1;
public static final int CONTINUE = 2;
public static final int ERROR = 3;
public static final int REJECT = 4;
public Command receive(InputStream s) throws IOException {
StringBuffer sb = new StringBuffer();
@ -388,8 +395,89 @@ public class Transport {
}
}
public String challenge = "";
public String cookie = "";
public int do_response(int auth, String Uid, String kernelUid, Command c) {
MessageDigest md = null;
try {
md = MessageDigest.getInstance("SHA");
} catch (NoSuchAlgorithmException NSAe) {
if (Debug.debug && AbstractConnection.EXCEPTION_DEBUG) Debug.print(Debug.ERR, NSAe);
return ERROR;
}
switch (auth) {
case AUTH_NONE:
switch (c.getMechs()) {
case AUTH_ANON:
return OK;
case AUTH_EXTERNAL:
if (0 == col.compare(Uid, c.getData()) &&
(null == kernelUid || 0 == col.compare(Uid, kernelUid)))
return OK;
else
return ERROR;
case AUTH_SHA:
String context = COOKIE_CONTEXT;
long id = System.currentTimeMillis();
byte[] buf = new byte[8];
Message.marshallintBig(id, buf, 0, 8);
challenge = stupidlyEncode(md.digest(buf));
Random r = new Random();
r.nextBytes(buf);
cookie = stupidlyEncode(md.digest(buf));
try {
addCookie(context, "" + id, id / 1000, cookie);
} catch (IOException IOe) {
if (Debug.debug && AbstractConnection.EXCEPTION_DEBUG) Debug.print(Debug.ERR, IOe);
}
if (Debug.debug)
Debug.print(Debug.DEBUG, "Sending challenge: " + context + ' ' + id + ' ' + challenge);
c.setResponse(stupidlyEncode(context + ' ' + id + ' ' + challenge));
return CONTINUE;
default:
return ERROR;
}
case AUTH_SHA:
String[] response = stupidlyDecode(c.getData()).split(" ");
if (response.length < 2) return ERROR;
String cchal = response[0];
String hash = response[1];
String prehash = challenge + ":" + cchal + ":" + cookie;
byte[] buf = md.digest(prehash.getBytes());
String posthash = stupidlyEncode(buf);
if (Debug.debug)
Debug.print(Debug.DEBUG, "Authenticating Hash; data=" + prehash + " remote hash=" + hash + " local hash=" + posthash);
if (0 == col.compare(posthash, hash))
return OK;
else
return ERROR;
default:
return ERROR;
}
}
public String[] getTypes(int types) {
switch (types) {
case AUTH_EXTERNAL:
return new String[]{"EXTERNAL"};
case AUTH_SHA:
return new String[]{"DBUS_COOKIE_SHA1"};
case AUTH_ANON:
return new String[]{"ANONYMOUS"};
case AUTH_SHA + AUTH_EXTERNAL:
return new String[]{"EXTERNAL", "DBUS_COOKIE_SHA1"};
case AUTH_SHA + AUTH_ANON:
return new String[]{"ANONYMOUS", "DBUS_COOKIE_SHA1"};
case AUTH_EXTERNAL + AUTH_ANON:
return new String[]{"ANONYMOUS", "EXTERNAL"};
case AUTH_EXTERNAL + AUTH_ANON + AUTH_SHA:
return new String[]{"ANONYMOUS", "EXTERNAL", "DBUS_COOKIE_SHA1"};
default:
return new String[]{};
}
}
/**
* performs SASL auth on the given streams.
* Mode selects whether to run as a SASL server or client.
@ -400,6 +488,7 @@ public class Transport {
public boolean auth(int mode, int types, String guid, OutputStream out, InputStream in, UnixSocket us) throws IOException {
String username = System.getProperty("user.name");
String Uid = null;
String kernelUid = null;
try {
Class c = Class.forName("com.sun.security.auth.module.UnixSystem");
Method m = c.getMethod("getUid");
@ -529,6 +618,110 @@ public class Transport {
state = FAILED;
}
break;
case MODE_SERVER:
switch (state) {
case INITIAL_STATE:
byte[] buf = new byte[1];
if (null == us) {
in.read(buf);
} else {
buf[0] = us.recvCredentialByte();
int kuid = us.getPeerUID();
if (kuid >= 0)
kernelUid = stupidlyEncode("" + kuid);
}
if (0 != buf[0]) state = FAILED;
else state = WAIT_AUTH;
break;
case WAIT_AUTH:
c = receive(in);
switch (c.getCommand()) {
case COMMAND_AUTH:
if (null == c.getData()) {
send(out, COMMAND_REJECTED, getTypes(types));
} else {
switch (do_response(current, Uid, kernelUid, c)) {
case CONTINUE:
send(out, COMMAND_DATA, c.getResponse());
current = c.getMechs();
state = WAIT_DATA;
break;
case OK:
send(out, COMMAND_OK, guid);
state = WAIT_BEGIN;
current = 0;
break;
case REJECT:
send(out, COMMAND_REJECTED, getTypes(types));
current = 0;
break;
}
}
break;
case COMMAND_ERROR:
send(out, COMMAND_REJECTED, getTypes(types));
break;
case COMMAND_BEGIN:
state = FAILED;
break;
default:
send(out, COMMAND_ERROR, "Got invalid command");
break;
}
break;
case WAIT_DATA:
c = receive(in);
switch (c.getCommand()) {
case COMMAND_DATA:
switch (do_response(current, Uid, kernelUid, c)) {
case CONTINUE:
send(out, COMMAND_DATA, c.getResponse());
state = WAIT_DATA;
break;
case OK:
send(out, COMMAND_OK, guid);
state = WAIT_BEGIN;
current = 0;
break;
case REJECT:
send(out, COMMAND_REJECTED, getTypes(types));
current = 0;
break;
}
break;
case COMMAND_ERROR:
case COMMAND_CANCEL:
send(out, COMMAND_REJECTED, getTypes(types));
state = WAIT_AUTH;
break;
case COMMAND_BEGIN:
state = FAILED;
break;
default:
send(out, COMMAND_ERROR, "Got invalid command");
break;
}
break;
case WAIT_BEGIN:
c = receive(in);
switch (c.getCommand()) {
case COMMAND_ERROR:
case COMMAND_CANCEL:
send(out, COMMAND_REJECTED, getTypes(types));
state = WAIT_AUTH;
break;
case COMMAND_BEGIN:
state = AUTHENTICATED;
break;
default:
send(out, COMMAND_ERROR, "Got invalid command");
break;
}
break;
default:
state = FAILED;
}
break;
default:
return false;
}
@ -588,15 +781,25 @@ public class Transport {
types = SASL.AUTH_EXTERNAL;
mode = SASL.MODE_CLIENT;
us = new UnixSocket();
if (null != address.getParameter("path"))
us.connect(new jnr.unixsocket.UnixSocketAddress(new File(address.getParameter("path"))));
if (null != address.getParameter("abstract"))
us.connect(new UnixSocketAddress(address.getParameter("abstract"), true));
else if (null != address.getParameter("path"))
us.connect(new UnixSocketAddress(address.getParameter("path"), false));
us.setPassCred(true);
in = us.getInputStream();
out = us.getOutputStream();
} else if ("tcp".equals(address.getType())) {
types = SASL.AUTH_SHA;
mode = SASL.MODE_CLIENT;
s = new Socket();
s.connect(new InetSocketAddress(address.getParameter("host"), Integer.parseInt(address.getParameter("port"))));
if (null != address.getParameter("listen")) {
mode = SASL.MODE_SERVER;
ServerSocket ss = new ServerSocket();
ss.bind(new InetSocketAddress(address.getParameter("host"), Integer.parseInt(address.getParameter("port"))));
s = ss.accept();
} else {
mode = SASL.MODE_CLIENT;
s = new Socket();
s.connect(new InetSocketAddress(address.getParameter("host"), Integer.parseInt(address.getParameter("port"))));
}
in = s.getInputStream();
out = s.getOutputStream();
} else {

116
misc/CrossDataCenter.md Normal file
View file

@ -0,0 +1,116 @@
Test Cross-Data-Center scenario (test with external JDG server)
===============================================================
These are temporary notes. This docs should be removed once we have cross-DC support finished and properly documented.
What is working right now is:
- Propagating of invalidation messages for "realms" and "users" caches
- All the other things provided by ClusterProvider, which is:
-- ClusterStartupTime (used for offlineSessions and revokeRefreshToken) is shared for all clusters in all datacenters
-- Periodic userStorage synchronization is always executed just on one node at a time. It won't be never executed concurrently on more nodes (Assuming "nodes" refer to all servers in all clusters in all datacenters)
What doesn't work right now:
- UserSessionProvider and offline sessions
Basic setup
===========
This is setup with 2 keycloak nodes, which are NOT in cluster. They just share the same database and they will be configured with "work" infinispan cache with remoteStore, which will point
to external JDG server.
JDG Server setup
----------------
- Download JDG 7.0 server and unzip to some folder
- Add this into JDG_HOME/standalone/configuration/standalone.xml under cache-container named "local" :
```
<local-cache name="work" start="EAGER" batching="false" />
```
- Start server:
```
cd JDG_HOME/bin
./standalone.sh -Djboss.socket.binding.port-offset=100
```
Keycloak servers setup
----------------------
You need to setup 2 Keycloak nodes in this way.
For now, it's recommended to test Keycloak overlay on EAP7 because of infinispan bug, which is fixed in EAP 7.0 (infinispan 8.1.2), but not
yet on Wildfly 10 (infinispan 8.1.0). See below for details.
1) Configure shared database in KEYCLOAK_HOME/standalone/configuration/standalone.xml . For example MySQL
2) Add `module` attribute to the infinispan keycloak container:
```
<cache-container name="keycloak" jndi-name="infinispan/Keycloak" module="org.keycloak.keycloak-model-infinispan">
```
3) Configure `work` cache to use remoteStore. You should use this:
```
<local-cache name="work">
<remote-store passivation="false" fetch-state="false" purge="false" preload="false" shared="true" cache="work" remote-servers="remote-cache">
<property name="rawValues">true</property>
<property name="marshaller">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>
</remote-store>
</local-cache>
```
4) Configure connection to the external JDG server. Because we used port offset 100 for JDG (see above), the HotRod endpoint is running on 11322 .
So add the config like this to the bottom of standalone.xml under `socket-binding-group` element:
```
<outbound-socket-binding name="remote-cache">
<remote-destination host="localhost" port="11322"/>
</outbound-socket-binding>
```
5) Optional: Configure logging in standalone.xml to see what invalidation events were send:
````
<logger category="org.keycloak.cluster.infinispan">
<level name="TRACE"/>
</logger>
<logger category="org.keycloak.models.cache.infinispan">
<level name="DEBUG"/>
</logger>
````
6) Setup Keycloak node2 . Just copy Keycloak to another location on your laptop and repeat steps 1-5 above for second server too.
7) Run server 1 with parameters like (assuming you have virtual hosts "node1" and "node2" defined in your `/etc/hosts` ):
```
./standalone.sh -Djboss.node.name=node1 -b node1 -bmanagement node1
```
and server2 with:
```
./standalone.sh -Djboss.node.name=node2 -b node2 -bmanagement node2
```
8) Note something like this in both `KEYCLOAK_HOME/standalone/log/server.log` on both nodes. Note that cluster Startup Time will be same time on both nodes:
```
2016-11-16 22:12:52,080 DEBUG [org.keycloak.cluster.infinispan.InfinispanClusterProviderFactory] (ServerService Thread Pool -- 62) My address: node1-1953169551
2016-11-16 22:12:52,081 DEBUG [org.keycloak.cluster.infinispan.CrossDCAwareCacheFactory] (ServerService Thread Pool -- 62) RemoteStore is available. Cross-DC scenario will be used
2016-11-16 22:12:52,119 DEBUG [org.keycloak.cluster.infinispan.InfinispanClusterProviderFactory] (ServerService Thread Pool -- 62) Loaded cluster startup time: Wed Nov 16 22:09:48 CET 2016
2016-11-16 22:12:52,128 DEBUG [org.keycloak.cluster.infinispan.InfinispanNotificationsManager] (ServerService Thread Pool -- 62) Added listener for HotRod remoteStore cache: work
```
9) Login to node1. Then change any realm on node2. You will see in the node2 server.log that RealmUpdatedEvent was sent and on node1 that this event was received.
This is done even if node1 and node2 are NOT in cluster as it's the external JDG used for communication between 2 keycloak servers and sending/receiving cache invalidation events. But note that userSession
doesn't yet work (eg. if you login to node1, you won't see the userSession on node2).
WARNING: Previous steps works on Keycloak server overlay deployed on EAP 7.0 . With deploy on Wildfly 10.0.0.Final, you will see exception
at startup caused by the bug https://issues.jboss.org/browse/ISPN-6203 .
There is a workaround to add this line into KEYCLOAK_HOME/modules/system/layers/base/org/wildfly/clustering/service/main/module.xml :
```
<module name="org.infinispan.client.hotrod"/>
```

View file

@ -48,6 +48,10 @@
<groupId>org.infinispan</groupId>
<artifactId>infinispan-core</artifactId>
</dependency>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-cachestore-remote</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>

View file

@ -0,0 +1,93 @@
/*
* Copyright 2016 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.cluster.infinispan;
import java.io.Serializable;
import java.util.Set;
import org.infinispan.Cache;
import org.infinispan.client.hotrod.Flag;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.commons.api.BasicCache;
import org.infinispan.persistence.remote.RemoteStore;
import org.jboss.logging.Logger;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
abstract class CrossDCAwareCacheFactory {
protected static final Logger logger = Logger.getLogger(CrossDCAwareCacheFactory.class);
abstract BasicCache<String, Serializable> getCache();
static CrossDCAwareCacheFactory getFactory(Cache<String, Serializable> workCache, Set<RemoteStore> remoteStores) {
if (remoteStores.isEmpty()) {
logger.debugf("No configured remoteStore available. Cross-DC scenario is not used");
return new InfinispanCacheWrapperFactory(workCache);
} else {
logger.debugf("RemoteStore is available. Cross-DC scenario will be used");
if (remoteStores.size() > 1) {
logger.warnf("More remoteStores configured for work cache. Will use just the first one");
}
// For cross-DC scenario, we need to return underlying remoteCache for atomic operations to work properly
RemoteStore remoteStore = remoteStores.iterator().next();
RemoteCache remoteCache = remoteStore.getRemoteCache();
return new RemoteCacheWrapperFactory(remoteCache);
}
}
// We don't have external JDG configured. No cross-DC.
private static class InfinispanCacheWrapperFactory extends CrossDCAwareCacheFactory {
private final Cache<String, Serializable> workCache;
InfinispanCacheWrapperFactory(Cache<String, Serializable> workCache) {
this.workCache = workCache;
}
@Override
BasicCache<String, Serializable> getCache() {
return workCache;
}
}
// We have external JDG configured. Cross-DC should be enabled
private static class RemoteCacheWrapperFactory extends CrossDCAwareCacheFactory {
private final RemoteCache<String, Serializable> remoteCache;
RemoteCacheWrapperFactory(RemoteCache<String, Serializable> remoteCache) {
this.remoteCache = remoteCache;
}
@Override
BasicCache<String, Serializable> getCache() {
// Flags are per-invocation!
return remoteCache.withFlags(Flag.FORCE_RETURN_VALUE);
}
}
}

View file

@ -17,20 +17,15 @@
package org.keycloak.cluster.infinispan;
import org.infinispan.Cache;
import org.infinispan.context.Flag;
import org.infinispan.lifecycle.ComponentStatus;
import org.infinispan.remoting.transport.Transport;
import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterEvent;
import org.keycloak.cluster.ClusterListener;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.cluster.ExecutionResult;
import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession;
import java.io.Serializable;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
/**
*
@ -43,34 +38,22 @@ public class InfinispanClusterProvider implements ClusterProvider {
public static final String CLUSTER_STARTUP_TIME_KEY = "cluster-start-time";
private static final String TASK_KEY_PREFIX = "task::";
private final InfinispanClusterProviderFactory factory;
private final KeycloakSession session;
private final Cache<String, Serializable> cache;
private final int clusterStartupTime;
private final String myAddress;
private final CrossDCAwareCacheFactory crossDCAwareCacheFactory;
private final InfinispanNotificationsManager notificationsManager; // Just to extract notifications related stuff to separate class
public InfinispanClusterProvider(InfinispanClusterProviderFactory factory, KeycloakSession session, Cache<String, Serializable> cache) {
this.factory = factory;
this.session = session;
this.cache = cache;
public InfinispanClusterProvider(int clusterStartupTime, String myAddress, CrossDCAwareCacheFactory crossDCAwareCacheFactory, InfinispanNotificationsManager notificationsManager) {
this.myAddress = myAddress;
this.clusterStartupTime = clusterStartupTime;
this.crossDCAwareCacheFactory = crossDCAwareCacheFactory;
this.notificationsManager = notificationsManager;
}
@Override
public int getClusterStartupTime() {
Integer existingClusterStartTime = (Integer) cache.get(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY);
if (existingClusterStartTime != null) {
return existingClusterStartTime;
} else {
// clusterStartTime not yet initialized. Let's try to put our startupTime
int serverStartTime = (int) (session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
existingClusterStartTime = (Integer) cache.putIfAbsent(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY, serverStartTime);
if (existingClusterStartTime == null) {
logger.debugf("Initialized cluster startup time to %s", Time.toDate(serverStartTime).toString());
return serverStartTime;
} else {
return existingClusterStartTime;
}
}
return clusterStartupTime;
}
@ -104,56 +87,33 @@ public class InfinispanClusterProvider implements ClusterProvider {
@Override
public void registerListener(String taskKey, ClusterListener task) {
factory.registerListener(taskKey, task);
this.notificationsManager.registerListener(taskKey, task);
}
@Override
public void notify(String taskKey, ClusterEvent event) {
// Put the value to the cache to notify listeners on all the nodes
cache.put(taskKey, event);
public void notify(String taskKey, ClusterEvent event, boolean ignoreSender) {
this.notificationsManager.notify(taskKey, event, ignoreSender);
}
private String getCurrentNode(Cache<String, Serializable> cache) {
Transport transport = cache.getCacheManager().getTransport();
return transport==null ? "local" : transport.getAddress().toString();
}
private LockEntry createLockEntry(Cache<String, Serializable> cache) {
private LockEntry createLockEntry() {
LockEntry lock = new LockEntry();
lock.setNode(getCurrentNode(cache));
lock.setNode(myAddress);
lock.setTimestamp(Time.currentTime());
return lock;
}
private boolean tryLock(String cacheKey, int taskTimeoutInSeconds) {
LockEntry myLock = createLockEntry(cache);
LockEntry myLock = createLockEntry();
LockEntry existingLock = (LockEntry) cache.putIfAbsent(cacheKey, myLock);
LockEntry existingLock = (LockEntry) crossDCAwareCacheFactory.getCache().putIfAbsent(cacheKey, myLock, taskTimeoutInSeconds, TimeUnit.SECONDS);
if (existingLock != null) {
// Task likely already in progress. Check if timestamp is not outdated
int thatTime = existingLock.getTimestamp();
int currentTime = Time.currentTime();
if (thatTime + taskTimeoutInSeconds < currentTime) {
if (logger.isTraceEnabled()) {
logger.tracef("Task %s outdated when in progress by node %s. Will try to replace task with our node %s", cacheKey, existingLock.getNode(), myLock.getNode());
}
boolean replaced = cache.replace(cacheKey, existingLock, myLock);
if (!replaced) {
if (logger.isTraceEnabled()) {
logger.tracef("Failed to replace the task %s. Other thread replaced in the meantime. Ignoring task.", cacheKey);
}
}
return replaced;
} else {
if (logger.isTraceEnabled()) {
logger.tracef("Task %s in progress already by node %s. Ignoring task.", cacheKey, existingLock.getNode());
}
return false;
if (logger.isTraceEnabled()) {
logger.tracef("Task %s in progress already by node %s. Ignoring task.", cacheKey, existingLock.getNode());
}
return false;
} else {
if (logger.isTraceEnabled()) {
logger.tracef("Successfully acquired lock for task %s. Our node is %s", cacheKey, myLock.getNode());
@ -168,20 +128,12 @@ public class InfinispanClusterProvider implements ClusterProvider {
int retry = 3;
while (true) {
try {
cache.getAdvancedCache()
.withFlags(Flag.IGNORE_RETURN_VALUES, Flag.FORCE_SYNCHRONOUS)
.remove(cacheKey);
crossDCAwareCacheFactory.getCache().remove(cacheKey);
if (logger.isTraceEnabled()) {
logger.tracef("Task %s removed from the cache", cacheKey);
}
return;
} catch (RuntimeException e) {
ComponentStatus status = cache.getStatus();
if (status.isStopping() || status.isTerminated()) {
logger.warnf("Failed to remove task %s from the cache. Cache is already terminating", cacheKey);
logger.debug(e.getMessage(), e);
return;
}
retry--;
if (retry == 0) {
throw e;

View file

@ -20,27 +20,24 @@ package org.keycloak.cluster.infinispan;
import org.infinispan.Cache;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryModified;
import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent;
import org.infinispan.notifications.cachelistener.event.CacheEntryModifiedEvent;
import org.infinispan.notifications.cachemanagerlistener.annotation.ViewChanged;
import org.infinispan.notifications.cachemanagerlistener.event.ViewChangedEvent;
import org.infinispan.persistence.manager.PersistenceManager;
import org.infinispan.persistence.remote.RemoteStore;
import org.infinispan.remoting.transport.Address;
import org.infinispan.remoting.transport.Transport;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.cluster.ClusterEvent;
import org.keycloak.cluster.ClusterListener;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.cluster.ClusterProviderFactory;
import org.keycloak.common.util.HostUtils;
import org.keycloak.common.util.Time;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
@ -49,6 +46,8 @@ import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* This impl is aware of Cross-Data-Center scenario too
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanClusterProviderFactory implements ClusterProviderFactory {
@ -57,28 +56,82 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory
protected static final Logger logger = Logger.getLogger(InfinispanClusterProviderFactory.class);
// Infinispan cache
private volatile Cache<String, Serializable> workCache;
private Map<String, ClusterListener> listeners = new HashMap<>();
// Ensure that atomic operations (like putIfAbsent) must work correctly in any of: non-clustered, clustered or cross-Data-Center (cross-DC) setups
private CrossDCAwareCacheFactory crossDCAwareCacheFactory;
private String myAddress;
private int clusterStartupTime;
// Just to extract notifications related stuff to separate class
private InfinispanNotificationsManager notificationsManager;
@Override
public ClusterProvider create(KeycloakSession session) {
lazyInit(session);
return new InfinispanClusterProvider(this, session, workCache);
return new InfinispanClusterProvider(clusterStartupTime, myAddress, crossDCAwareCacheFactory, notificationsManager);
}
private void lazyInit(KeycloakSession session) {
if (workCache == null) {
synchronized (this) {
if (workCache == null) {
workCache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
InfinispanConnectionProvider ispnConnections = session.getProvider(InfinispanConnectionProvider.class);
workCache = ispnConnections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
workCache.getCacheManager().addListener(new ViewChangeListener());
workCache.addListener(new CacheEntryListener());
initMyAddress();
Set<RemoteStore> remoteStores = getRemoteStores();
crossDCAwareCacheFactory = CrossDCAwareCacheFactory.getFactory(workCache, remoteStores);
clusterStartupTime = initClusterStartupTime(session);
notificationsManager = InfinispanNotificationsManager.create(workCache, myAddress, remoteStores);
}
}
}
}
// See if we have RemoteStore (external JDG) configured for cross-Data-Center scenario
private Set<RemoteStore> getRemoteStores() {
return workCache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class);
}
protected void initMyAddress() {
Transport transport = workCache.getCacheManager().getTransport();
this.myAddress = transport == null ? HostUtils.getHostName() + "-" + workCache.hashCode() : transport.getAddress().toString();
logger.debugf("My address: %s", this.myAddress);
}
protected int initClusterStartupTime(KeycloakSession session) {
Integer existingClusterStartTime = (Integer) crossDCAwareCacheFactory.getCache().get(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY);
if (existingClusterStartTime != null) {
logger.debugf("Loaded cluster startup time: %s", Time.toDate(existingClusterStartTime).toString());
return existingClusterStartTime;
} else {
// clusterStartTime not yet initialized. Let's try to put our startupTime
int serverStartTime = (int) (session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
existingClusterStartTime = (Integer) crossDCAwareCacheFactory.getCache().putIfAbsent(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY, serverStartTime);
if (existingClusterStartTime == null) {
logger.debugf("Initialized cluster startup time to %s", Time.toDate(serverStartTime).toString());
return serverStartTime;
} else {
logger.debugf("Loaded cluster startup time: %s", Time.toDate(existingClusterStartTime).toString());
return existingClusterStartTime;
}
}
}
@Override
public void init(Config.Scope config) {
}
@ -167,34 +220,4 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory
}
<T> void registerListener(String taskKey, ClusterListener task) {
listeners.put(taskKey, task);
}
@Listener
public class CacheEntryListener {
@CacheEntryCreated
public void cacheEntryCreated(CacheEntryCreatedEvent<String, Object> event) {
if (!event.isPre()) {
trigger(event.getKey(), event.getValue());
}
}
@CacheEntryModified
public void cacheEntryModified(CacheEntryModifiedEvent<String, Object> event) {
if (!event.isPre()) {
trigger(event.getKey(), event.getValue());
}
}
private void trigger(String key, Object value) {
ClusterListener task = listeners.get(key);
if (task != null) {
ClusterEvent event = (ClusterEvent) value;
task.run(event);
}
}
}
}

View file

@ -0,0 +1,204 @@
/*
* Copyright 2016 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.cluster.infinispan;
import java.io.Serializable;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.infinispan.Cache;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated;
import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified;
import org.infinispan.client.hotrod.annotation.ClientListener;
import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent;
import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent;
import org.infinispan.client.hotrod.event.ClientEvent;
import org.infinispan.context.Flag;
import org.infinispan.marshall.core.MarshalledEntry;
import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryModified;
import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent;
import org.infinispan.notifications.cachelistener.event.CacheEntryModifiedEvent;
import org.infinispan.persistence.manager.PersistenceManager;
import org.infinispan.persistence.remote.RemoteStore;
import org.infinispan.remoting.transport.Transport;
import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterEvent;
import org.keycloak.cluster.ClusterListener;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.common.util.HostUtils;
import org.keycloak.common.util.MultivaluedHashMap;
/**
* Impl for sending infinispan messages across cluster and listening to them
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanNotificationsManager {
protected static final Logger logger = Logger.getLogger(InfinispanNotificationsManager.class);
private final MultivaluedHashMap<String, ClusterListener> listeners = new MultivaluedHashMap<>();
private final Cache<String, Serializable> workCache;
private final String myAddress;
protected InfinispanNotificationsManager(Cache<String, Serializable> workCache, String myAddress) {
this.workCache = workCache;
this.myAddress = myAddress;
}
// Create and init manager including all listeners etc
public static InfinispanNotificationsManager create(Cache<String, Serializable> workCache, String myAddress, Set<RemoteStore> remoteStores) {
InfinispanNotificationsManager manager = new InfinispanNotificationsManager(workCache, myAddress);
// We need CacheEntryListener just if we don't have remoteStore. With remoteStore will be all cluster nodes notified anyway from HotRod listener
if (remoteStores.isEmpty()) {
workCache.addListener(manager.new CacheEntryListener());
logger.debugf("Added listener for infinispan cache: %s", workCache.getName());
} else {
for (RemoteStore remoteStore : remoteStores) {
RemoteCache<Object, Object> remoteCache = remoteStore.getRemoteCache();
remoteCache.addClientListener(manager.new HotRodListener(remoteCache));
logger.debugf("Added listener for HotRod remoteStore cache: %s", remoteCache.getName());
}
}
return manager;
}
void registerListener(String taskKey, ClusterListener task) {
listeners.add(taskKey, task);
}
void notify(String taskKey, ClusterEvent event, boolean ignoreSender) {
WrapperClusterEvent wrappedEvent = new WrapperClusterEvent();
wrappedEvent.setDelegateEvent(event);
wrappedEvent.setIgnoreSender(ignoreSender);
wrappedEvent.setSender(myAddress);
if (logger.isTraceEnabled()) {
logger.tracef("Sending event %s: %s", taskKey, event);
}
// Put the value to the cache to notify listeners on all the nodes
workCache.getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES)
.put(taskKey, wrappedEvent, 120, TimeUnit.SECONDS);
}
@Listener(observation = Listener.Observation.POST)
public class CacheEntryListener {
@CacheEntryCreated
public void cacheEntryCreated(CacheEntryCreatedEvent<String, Serializable> event) {
eventReceived(event.getKey(), event.getValue());
}
@CacheEntryModified
public void cacheEntryModified(CacheEntryModifiedEvent<String, Serializable> event) {
eventReceived(event.getKey(), event.getValue());
}
}
@ClientListener
public class HotRodListener {
private final RemoteCache<Object, Object> remoteCache;
public HotRodListener(RemoteCache<Object, Object> remoteCache) {
this.remoteCache = remoteCache;
}
@ClientCacheEntryCreated
public void created(ClientCacheEntryCreatedEvent event) {
String key = event.getKey().toString();
hotrodEventReceived(key);
}
@ClientCacheEntryModified
public void updated(ClientCacheEntryModifiedEvent event) {
String key = event.getKey().toString();
hotrodEventReceived(key);
}
private void hotrodEventReceived(String key) {
// TODO: Look at CacheEventConverter stuff to possibly include value in the event and avoid additional remoteCache request
Object value = remoteCache.get(key);
Serializable rawValue;
if (value instanceof MarshalledEntry) {
Object rw = ((MarshalledEntry)value).getValue();
rawValue = (Serializable) rw;
} else {
rawValue = (Serializable) value;
}
eventReceived(key, rawValue);
}
}
private void eventReceived(String key, Serializable obj) {
if (!(obj instanceof WrapperClusterEvent)) {
return;
}
WrapperClusterEvent event = (WrapperClusterEvent) obj;
if (event.isIgnoreSender()) {
if (this.myAddress.equals(event.getSender())) {
return;
}
}
if (logger.isTraceEnabled()) {
logger.tracef("Received event %s: %s", key, event);
}
ClusterEvent wrappedEvent = event.getDelegateEvent();
List<ClusterListener> myListeners = listeners.get(key);
if (myListeners != null) {
for (ClusterListener listener : myListeners) {
listener.eventReceived(wrappedEvent);
}
}
myListeners = listeners.get(ClusterProvider.ALL);
if (myListeners != null) {
for (ClusterListener listener : myListeners) {
listener.eventReceived(wrappedEvent);
}
}
}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright 2016 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.cluster.infinispan;
import org.infinispan.commons.marshall.jboss.GenericJBossMarshaller;
/**
* Needed on Wildfly, so that remoteStore (hotRod client) can find our classes
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class KeycloakHotRodMarshallerFactory {
public static GenericJBossMarshaller getInstance() {
return new GenericJBossMarshaller(KeycloakHotRodMarshallerFactory.class.getClassLoader());
}
}

View file

@ -0,0 +1,59 @@
/*
* Copyright 2016 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.cluster.infinispan;
import org.keycloak.cluster.ClusterEvent;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class WrapperClusterEvent implements ClusterEvent {
private String sender; // will be null in non-clustered environment
private boolean ignoreSender;
private ClusterEvent delegateEvent;
public String getSender() {
return sender;
}
public void setSender(String sender) {
this.sender = sender;
}
public boolean isIgnoreSender() {
return ignoreSender;
}
public void setIgnoreSender(boolean ignoreSender) {
this.ignoreSender = ignoreSender;
}
public ClusterEvent getDelegateEvent() {
return delegateEvent;
}
public void setDelegateEvent(ClusterEvent delegateEvent) {
this.delegateEvent = delegateEvent;
}
@Override
public String toString() {
return String.format("WrapperClusterEvent [ sender=%s, delegateEvent=%s ]", sender, delegateEvent.toString());
}
}

View file

@ -27,11 +27,14 @@ import org.infinispan.eviction.EvictionStrategy;
import org.infinispan.eviction.EvictionType;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.persistence.remote.configuration.ExhaustedAction;
import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
import org.infinispan.transaction.LockingMode;
import org.infinispan.transaction.TransactionMode;
import org.infinispan.transaction.lookup.DummyTransactionManagerLookup;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@ -126,7 +129,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
boolean clustered = config.getBoolean("clustered", false);
boolean async = config.getBoolean("async", true);
boolean async = config.getBoolean("async", false);
boolean allowDuplicateJMXDomains = config.getBoolean("allowDuplicateJMXDomains", true);
if (clustered) {
@ -139,14 +142,11 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
logger.debug("Started embedded Infinispan cache container");
ConfigurationBuilder invalidationConfigBuilder = new ConfigurationBuilder();
if (clustered) {
invalidationConfigBuilder.clustering().cacheMode(async ? CacheMode.INVALIDATION_ASYNC : CacheMode.INVALIDATION_SYNC);
}
Configuration invalidationCacheConfiguration = invalidationConfigBuilder.build();
ConfigurationBuilder modelCacheConfigBuilder = new ConfigurationBuilder();
Configuration modelCacheConfiguration = modelCacheConfigBuilder.build();
cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_CACHE_NAME, invalidationCacheConfiguration);
cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_CACHE_NAME, invalidationCacheConfiguration);
cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_CACHE_NAME, modelCacheConfiguration);
cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_CACHE_NAME, modelCacheConfiguration);
ConfigurationBuilder sessionConfigBuilder = new ConfigurationBuilder();
if (clustered) {
@ -174,8 +174,14 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
if (clustered) {
replicationConfigBuilder.clustering().cacheMode(async ? CacheMode.REPL_ASYNC : CacheMode.REPL_SYNC);
}
Configuration replicationCacheConfiguration = replicationConfigBuilder.build();
cacheManager.defineConfiguration(InfinispanConnectionProvider.WORK_CACHE_NAME, replicationCacheConfiguration);
boolean jdgEnabled = config.getBoolean("remoteStoreEnabled", false);
if (jdgEnabled) {
configureRemoteCacheStore(replicationConfigBuilder, async);
}
Configuration replicationEvictionCacheConfiguration = replicationConfigBuilder.build();
cacheManager.defineConfiguration(InfinispanConnectionProvider.WORK_CACHE_NAME, replicationEvictionCacheConfiguration);
ConfigurationBuilder counterConfigBuilder = new ConfigurationBuilder();
counterConfigBuilder.invocationBatching().enable()
@ -211,6 +217,34 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
return cb.build();
}
// Used for cross-data centers scenario. Usually integration with external JDG server, which itself handles communication between DCs.
private void configureRemoteCacheStore(ConfigurationBuilder builder, boolean async) {
String jdgServer = config.get("remoteStoreServer", "localhost");
Integer jdgPort = config.getInt("remoteStorePort", 11222);
builder.persistence()
.passivation(false)
.addStore(RemoteStoreConfigurationBuilder.class)
.fetchPersistentState(false)
.ignoreModifications(false)
.purgeOnStartup(false)
.preload(false)
.shared(true)
.remoteCacheName(InfinispanConnectionProvider.WORK_CACHE_NAME)
.rawValues(true)
.forceReturnValues(false)
.marshaller(KeycloakHotRodMarshallerFactory.class.getName())
.addServer()
.host(jdgServer)
.port(jdgPort)
// .connectionPool()
// .maxActive(100)
// .exhaustedAction(ExhaustedAction.CREATE_NEW)
.async()
.enabled(async);
}
protected Configuration getKeysCacheConfig() {
ConfigurationBuilder cb = new ConfigurationBuilder();
cb.eviction().strategy(EvictionStrategy.LRU).type(EvictionType.COUNT).size(InfinispanConnectionProvider.KEYS_CACHE_DEFAULT_MAX);

View file

@ -1,14 +1,13 @@
package org.keycloak.models.cache.infinispan;
import org.infinispan.Cache;
import org.infinispan.notifications.cachelistener.annotation.CacheEntriesEvicted;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryInvalidated;
import org.infinispan.notifications.cachelistener.event.CacheEntriesEvictedEvent;
import org.infinispan.notifications.cachelistener.event.CacheEntryInvalidatedEvent;
import org.jboss.logging.Logger;
import org.keycloak.models.cache.infinispan.entities.AbstractRevisioned;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
@ -55,7 +54,7 @@ import java.util.function.Predicate;
* @version $Revision: 1 $
*/
public abstract class CacheManager {
protected static final Logger logger = Logger.getLogger(CacheManager.class);
protected final Cache<String, Long> revisions;
protected final Cache<String, Revisioned> cache;
protected final UpdateCounter counter = new UpdateCounter();
@ -63,9 +62,10 @@ public abstract class CacheManager {
public CacheManager(Cache<String, Revisioned> cache, Cache<String, Long> revisions) {
this.cache = cache;
this.revisions = revisions;
this.cache.addListener(this);
}
protected abstract Logger getLogger();
public Cache<String, Revisioned> getCache() {
return cache;
}
@ -79,10 +79,7 @@ public abstract class CacheManager {
if (revision == null) {
revision = counter.current();
}
// if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event
// so, we do this to force this.
String invalidationKey = "invalidation.key" + id;
cache.putForExternalRead(invalidationKey, new AbstractRevisioned(-1L, invalidationKey));
return revision;
}
@ -101,12 +98,16 @@ public abstract class CacheManager {
}
Long rev = revisions.get(id);
if (rev == null) {
RealmCacheManager.logger.tracev("get() missing rev");
if (getLogger().isTraceEnabled()) {
getLogger().tracev("get() missing rev {0}", id);
}
return null;
}
long oRev = o.getRevision() == null ? -1L : o.getRevision().longValue();
if (rev > oRev) {
RealmCacheManager.logger.tracev("get() rev: {0} o.rev: {1}", rev.longValue(), oRev);
if (getLogger().isTraceEnabled()) {
getLogger().tracev("get() rev: {0} o.rev: {1}", rev.longValue(), oRev);
}
return null;
}
return o != null && type.isInstance(o) ? type.cast(o) : null;
@ -114,9 +115,11 @@ public abstract class CacheManager {
public Object invalidateObject(String id) {
Revisioned removed = (Revisioned)cache.remove(id);
// if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event
// so, we do this to force the event.
cache.remove("invalidation.key" + id);
if (getLogger().isTraceEnabled()) {
getLogger().tracef("Removed key='%s', value='%s' from cache", id, removed);
}
bumpVersion(id);
return removed;
}
@ -137,37 +140,35 @@ public abstract class CacheManager {
//revisions.getAdvancedCache().lock(id);
Long rev = revisions.get(id);
if (rev == null) {
if (id.endsWith("realm.clients")) RealmCacheManager.logger.trace("addRevisioned rev == null realm.clients");
rev = counter.current();
revisions.put(id, rev);
}
revisions.startBatch();
if (!revisions.getAdvancedCache().lock(id)) {
RealmCacheManager.logger.trace("Could not obtain version lock");
if (getLogger().isTraceEnabled()) {
getLogger().tracev("Could not obtain version lock: {0}", id);
}
return;
}
rev = revisions.get(id);
if (rev == null) {
if (id.endsWith("realm.clients")) RealmCacheManager.logger.trace("addRevisioned rev2 == null realm.clients");
return;
}
if (rev > startupRevision) { // revision is ahead transaction start. Other transaction updated in the meantime. Don't cache
if (RealmCacheManager.logger.isTraceEnabled()) {
RealmCacheManager.logger.tracev("Skipped cache. Current revision {0}, Transaction start revision {1}", object.getRevision(), startupRevision);
if (getLogger().isTraceEnabled()) {
getLogger().tracev("Skipped cache. Current revision {0}, Transaction start revision {1}", object.getRevision(), startupRevision);
}
return;
}
if (rev.equals(object.getRevision())) {
if (id.endsWith("realm.clients")) RealmCacheManager.logger.tracev("adding Object.revision {0} rev {1}", object.getRevision(), rev);
cache.putForExternalRead(id, object);
return;
}
if (rev > object.getRevision()) { // revision is ahead, don't cache
if (id.endsWith("realm.clients")) RealmCacheManager.logger.trace("addRevisioned revision is ahead realm.clients");
if (getLogger().isTraceEnabled()) getLogger().tracev("Skipped cache. Object revision {0}, Cache revision {1}", object.getRevision(), rev);
return;
}
// revisions cache has a lower value than the object.revision, so update revision and add it to cache
if (id.endsWith("realm.clients")) RealmCacheManager.logger.tracev("adding Object.revision {0} rev {1}", object.getRevision(), rev);
revisions.put(id, object.getRevision());
if (lifespan < 0) cache.putForExternalRead(id, object);
else cache.putForExternalRead(id, object, lifespan, TimeUnit.MILLISECONDS);
@ -196,63 +197,36 @@ public abstract class CacheManager {
.filter(predicate).iterator();
}
@CacheEntryInvalidated
public void cacheInvalidated(CacheEntryInvalidatedEvent<String, Object> event) {
if (event.isPre()) {
String key = event.getKey();
if (key.startsWith("invalidation.key")) {
// if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event
// so, we do this to force this.
String bump = key.substring("invalidation.key".length());
RealmCacheManager.logger.tracev("bumping invalidation key {0}", bump);
bumpVersion(bump);
return;
}
} else {
//if (!event.isPre()) {
String key = event.getKey();
if (key.startsWith("invalidation.key")) {
// if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event
// so, we do this to force this.
String bump = key.substring("invalidation.key".length());
bumpVersion(bump);
RealmCacheManager.logger.tracev("bumping invalidation key {0}", bump);
return;
}
bumpVersion(key);
Object object = event.getValue();
if (object != null) {
bumpVersion(key);
Predicate<Map.Entry<String, Revisioned>> predicate = getInvalidationPredicate(object);
if (predicate != null) runEvictions(predicate);
RealmCacheManager.logger.tracev("invalidating: {0}" + object.getClass().getName());
}
public void sendInvalidationEvents(KeycloakSession session, Collection<InvalidationEvent> invalidationEvents) {
ClusterProvider clusterProvider = session.getProvider(ClusterProvider.class);
// Maybe add InvalidationEvent, which will be collection of all invalidationEvents? That will reduce cluster traffic even more.
for (InvalidationEvent event : invalidationEvents) {
clusterProvider.notify(generateEventId(event), event, true);
}
}
@CacheEntriesEvicted
public void cacheEvicted(CacheEntriesEvictedEvent<String, Object> event) {
if (!event.isPre())
for (Map.Entry<String, Object> entry : event.getEntries().entrySet()) {
Object object = entry.getValue();
bumpVersion(entry.getKey());
if (object == null) continue;
RealmCacheManager.logger.tracev("evicting: {0}" + object.getClass().getName());
Predicate<Map.Entry<String, Revisioned>> predicate = getInvalidationPredicate(object);
if (predicate != null) runEvictions(predicate);
protected String generateEventId(InvalidationEvent event) {
return new StringBuilder(event.getId())
.append("_")
.append(event.hashCode())
.toString();
}
protected void invalidationEventReceived(InvalidationEvent event) {
Set<String> invalidations = new HashSet<>();
addInvalidationsFromEvent(event, invalidations);
getLogger().debugf("Invalidating %d cache items after received event %s", invalidations.size(), event);
for (String invalidation : invalidations) {
invalidateObject(invalidation);
}
}
public void runEvictions(Predicate<Map.Entry<String, Revisioned>> current) {
Set<String> evictions = new HashSet<>();
addInvalidations(current, evictions);
RealmCacheManager.logger.tracev("running evictions size: {0}", evictions.size());
for (String key : evictions) {
cache.evict(key);
bumpVersion(key);
}
}
protected abstract void addInvalidationsFromEvent(InvalidationEvent event, Set<String> invalidations);
protected abstract Predicate<Map.Entry<String, Revisioned>> getInvalidationPredicate(Object object);
}

View file

@ -52,7 +52,7 @@ public class ClientAdapter implements ClientModel {
private void getDelegateForUpdate() {
if (updated == null) {
cacheSession.registerClientInvalidation(cached.getId());
cacheSession.registerClientInvalidation(cached.getId(), cached.getClientId(), cachedRealm.getId());
updated = cacheSession.getDelegate().getClientById(cached.getId(), cachedRealm);
if (updated == null) throw new IllegalStateException("Not found in database");
}
@ -577,18 +577,12 @@ public class ClientAdapter implements ClientModel {
@Override
public RoleModel addRole(String name) {
getDelegateForUpdate();
RoleModel role = updated.addRole(name);
cacheSession.registerRoleInvalidation(role.getId());
return role;
return cacheSession.addClientRole(getRealm(), this, name);
}
@Override
public RoleModel addRole(String id, String name) {
getDelegateForUpdate();
RoleModel role = updated.addRole(id, name);
cacheSession.registerRoleInvalidation(role.getId());
return role;
return cacheSession.addClientRole(getRealm(), this, id, name);
}
@Override

View file

@ -21,7 +21,6 @@ import org.infinispan.Cache;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.cluster.ClusterEvent;
import org.keycloak.cluster.ClusterListener;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
@ -29,6 +28,7 @@ import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.cache.CacheRealmProvider;
import org.keycloak.models.cache.CacheRealmProviderFactory;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -54,14 +54,23 @@ public class InfinispanCacheRealmProviderFactory implements CacheRealmProviderFa
Cache<String, Revisioned> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
Cache<String, Long> revisions = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_NAME);
realmCache = new RealmCacheManager(cache, revisions);
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.registerListener(REALM_CLEAR_CACHE_EVENTS, new ClusterListener() {
@Override
public void run(ClusterEvent event) {
realmCache.clear();
cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> {
if (event instanceof InvalidationEvent) {
InvalidationEvent invalidationEvent = (InvalidationEvent) event;
realmCache.invalidationEventReceived(invalidationEvent);
}
});
cluster.registerListener(REALM_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> {
realmCache.clear();
});
log.debug("Registered cluster listeners");
}
}
}

View file

@ -21,7 +21,6 @@ import org.infinispan.Cache;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.cluster.ClusterEvent;
import org.keycloak.cluster.ClusterListener;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
@ -29,6 +28,7 @@ import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.cache.UserCache;
import org.keycloak.models.cache.UserCacheProviderFactory;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -55,13 +55,25 @@ public class InfinispanUserCacheProviderFactory implements UserCacheProviderFact
Cache<String, Revisioned> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_CACHE_NAME);
Cache<String, Long> revisions = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME);
userCache = new UserCacheManager(cache, revisions);
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.registerListener(USER_CLEAR_CACHE_EVENTS, new ClusterListener() {
@Override
public void run(ClusterEvent event) {
userCache.clear();
cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> {
if (event instanceof InvalidationEvent) {
InvalidationEvent invalidationEvent = (InvalidationEvent) event;
userCache.invalidationEventReceived(invalidationEvent);
}
});
cluster.registerListener(USER_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> {
userCache.clear();
});
log.debug("Registered cluster listeners");
}
}
}

View file

@ -39,13 +39,8 @@ import org.keycloak.models.UserFederationMapperModel;
import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.cache.CachedRealmModel;
import org.keycloak.models.cache.infinispan.entities.CachedRealm;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.storage.UserStorageProvider;
import java.security.Key;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
@ -75,7 +70,7 @@ public class RealmAdapter implements CachedRealmModel {
@Override
public RealmModel getDelegateForUpdate() {
if (updated == null) {
cacheSession.registerRealmInvalidation(cached.getId());
cacheSession.registerRealmInvalidation(cached.getId(), cached.getName());
updated = cacheSession.getDelegate().getRealm(cached.getId());
if (updated == null) throw new IllegalStateException("Not found in database");
}
@ -731,13 +726,6 @@ public class RealmAdapter implements CachedRealmModel {
updated.setNotBefore(notBefore);
}
@Override
public boolean removeRoleById(String id) {
cacheSession.registerRoleInvalidation(id);
getDelegateForUpdate();
return updated.removeRoleById(id);
}
@Override
public boolean isEventsEnabled() {
if (isUpdated()) return updated.isEventsEnabled();
@ -837,18 +825,12 @@ public class RealmAdapter implements CachedRealmModel {
@Override
public RoleModel addRole(String name) {
getDelegateForUpdate();
RoleModel role = updated.addRole(name);
cacheSession.registerRoleInvalidation(role.getId());
return role;
return cacheSession.addRealmRole(this, name);
}
@Override
public RoleModel addRole(String id, String name) {
getDelegateForUpdate();
RoleModel role = updated.addRole(id, name);
cacheSession.registerRoleInvalidation(role.getId());
return role;
return cacheSession.addRealmRole(this, id, name);
}
@Override
@ -1257,12 +1239,6 @@ public class RealmAdapter implements CachedRealmModel {
return cacheSession.createGroup(this, id, name);
}
@Override
public void addTopLevelGroup(GroupModel subGroup) {
cacheSession.addTopLevelGroup(this, subGroup);
}
@Override
public void moveGroup(GroupModel group, GroupModel toParent) {
cacheSession.moveGroup(this, group, toParent);

View file

@ -18,145 +18,88 @@
package org.keycloak.models.cache.infinispan;
import org.infinispan.Cache;
import org.infinispan.notifications.Listener;
import org.jboss.logging.Logger;
import org.keycloak.models.cache.infinispan.entities.CachedClient;
import org.keycloak.models.cache.infinispan.entities.CachedClientTemplate;
import org.keycloak.models.cache.infinispan.entities.CachedGroup;
import org.keycloak.models.cache.infinispan.entities.CachedRealm;
import org.keycloak.models.cache.infinispan.entities.CachedRole;
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
import org.keycloak.models.cache.infinispan.stream.ClientQueryPredicate;
import org.keycloak.models.cache.infinispan.stream.ClientTemplateQueryPredicate;
import org.keycloak.models.cache.infinispan.stream.GroupQueryPredicate;
import org.keycloak.models.cache.infinispan.events.RealmCacheInvalidationEvent;
import org.keycloak.models.cache.infinispan.stream.HasRolePredicate;
import org.keycloak.models.cache.infinispan.stream.InClientPredicate;
import org.keycloak.models.cache.infinispan.stream.InRealmPredicate;
import org.keycloak.models.cache.infinispan.stream.RealmQueryPredicate;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@Listener
public class RealmCacheManager extends CacheManager {
protected static final Logger logger = Logger.getLogger(RealmCacheManager.class);
private static final Logger logger = Logger.getLogger(RealmCacheManager.class);
@Override
protected Logger getLogger() {
return logger;
}
public RealmCacheManager(Cache<String, Revisioned> cache, Cache<String, Long> revisions) {
super(cache, revisions);
}
public void realmInvalidation(String id, Set<String> invalidations) {
Predicate<Map.Entry<String, Revisioned>> predicate = getRealmInvalidationPredicate(id);
addInvalidations(predicate, invalidations);
public void realmUpdated(String id, String name, Set<String> invalidations) {
invalidations.add(id);
invalidations.add(RealmCacheSession.getRealmByNameCacheKey(name));
}
public Predicate<Map.Entry<String, Revisioned>> getRealmInvalidationPredicate(String id) {
return RealmQueryPredicate.create().realm(id);
public void realmRemoval(String id, String name, Set<String> invalidations) {
realmUpdated(id, name, invalidations);
addInvalidations(InRealmPredicate.create().realm(id), invalidations);
}
public void clientInvalidation(String id, Set<String> invalidations) {
addInvalidations(getClientInvalidationPredicate(id), invalidations);
public void roleAdded(String roleContainerId, Set<String> invalidations) {
invalidations.add(RealmCacheSession.getRolesCacheKey(roleContainerId));
}
public Predicate<Map.Entry<String, Revisioned>> getClientInvalidationPredicate(String id) {
return ClientQueryPredicate.create().client(id);
public void roleUpdated(String roleContainerId, String roleName, Set<String> invalidations) {
invalidations.add(RealmCacheSession.getRoleByNameCacheKey(roleContainerId, roleName));
}
public void roleInvalidation(String id, Set<String> invalidations) {
addInvalidations(getRoleInvalidationPredicate(id), invalidations);
public void roleRemoval(String id, String roleName, String roleContainerId, Set<String> invalidations) {
invalidations.add(RealmCacheSession.getRolesCacheKey(roleContainerId));
invalidations.add(RealmCacheSession.getRoleByNameCacheKey(roleContainerId, roleName));
addInvalidations(HasRolePredicate.create().role(id), invalidations);
}
public Predicate<Map.Entry<String, Revisioned>> getRoleInvalidationPredicate(String id) {
return HasRolePredicate.create().role(id);
public void groupQueriesInvalidations(String realmId, Set<String> invalidations) {
invalidations.add(RealmCacheSession.getGroupsQueryCacheKey(realmId));
invalidations.add(RealmCacheSession.getTopGroupsQueryCacheKey(realmId)); // Just easier to always invalidate top-level too. It's not big performance penalty
}
public void groupInvalidation(String id, Set<String> invalidations) {
addInvalidations(getGroupInvalidationPredicate(id), invalidations);
public void clientAdded(String realmId, String clientUUID, String clientId, Set<String> invalidations) {
invalidations.add(RealmCacheSession.getRealmClientsQueryCacheKey(realmId));
}
public Predicate<Map.Entry<String, Revisioned>> getGroupInvalidationPredicate(String id) {
return GroupQueryPredicate.create().group(id);
public void clientUpdated(String realmId, String clientUuid, String clientId, Set<String> invalidations) {
invalidations.add(RealmCacheSession.getClientByClientIdCacheKey(clientId, realmId));
}
public void clientTemplateInvalidation(String id, Set<String> invalidations) {
addInvalidations(getClientTemplateInvalidationPredicate(id), invalidations);
// Client roles invalidated separately
public void clientRemoval(String realmId, String clientUUID, String clientId, Set<String> invalidations) {
invalidations.add(RealmCacheSession.getRealmClientsQueryCacheKey(realmId));
invalidations.add(RealmCacheSession.getClientByClientIdCacheKey(clientId, realmId));
addInvalidations(InClientPredicate.create().client(clientUUID), invalidations);
}
public Predicate<Map.Entry<String, Revisioned>> getClientTemplateInvalidationPredicate(String id) {
return ClientTemplateQueryPredicate.create().template(id);
}
public void realmRemoval(String id, Set<String> invalidations) {
Predicate<Map.Entry<String, Revisioned>> predicate = getRealmRemovalPredicate(id);
addInvalidations(predicate, invalidations);
}
public Predicate<Map.Entry<String, Revisioned>> getRealmRemovalPredicate(String id) {
Predicate<Map.Entry<String, Revisioned>> predicate = null;
predicate = RealmQueryPredicate.create().realm(id)
.or(InRealmPredicate.create().realm(id));
return predicate;
}
public void clientAdded(String realmId, String id, Set<String> invalidations) {
addInvalidations(getClientAddedPredicate(realmId), invalidations);
}
public Predicate<Map.Entry<String, Revisioned>> getClientAddedPredicate(String realmId) {
return ClientQueryPredicate.create().inRealm(realmId);
}
public void clientRemoval(String realmId, String id, Set<String> invalidations) {
Predicate<Map.Entry<String, Revisioned>> predicate = null;
predicate = getClientRemovalPredicate(realmId, id);
addInvalidations(predicate, invalidations);
}
public Predicate<Map.Entry<String, Revisioned>> getClientRemovalPredicate(String realmId, String id) {
Predicate<Map.Entry<String, Revisioned>> predicate;
predicate = ClientQueryPredicate.create().inRealm(realmId)
.or(ClientQueryPredicate.create().client(id))
.or(InClientPredicate.create().client(id));
return predicate;
}
public void roleRemoval(String id, Set<String> invalidations) {
addInvalidations(getRoleRemovalPredicate(id), invalidations);
}
public Predicate<Map.Entry<String, Revisioned>> getRoleRemovalPredicate(String id) {
return getRoleInvalidationPredicate(id);
}
@Override
protected Predicate<Map.Entry<String, Revisioned>> getInvalidationPredicate(Object object) {
if (object instanceof CachedRealm) {
CachedRealm cached = (CachedRealm)object;
return getRealmRemovalPredicate(cached.getId());
} else if (object instanceof CachedClient) {
CachedClient cached = (CachedClient)object;
Predicate<Map.Entry<String, Revisioned>> predicate = getClientRemovalPredicate(cached.getRealm(), cached.getId());
return predicate;
} else if (object instanceof CachedRole) {
CachedRole cached = (CachedRole)object;
return getRoleRemovalPredicate(cached.getId());
} else if (object instanceof CachedGroup) {
CachedGroup cached = (CachedGroup)object;
return getGroupInvalidationPredicate(cached.getId());
} else if (object instanceof CachedClientTemplate) {
CachedClientTemplate cached = (CachedClientTemplate)object;
return getClientTemplateInvalidationPredicate(cached.getId());
protected void addInvalidationsFromEvent(InvalidationEvent event, Set<String> invalidations) {
if (event instanceof RealmCacheInvalidationEvent) {
invalidations.add(event.getId());
((RealmCacheInvalidationEvent) event).addInvalidations(this, invalidations);
}
return null;
}
}

View file

@ -19,6 +19,7 @@ package org.keycloak.models.cache.infinispan;
import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
import org.keycloak.migration.MigrationModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientTemplateModel;
@ -38,8 +39,22 @@ import org.keycloak.models.cache.infinispan.entities.CachedRealm;
import org.keycloak.models.cache.infinispan.entities.CachedRealmRole;
import org.keycloak.models.cache.infinispan.entities.CachedRole;
import org.keycloak.models.cache.infinispan.entities.ClientListQuery;
import org.keycloak.models.cache.infinispan.entities.GroupListQuery;
import org.keycloak.models.cache.infinispan.entities.RealmListQuery;
import org.keycloak.models.cache.infinispan.entities.RoleListQuery;
import org.keycloak.models.cache.infinispan.events.ClientAddedEvent;
import org.keycloak.models.cache.infinispan.events.ClientRemovedEvent;
import org.keycloak.models.cache.infinispan.events.ClientTemplateEvent;
import org.keycloak.models.cache.infinispan.events.ClientUpdatedEvent;
import org.keycloak.models.cache.infinispan.events.GroupAddedEvent;
import org.keycloak.models.cache.infinispan.events.GroupMovedEvent;
import org.keycloak.models.cache.infinispan.events.GroupRemovedEvent;
import org.keycloak.models.cache.infinispan.events.GroupUpdatedEvent;
import org.keycloak.models.cache.infinispan.events.RealmRemovedEvent;
import org.keycloak.models.cache.infinispan.events.RealmUpdatedEvent;
import org.keycloak.models.cache.infinispan.events.RoleAddedEvent;
import org.keycloak.models.cache.infinispan.events.RoleRemovedEvent;
import org.keycloak.models.cache.infinispan.events.RoleUpdatedEvent;
import org.keycloak.models.utils.KeycloakModelUtils;
import java.util.HashMap;
@ -126,6 +141,7 @@ public class RealmCacheSession implements CacheRealmProvider {
protected Map<String, GroupAdapter> managedGroups = new HashMap<>();
protected Set<String> listInvalidations = new HashSet<>();
protected Set<String> invalidations = new HashSet<>();
protected Set<InvalidationEvent> invalidationEvents = new HashSet<>(); // Events to be sent across cluster
protected boolean clearAll;
protected final long startupRevision;
@ -150,7 +166,7 @@ public class RealmCacheSession implements CacheRealmProvider {
public void clear() {
cache.clear();
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.notify(InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS, new ClearCacheEvent());
cluster.notify(InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true);
}
@Override
@ -167,21 +183,19 @@ public class RealmCacheSession implements CacheRealmProvider {
}
@Override
public void registerRealmInvalidation(String id) {
invalidateRealm(id);
cache.realmInvalidation(id, invalidations);
}
private void invalidateRealm(String id) {
invalidations.add(id);
public void registerRealmInvalidation(String id, String name) {
cache.realmUpdated(id, name, invalidations);
RealmAdapter adapter = managedRealms.get(id);
if (adapter != null) adapter.invalidateFlag();
invalidationEvents.add(RealmUpdatedEvent.create(id, name));
}
@Override
public void registerClientInvalidation(String id) {
public void registerClientInvalidation(String id, String clientId, String realmId) {
invalidateClient(id);
cache.clientInvalidation(id, invalidations);
invalidationEvents.add(ClientUpdatedEvent.create(id, clientId, realmId));
cache.clientUpdated(realmId, id, clientId, invalidations);
}
private void invalidateClient(String id) {
@ -193,7 +207,9 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public void registerClientTemplateInvalidation(String id) {
invalidateClientTemplate(id);
cache.clientTemplateInvalidation(id, invalidations);
// Note: Adding/Removing client template is supposed to invalidate CachedRealm as well, so the list of clientTemplates is invalidated.
// But separate RealmUpdatedEvent will be sent for it. So ClientTemplateEvent don't need to take care of it.
invalidationEvents.add(ClientTemplateEvent.create(id));
}
private void invalidateClientTemplate(String id) {
@ -203,14 +219,15 @@ public class RealmCacheSession implements CacheRealmProvider {
}
@Override
public void registerRoleInvalidation(String id) {
public void registerRoleInvalidation(String id, String roleName, String roleContainerId) {
invalidateRole(id);
roleInvalidations(id);
cache.roleUpdated(roleContainerId, roleName, invalidations);
invalidationEvents.add(RoleUpdatedEvent.create(id, roleName, roleContainerId));
}
private void roleInvalidations(String roleId) {
private void roleRemovalInvalidations(String roleId, String roleName, String roleContainerId) {
Set<String> newInvalidations = new HashSet<>();
cache.roleInvalidation(roleId, newInvalidations);
cache.roleRemoval(roleId, roleName, roleContainerId, newInvalidations);
invalidations.addAll(newInvalidations);
// need to make sure that scope and group mapping clients and groups are invalidated
for (String id : newInvalidations) {
@ -229,6 +246,11 @@ public class RealmCacheSession implements CacheRealmProvider {
clientTemplate.invalidate();
continue;
}
RoleAdapter role = managedRoles.get(id);
if (role != null) {
role.invalidate();
continue;
}
}
@ -243,10 +265,26 @@ public class RealmCacheSession implements CacheRealmProvider {
if (adapter != null) adapter.invalidate();
}
private void addedRole(String roleId, String roleContainerId) {
// this is needed so that a new role that hasn't been committed isn't cached in a query
listInvalidations.add(roleContainerId);
invalidateRole(roleId);
cache.roleAdded(roleContainerId, invalidations);
invalidationEvents.add(RoleAddedEvent.create(roleId, roleContainerId));
}
@Override
public void registerGroupInvalidation(String id) {
invalidateGroup(id, null, false);
addGroupEventIfAbsent(GroupUpdatedEvent.create(id));
}
private void invalidateGroup(String id, String realmId, boolean invalidateQueries) {
invalidateGroup(id);
cache.groupInvalidation(id, invalidations);
if (invalidateQueries) {
cache.groupQueriesInvalidations(realmId, invalidations);
}
}
private void invalidateGroup(String id) {
@ -259,6 +297,8 @@ public class RealmCacheSession implements CacheRealmProvider {
for (String id : invalidations) {
cache.invalidateObject(id);
}
cache.sendInvalidationEvents(session, invalidationEvents);
}
private KeycloakTransaction getPrepareTransaction() {
@ -358,14 +398,14 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public RealmModel createRealm(String name) {
RealmModel realm = getDelegate().createRealm(name);
registerRealmInvalidation(realm.getId());
registerRealmInvalidation(realm.getId(), realm.getName());
return realm;
}
@Override
public RealmModel createRealm(String id, String name) {
RealmModel realm = getDelegate().createRealm(id, name);
registerRealmInvalidation(realm.getId());
registerRealmInvalidation(realm.getId(), realm.getName());
return realm;
}
@ -434,7 +474,7 @@ public class RealmCacheSession implements CacheRealmProvider {
}
}
public String getRealmByNameCacheKey(String name) {
static String getRealmByNameCacheKey(String name) {
return "realm.query.by.name." + name;
}
@ -457,20 +497,12 @@ public class RealmCacheSession implements CacheRealmProvider {
RealmModel realm = getRealm(id);
if (realm == null) return false;
invalidations.add(getRealmClientsQueryCacheKey(id));
invalidations.add(getRealmByNameCacheKey(realm.getName()));
cache.invalidateObject(id);
cache.realmRemoval(id, invalidations);
invalidationEvents.add(RealmRemovedEvent.create(id, realm.getName()));
cache.realmRemoval(id, realm.getName(), invalidations);
return getDelegate().removeRealm(id);
}
protected void invalidateClient(RealmModel realm, ClientModel client) {
invalidateClient(client.getId());
invalidations.add(getRealmClientsQueryCacheKey(realm.getId()));
invalidations.add(getClientByClientIdCacheKey(client.getClientId(), realm));
listInvalidations.add(realm.getId());
}
@Override
public ClientModel addClient(RealmModel realm, String clientId) {
@ -486,30 +518,32 @@ public class RealmCacheSession implements CacheRealmProvider {
private ClientModel addedClient(RealmModel realm, ClientModel client) {
logger.trace("added Client.....");
// need to invalidate realm client query cache every time as it may not be loaded on this node, but loaded on another
invalidateClient(realm, client);
cache.clientAdded(realm.getId(), client.getId(), invalidations);
// this is needed so that a new client that hasn't been committed isn't cached in a query
invalidateClient(client.getId());
// this is needed so that a client that hasn't been committed isn't cached in a query
listInvalidations.add(realm.getId());
invalidationEvents.add(ClientAddedEvent.create(client.getId(), client.getClientId(), realm.getId()));
cache.clientAdded(realm.getId(), client.getId(), client.getClientId(), invalidations);
return client;
}
private String getRealmClientsQueryCacheKey(String realm) {
static String getRealmClientsQueryCacheKey(String realm) {
return realm + REALM_CLIENTS_QUERY_SUFFIX;
}
private String getGroupsQueryCacheKey(String realm) {
static String getGroupsQueryCacheKey(String realm) {
return realm + ".groups";
}
private String getTopGroupsQueryCacheKey(String realm) {
static String getTopGroupsQueryCacheKey(String realm) {
return realm + ".top.groups";
}
private String getRolesCacheKey(String container) {
static String getRolesCacheKey(String container) {
return container + ROLES_QUERY_SUFFIX;
}
private String getRoleByNameCacheKey(String container, String name) {
static String getRoleByNameCacheKey(String container, String name) {
return container + "." + name + ROLES_QUERY_SUFFIX;
}
@ -541,6 +575,7 @@ public class RealmCacheSession implements CacheRealmProvider {
for (String id : query.getClients()) {
ClientModel client = session.realms().getClientById(id, realm);
if (client == null) {
// TODO: Handle with cluster invalidations too
invalidations.add(cacheKey);
return getDelegate().getClients(realm);
}
@ -554,12 +589,16 @@ public class RealmCacheSession implements CacheRealmProvider {
public boolean removeClient(String id, RealmModel realm) {
ClientModel client = getClientById(id, realm);
if (client == null) return false;
// need to invalidate realm client query cache every time client list is changed
invalidateClient(realm, client);
cache.clientRemoval(realm.getId(), id, invalidations);
invalidateClient(client.getId());
// this is needed so that a client that hasn't been committed isn't cached in a query
listInvalidations.add(realm.getId());
invalidationEvents.add(ClientRemovedEvent.create(client));
cache.clientRemoval(realm.getId(), id, client.getClientId(), invalidations);
for (RoleModel role : client.getRoles()) {
String roleId = role.getId();
roleInvalidations(roleId);
roleRemovalInvalidations(role.getId(), role.getName(), client.getId());
}
return getDelegate().removeClient(id, realm);
}
@ -577,11 +616,8 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public RoleModel addRealmRole(RealmModel realm, String id, String name) {
invalidations.add(getRolesCacheKey(realm.getId()));
// this is needed so that a new role that hasn't been committed isn't cached in a query
listInvalidations.add(realm.getId());
RoleModel role = getDelegate().addRealmRole(realm, name);
invalidations.add(role.getId());
addedRole(role.getId(), realm.getId());
return role;
}
@ -664,11 +700,8 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public RoleModel addClientRole(RealmModel realm, ClientModel client, String id, String name) {
invalidations.add(getRolesCacheKey(client.getId()));
// this is needed so that a new role that hasn't been committed isn't cached in a query
listInvalidations.add(client.getId());
RoleModel role = getDelegate().addClientRole(realm, client, id, name);
invalidateRole(role.getId());
addedRole(role.getId(), client.getId());
return role;
}
@ -734,10 +767,12 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public boolean removeRole(RealmModel realm, RoleModel role) {
invalidations.add(getRolesCacheKey(role.getContainer().getId()));
invalidations.add(getRoleByNameCacheKey(role.getContainer().getId(), role.getName()));
listInvalidations.add(role.getContainer().getId());
registerRoleInvalidation(role.getId());
invalidateRole(role.getId());
invalidationEvents.add(RoleRemovedEvent.create(role.getId(), role.getName(), role.getContainer().getId()));
roleRemovalInvalidations(role.getId(), role.getName(), role.getContainer().getId());
return getDelegate().removeRole(realm, role);
}
@ -797,8 +832,11 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public void moveGroup(RealmModel realm, GroupModel group, GroupModel toParent) {
registerGroupInvalidation(group.getId());
if (toParent != null) registerGroupInvalidation(toParent.getId());
invalidateGroup(group.getId(), realm.getId(), true);
if (toParent != null) invalidateGroup(group.getId(), realm.getId(), false); // Queries already invalidated
listInvalidations.add(realm.getId());
invalidationEvents.add(GroupMovedEvent.create(group, toParent, realm.getId()));
getDelegate().moveGroup(realm, group, toParent);
}
@ -876,14 +914,15 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public boolean removeGroup(RealmModel realm, GroupModel group) {
registerGroupInvalidation(group.getId());
invalidateGroup(group.getId(), realm.getId(), true);
listInvalidations.add(realm.getId());
invalidations.add(getGroupsQueryCacheKey(realm.getId()));
if (group.getParentId() == null) {
invalidations.add(getTopGroupsQueryCacheKey(realm.getId()));
} else {
registerGroupInvalidation(group.getParentId());
cache.groupQueriesInvalidations(realm.getId(), invalidations);
if (group.getParentId() != null) {
invalidateGroup(group.getParentId(), realm.getId(), false); // Queries already invalidated
}
invalidationEvents.add(GroupRemovedEvent.create(group, realm.getId()));
return getDelegate().removeGroup(realm, group);
}
@ -893,11 +932,11 @@ public class RealmCacheSession implements CacheRealmProvider {
return groupAdded(realm, group);
}
public GroupModel groupAdded(RealmModel realm, GroupModel group) {
private GroupModel groupAdded(RealmModel realm, GroupModel group) {
listInvalidations.add(realm.getId());
invalidations.add(getGroupsQueryCacheKey(realm.getId()));
invalidations.add(getTopGroupsQueryCacheKey(realm.getId()));
cache.groupQueriesInvalidations(realm.getId(), invalidations);
invalidations.add(group.getId());
invalidationEvents.add(GroupAddedEvent.create(group.getId(), realm.getId()));
return group;
}
@ -909,15 +948,32 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public void addTopLevelGroup(RealmModel realm, GroupModel subGroup) {
invalidations.add(getTopGroupsQueryCacheKey(realm.getId()));
invalidations.add(subGroup.getId());
invalidateGroup(subGroup.getId(), realm.getId(), true);
if (subGroup.getParentId() != null) {
registerGroupInvalidation(subGroup.getParentId());
invalidateGroup(subGroup.getParentId(), realm.getId(), false); // Queries already invalidated
}
addGroupEventIfAbsent(GroupMovedEvent.create(subGroup, null, realm.getId()));
getDelegate().addTopLevelGroup(realm, subGroup);
}
private void addGroupEventIfAbsent(InvalidationEvent eventToAdd) {
String groupId = eventToAdd.getId();
// Check if we have existing event with bigger priority
boolean eventAlreadyExists = invalidationEvents.stream().filter((InvalidationEvent event) -> {
return (event.getId().equals(groupId)) && (event instanceof GroupAddedEvent || event instanceof GroupMovedEvent || event instanceof GroupRemovedEvent);
}).findFirst().isPresent();
if (!eventAlreadyExists) {
invalidationEvents.add(eventToAdd);
}
}
@Override
public ClientModel getClientById(String id, RealmModel realm) {
CachedClient cached = cache.get(id, CachedClient.class);
@ -948,7 +1004,7 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public ClientModel getClientByClientId(String clientId, RealmModel realm) {
String cacheKey = getClientByClientIdCacheKey(clientId, realm);
String cacheKey = getClientByClientIdCacheKey(clientId, realm.getId());
ClientListQuery query = cache.get(cacheKey, ClientListQuery.class);
String id = null;
@ -976,8 +1032,8 @@ public class RealmCacheSession implements CacheRealmProvider {
return getClientById(id, realm);
}
public String getClientByClientIdCacheKey(String clientId, RealmModel realm) {
return realm.getId() + ".client.query.by.clientId." + clientId;
static String getClientByClientIdCacheKey(String clientId, String realmId) {
return realmId + ".client.query.by.clientId." + clientId;
}
@Override

View file

@ -47,7 +47,7 @@ public class RoleAdapter implements RoleModel {
protected void getDelegateForUpdate() {
if (updated == null) {
cacheSession.registerRoleInvalidation(cached.getId());
cacheSession.registerRoleInvalidation(cached.getId(), cached.getName(), getContainerId());
updated = cacheSession.getDelegate().getRoleById(cached.getId(), realm);
if (updated == null) throw new IllegalStateException("Not found in database");
}

View file

@ -18,40 +18,94 @@
package org.keycloak.models.cache.infinispan;
import org.infinispan.Cache;
import org.infinispan.notifications.Listener;
import org.jboss.logging.Logger;
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
import org.keycloak.models.cache.infinispan.events.UserCacheInvalidationEvent;
import org.keycloak.models.cache.infinispan.stream.InRealmPredicate;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@Listener
public class UserCacheManager extends CacheManager {
protected static final Logger logger = Logger.getLogger(UserCacheManager.class);
private static final Logger logger = Logger.getLogger(UserCacheManager.class);
protected volatile boolean enabled = true;
public UserCacheManager(Cache<String, Revisioned> cache, Cache<String, Long> revisions) {
super(cache, revisions);
}
@Override
protected Logger getLogger() {
return logger;
}
@Override
public void clear() {
cache.clear();
revisions.clear();
}
public void userUpdatedInvalidations(String userId, String username, String email, String realmId, Set<String> invalidations) {
invalidations.add(userId);
if (email != null) invalidations.add(UserCacheSession.getUserByEmailCacheKey(realmId, email));
invalidations.add(UserCacheSession.getUserByUsernameCacheKey(realmId, username));
}
// Fully invalidate user including consents and federatedIdentity links.
public void fullUserInvalidation(String userId, String username, String email, String realmId, boolean identityFederationEnabled, Map<String, String> federatedIdentities, Set<String> invalidations) {
userUpdatedInvalidations(userId, username, email, realmId, invalidations);
if (identityFederationEnabled) {
// Invalidate all keys for lookup this user by any identityProvider link
for (Map.Entry<String, String> socialLink : federatedIdentities.entrySet()) {
String fedIdentityCacheKey = UserCacheSession.getUserByFederatedIdentityCacheKey(realmId, socialLink.getKey(), socialLink.getValue());
invalidations.add(fedIdentityCacheKey);
}
// Invalidate federationLinks of user
invalidations.add(UserCacheSession.getFederatedIdentityLinksCacheKey(userId));
}
// Consents
invalidations.add(UserCacheSession.getConsentCacheKey(userId));
}
public void federatedIdentityLinkUpdatedInvalidation(String userId, Set<String> invalidations) {
invalidations.add(UserCacheSession.getFederatedIdentityLinksCacheKey(userId));
}
public void federatedIdentityLinkRemovedInvalidation(String userId, String realmId, String identityProviderId, String socialUserId, Set<String> invalidations) {
invalidations.add(UserCacheSession.getFederatedIdentityLinksCacheKey(userId));
if (identityProviderId != null) {
invalidations.add(UserCacheSession.getUserByFederatedIdentityCacheKey(realmId, identityProviderId, socialUserId));
}
}
public void consentInvalidation(String userId, Set<String> invalidations) {
invalidations.add(UserCacheSession.getConsentCacheKey(userId));
}
@Override
protected Predicate<Map.Entry<String, Revisioned>> getInvalidationPredicate(Object object) {
return null;
protected void addInvalidationsFromEvent(InvalidationEvent event, Set<String> invalidations) {
if (event instanceof UserCacheInvalidationEvent) {
((UserCacheInvalidationEvent) event).addInvalidations(this, invalidations);
}
}
public void invalidateRealmUsers(String realm, Set<String> invalidations) {
addInvalidations(InRealmPredicate.create().realm(realm), invalidations);
InRealmPredicate inRealmPredicate = getInRealmPredicate(realm);
addInvalidations(inRealmPredicate, invalidations);
}
private InRealmPredicate getInRealmPredicate(String realmId) {
return InRealmPredicate.create().realm(realmId);
}
}

View file

@ -19,6 +19,7 @@ package org.keycloak.models.cache.infinispan;
import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
import org.keycloak.common.constants.ServiceAccountConstants;
import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentModel;
@ -42,6 +43,12 @@ import org.keycloak.models.cache.infinispan.entities.CachedUser;
import org.keycloak.models.cache.infinispan.entities.CachedUserConsent;
import org.keycloak.models.cache.infinispan.entities.CachedUserConsents;
import org.keycloak.models.cache.infinispan.entities.UserListQuery;
import org.keycloak.models.cache.infinispan.events.UserCacheRealmInvalidationEvent;
import org.keycloak.models.cache.infinispan.events.UserConsentsUpdatedEvent;
import org.keycloak.models.cache.infinispan.events.UserFederationLinkRemovedEvent;
import org.keycloak.models.cache.infinispan.events.UserFederationLinkUpdatedEvent;
import org.keycloak.models.cache.infinispan.events.UserFullInvalidationEvent;
import org.keycloak.models.cache.infinispan.events.UserUpdatedEvent;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel;
@ -72,6 +79,7 @@ public class UserCacheSession implements UserCache {
protected Set<String> invalidations = new HashSet<>();
protected Set<String> realmInvalidations = new HashSet<>();
protected Set<InvalidationEvent> invalidationEvents = new HashSet<>(); // Events to be sent across cluster
protected Map<String, UserModel> managedUsers = new HashMap<>();
public UserCacheSession(UserCacheManager cache, KeycloakSession session) {
@ -85,7 +93,7 @@ public class UserCacheSession implements UserCache {
public void clear() {
cache.clear();
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.notify(InfinispanUserCacheProviderFactory.USER_CLEAR_CACHE_EVENTS, new ClearCacheEvent());
cluster.notify(InfinispanUserCacheProviderFactory.USER_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true);
}
public UserProvider getDelegate() {
@ -97,10 +105,8 @@ public class UserCacheSession implements UserCache {
}
public void registerUserInvalidation(RealmModel realm,CachedUser user) {
invalidations.add(user.getId());
if (user.getEmail() != null) invalidations.add(getUserByEmailCacheKey(realm.getId(), user.getEmail()));
invalidations.add(getUserByUsernameCacheKey(realm.getId(), user.getUsername()));
if (realm.isIdentityFederationEnabled()) invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
cache.userUpdatedInvalidations(user.getId(), user.getUsername(), user.getEmail(), user.getRealm(), invalidations);
invalidationEvents.add(UserUpdatedEvent.create(user.getId(), user.getUsername(), user.getEmail(), user.getRealm()));
}
@Override
@ -108,16 +114,14 @@ public class UserCacheSession implements UserCache {
if (user instanceof CachedUserModel) {
((CachedUserModel)user).invalidate();
} else {
invalidations.add(user.getId());
if (user.getEmail() != null) invalidations.add(getUserByEmailCacheKey(realm.getId(), user.getEmail()));
invalidations.add(getUserByUsernameCacheKey(realm.getId(), user.getUsername()));
if (realm.isIdentityFederationEnabled()) invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
cache.userUpdatedInvalidations(user.getId(), user.getUsername(), user.getEmail(), realm.getId(), invalidations);
invalidationEvents.add(UserUpdatedEvent.create(user.getId(), user.getUsername(), user.getEmail(), realm.getId()));
}
}
@Override
public void evict(RealmModel realm) {
realmInvalidations.add(realm.getId());
addRealmInvalidation(realm.getId());
}
protected void runInvalidations() {
@ -127,6 +131,8 @@ public class UserCacheSession implements UserCache {
for (String invalidation : invalidations) {
cache.invalidateObject(invalidation);
}
cache.sendInvalidationEvents(session, invalidationEvents);
}
private KeycloakTransaction getTransaction() {
@ -201,19 +207,23 @@ public class UserCacheSession implements UserCache {
return adapter;
}
public String getUserByUsernameCacheKey(String realmId, String username) {
static String getUserByUsernameCacheKey(String realmId, String username) {
return realmId + ".username." + username;
}
public String getUserByEmailCacheKey(String realmId, String email) {
static String getUserByEmailCacheKey(String realmId, String email) {
return realmId + ".email." + email;
}
public String getUserByFederatedIdentityCacheKey(String realmId, FederatedIdentityModel socialLink) {
return realmId + ".idp." + socialLink.getIdentityProvider() + "." + socialLink.getUserId();
private static String getUserByFederatedIdentityCacheKey(String realmId, FederatedIdentityModel socialLink) {
return getUserByFederatedIdentityCacheKey(realmId, socialLink.getIdentityProvider(), socialLink.getUserId());
}
public String getFederatedIdentityLinksCacheKey(String userId) {
static String getUserByFederatedIdentityCacheKey(String realmId, String identityProvider, String socialUserId) {
return realmId + ".idp." + identityProvider + "." + socialUserId;
}
static String getFederatedIdentityLinksCacheKey(String userId) {
return userId + ".idplinks";
}
@ -655,27 +665,32 @@ public class UserCacheSession implements UserCache {
@Override
public void updateConsent(RealmModel realm, String userId, UserConsentModel consent) {
invalidations.add(getConsentCacheKey(userId));
invalidateConsent(userId);
getDelegate().updateConsent(realm, userId, consent);
}
@Override
public boolean revokeConsentForClient(RealmModel realm, String userId, String clientInternalId) {
invalidations.add(getConsentCacheKey(userId));
invalidateConsent(userId);
return getDelegate().revokeConsentForClient(realm, userId, clientInternalId);
}
public String getConsentCacheKey(String userId) {
static String getConsentCacheKey(String userId) {
return userId + ".consents";
}
@Override
public void addConsent(RealmModel realm, String userId, UserConsentModel consent) {
invalidations.add(getConsentCacheKey(userId));
invalidateConsent(userId);
getDelegate().addConsent(realm, userId, consent);
}
private void invalidateConsent(String userId) {
cache.consentInvalidation(userId, invalidations);
invalidationEvents.add(UserConsentsUpdatedEvent.create(userId));
}
@Override
public UserConsentModel getConsentByClient(RealmModel realm, String userId, String clientId) {
logger.tracev("getConsentByClient: {0}", userId);
@ -754,7 +769,7 @@ public class UserCacheSession implements UserCache {
public UserModel addUser(RealmModel realm, String id, String username, boolean addDefaultRoles, boolean addDefaultRequiredActions) {
UserModel user = getDelegate().addUser(realm, id, username, addDefaultRoles, addDefaultRoles);
// just in case the transaction is rolled back you need to invalidate the user and all cache queries for that user
invalidateUser(realm, user);
fullyInvalidateUser(realm, user);
managedUsers.put(user.getId(), user);
return user;
}
@ -763,94 +778,89 @@ public class UserCacheSession implements UserCache {
public UserModel addUser(RealmModel realm, String username) {
UserModel user = getDelegate().addUser(realm, username);
// just in case the transaction is rolled back you need to invalidate the user and all cache queries for that user
invalidateUser(realm, user);
fullyInvalidateUser(realm, user);
managedUsers.put(user.getId(), user);
return user;
}
protected void invalidateUser(RealmModel realm, UserModel user) {
// just in case the transaction is rolled back you need to invalidate the user and all cache queries for that user
// just in case the transaction is rolled back you need to invalidate the user and all cache queries for that user
protected void fullyInvalidateUser(RealmModel realm, UserModel user) {
Set<FederatedIdentityModel> federatedIdentities = realm.isIdentityFederationEnabled() ? getFederatedIdentities(user, realm) : null;
if (realm.isIdentityFederationEnabled()) {
// Invalidate all keys for lookup this user by any identityProvider link
Set<FederatedIdentityModel> federatedIdentities = getFederatedIdentities(user, realm);
for (FederatedIdentityModel socialLink : federatedIdentities) {
String fedIdentityCacheKey = getUserByFederatedIdentityCacheKey(realm.getId(), socialLink);
invalidations.add(fedIdentityCacheKey);
}
UserFullInvalidationEvent event = UserFullInvalidationEvent.create(user.getId(), user.getUsername(), user.getEmail(), realm.getId(), realm.isIdentityFederationEnabled(), federatedIdentities);
// Invalidate federationLinks of user
invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
}
invalidations.add(user.getId());
if (user.getEmail() != null) invalidations.add(getUserByEmailCacheKey(realm.getId(), user.getEmail()));
invalidations.add(getUserByUsernameCacheKey(realm.getId(), user.getUsername()));
cache.fullUserInvalidation(user.getId(), user.getUsername(), user.getEmail(), realm.getId(), realm.isIdentityFederationEnabled(), event.getFederatedIdentities(), invalidations);
invalidationEvents.add(event);
}
@Override
public boolean removeUser(RealmModel realm, UserModel user) {
invalidateUser(realm, user);
fullyInvalidateUser(realm, user);
return getDelegate().removeUser(realm, user);
}
@Override
public void addFederatedIdentity(RealmModel realm, UserModel user, FederatedIdentityModel socialLink) {
invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
invalidateFederationLink(user.getId());
getDelegate().addFederatedIdentity(realm, user, socialLink);
}
@Override
public void updateFederatedIdentity(RealmModel realm, UserModel federatedUser, FederatedIdentityModel federatedIdentityModel) {
invalidations.add(getFederatedIdentityLinksCacheKey(federatedUser.getId()));
invalidateFederationLink(federatedUser.getId());
getDelegate().updateFederatedIdentity(realm, federatedUser, federatedIdentityModel);
}
private void invalidateFederationLink(String userId) {
cache.federatedIdentityLinkUpdatedInvalidation(userId, invalidations);
invalidationEvents.add(UserFederationLinkUpdatedEvent.create(userId));
}
@Override
public boolean removeFederatedIdentity(RealmModel realm, UserModel user, String socialProvider) {
// Needs to invalidate both directions
FederatedIdentityModel socialLink = getFederatedIdentity(user, socialProvider, realm);
invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
if (socialLink != null) {
invalidations.add(getUserByFederatedIdentityCacheKey(realm.getId(), socialLink));
}
UserFederationLinkRemovedEvent event = UserFederationLinkRemovedEvent.create(user.getId(), realm.getId(), socialLink);
cache.federatedIdentityLinkRemovedInvalidation(user.getId(), realm.getId(), event.getIdentityProviderId(), event.getSocialUserId(), invalidations);
invalidationEvents.add(event);
return getDelegate().removeFederatedIdentity(realm, user, socialProvider);
}
@Override
public void grantToAllUsers(RealmModel realm, RoleModel role) {
realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().grantToAllUsers(realm, role);
}
@Override
public void preRemove(RealmModel realm) {
realmInvalidations.add(realm.getId());
addRealmInvalidation(realm.getId());
getDelegate().preRemove(realm);
}
@Override
public void preRemove(RealmModel realm, RoleModel role) {
realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().preRemove(realm, role);
}
@Override
public void preRemove(RealmModel realm, GroupModel group) {
realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().preRemove(realm, group);
}
@Override
public void preRemove(RealmModel realm, UserFederationProviderModel link) {
realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().preRemove(realm, link);
}
@Override
public void preRemove(RealmModel realm, ClientModel client) {
realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().preRemove(realm, client);
}
@ -862,9 +872,14 @@ public class UserCacheSession implements UserCache {
@Override
public void preRemove(RealmModel realm, ComponentModel component) {
if (!component.getProviderType().equals(UserStorageProvider.class.getName())) return;
realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().preRemove(realm, component);
}
private void addRealmInvalidation(String realmId) {
realmInvalidations.add(realmId);
invalidationEvents.add(UserCacheRealmInvalidationEvent.create(realmId));
}
}

View file

@ -135,7 +135,6 @@ public class CachedRealm extends AbstractExtendableRevisioned {
}
protected List<String> defaultGroups = new LinkedList<String>();
protected Set<String> groups = new HashSet<String>();
protected List<String> clientTemplates= new LinkedList<>();
protected boolean internationalizationEnabled;
protected Set<String> supportedLocales;
@ -237,9 +236,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
executionsById.put(execution.getId(), execution);
}
}
for (GroupModel group : model.getGroups()) {
groups.add(group.getId());
}
for (AuthenticatorConfigModel authenticator : model.getAuthenticatorConfigs()) {
authenticatorConfigs.put(authenticator.getId(), authenticator);
}
@ -541,10 +538,6 @@ public class CachedRealm extends AbstractExtendableRevisioned {
return clientAuthenticationFlow;
}
public Set<String> getGroups() {
return groups;
}
public List<String> getDefaultGroups() {
return defaultGroups;
}

View file

@ -1,11 +0,0 @@
package org.keycloak.models.cache.infinispan.entities;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface ClientTemplateQuery extends InRealm {
Set<String> getTemplates();
}

View file

@ -1,8 +1,6 @@
package org.keycloak.models.cache.infinispan;
package org.keycloak.models.cache.infinispan.entities;
import org.keycloak.models.RealmModel;
import org.keycloak.models.cache.infinispan.entities.AbstractRevisioned;
import org.keycloak.models.cache.infinispan.entities.GroupQuery;
import java.util.Set;

View file

@ -59,7 +59,8 @@ public class RoleListQuery extends AbstractRevisioned implements RoleQuery, InCl
public String toString() {
return "RoleListQuery{" +
"id='" + getId() + "'" +
"realmName='" + realmName + '\'' +
", realmName='" + realmName + '\'' +
", clientUuid='" + client + '\'' +
'}';
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright 2016 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.models.cache.infinispan.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientAddedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String clientUuid;
private String clientId;
private String realmId;
public static ClientAddedEvent create(String clientUuid, String clientId, String realmId) {
ClientAddedEvent event = new ClientAddedEvent();
event.clientUuid = clientUuid;
event.clientId = clientId;
event.realmId = realmId;
return event;
}
@Override
public String getId() {
return clientUuid;
}
@Override
public String toString() {
return String.format("ClientAddedEvent [ realmId=%s, clientUuid=%s, clientId=%s ]", realmId, clientUuid, clientId);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.clientAdded(realmId, clientUuid, clientId, invalidations);
}
}

View file

@ -0,0 +1,74 @@
/*
* Copyright 2016 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.models.cache.infinispan.events;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientRemovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String clientUuid;
private String clientId;
private String realmId;
// roleId -> roleName
private Map<String, String> clientRoles;
public static ClientRemovedEvent create(ClientModel client) {
ClientRemovedEvent event = new ClientRemovedEvent();
event.realmId = client.getRealm().getId();
event.clientUuid = client.getId();
event.clientId = client.getClientId();
event.clientRoles = new HashMap<>();
for (RoleModel clientRole : client.getRoles()) {
event.clientRoles.put(clientRole.getId(), clientRole.getName());
}
return event;
}
@Override
public String getId() {
return clientUuid;
}
@Override
public String toString() {
return String.format("ClientRemovedEvent [ realmId=%s, clientUuid=%s, clientId=%s, clientRoleIds=%s ]", realmId, clientUuid, clientId, clientRoles);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.clientRemoval(realmId, clientUuid, clientId, invalidations);
// Separate iteration for all client roles to invalidate records dependent on them
for (Map.Entry<String, String> clientRole : clientRoles.entrySet()) {
String roleId = clientRole.getKey();
String roleName = clientRole.getValue();
realmCache.roleRemoval(roleId, roleName, clientUuid, invalidations);
}
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright 2016 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.models.cache.infinispan.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientTemplateEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String clientTemplateId;
public static ClientTemplateEvent create(String clientTemplateId) {
ClientTemplateEvent event = new ClientTemplateEvent();
event.clientTemplateId = clientTemplateId;
return event;
}
@Override
public String getId() {
return clientTemplateId;
}
@Override
public String toString() {
return "ClientTemplateEvent [ " + clientTemplateId + " ]";
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
// Nothing. ID was already invalidated
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright 2016 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.models.cache.infinispan.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientUpdatedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String clientUuid;
private String clientId;
private String realmId;
public static ClientUpdatedEvent create(String clientUuid, String clientId, String realmId) {
ClientUpdatedEvent event = new ClientUpdatedEvent();
event.clientUuid = clientUuid;
event.clientId = clientId;
event.realmId = realmId;
return event;
}
@Override
public String getId() {
return clientUuid;
}
@Override
public String toString() {
return String.format("ClientUpdatedEvent [ realmId=%s, clientUuid=%s, clientId=%s ]", realmId, clientUuid, clientId);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.clientUpdated(realmId, clientUuid, clientId, invalidations);
}
}

View file

@ -0,0 +1,54 @@
/*
* Copyright 2016 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.models.cache.infinispan.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class GroupAddedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String groupId;
private String realmId;
public static GroupAddedEvent create(String groupId, String realmId) {
GroupAddedEvent event = new GroupAddedEvent();
event.realmId = realmId;
event.groupId = groupId;
return event;
}
@Override
public String getId() {
return groupId;
}
@Override
public String toString() {
return String.format("GroupAddedEvent [ realmId=%s, groupId=%s ]", realmId, groupId);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.groupQueriesInvalidations(realmId, invalidations);
}
}

View file

@ -0,0 +1,64 @@
/*
* Copyright 2016 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.models.cache.infinispan.events;
import java.util.Set;
import org.keycloak.models.GroupModel;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class GroupMovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String groupId;
private String newParentId; // null if moving to top-level
private String oldParentId; // null if moving from top-level
private String realmId;
public static GroupMovedEvent create(GroupModel group, GroupModel toParent, String realmId) {
GroupMovedEvent event = new GroupMovedEvent();
event.realmId = realmId;
event.groupId = group.getId();
event.oldParentId = group.getParentId();
event.newParentId = toParent==null ? null : toParent.getId();
return event;
}
@Override
public String getId() {
return groupId;
}
@Override
public String toString() {
return String.format("GroupMovedEvent [ realmId=%s, groupId=%s, newParentId=%s, oldParentId=%s ]", realmId, groupId, newParentId, oldParentId);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.groupQueriesInvalidations(realmId, invalidations);
if (newParentId != null) {
invalidations.add(newParentId);
}
if (oldParentId != null) {
invalidations.add(oldParentId);
}
}
}

View file

@ -0,0 +1,59 @@
/*
* Copyright 2016 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.models.cache.infinispan.events;
import java.util.Set;
import org.keycloak.models.GroupModel;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class GroupRemovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String groupId;
private String parentId;
private String realmId;
public static GroupRemovedEvent create(GroupModel group, String realmId) {
GroupRemovedEvent event = new GroupRemovedEvent();
event.realmId = realmId;
event.groupId = group.getId();
event.parentId = group.getParentId();
return event;
}
@Override
public String getId() {
return groupId;
}
@Override
public String toString() {
return String.format("GroupRemovedEvent [ realmId=%s, groupId=%s, parentId=%s ]", realmId, groupId, parentId);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.groupQueriesInvalidations(realmId, invalidations);
if (parentId != null) {
invalidations.add(parentId);
}
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright 2016 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.models.cache.infinispan.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class GroupUpdatedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String groupId;
public static GroupUpdatedEvent create(String groupId) {
GroupUpdatedEvent event = new GroupUpdatedEvent();
event.groupId = groupId;
return event;
}
@Override
public String getId() {
return groupId;
}
@Override
public String toString() {
return "GroupUpdatedEvent [ " + groupId + " ]";
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
// Nothing. ID already invalidated
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright 2016 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.models.cache.infinispan.events;
import org.keycloak.cluster.ClusterEvent;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public abstract class InvalidationEvent implements ClusterEvent {
public abstract String getId();
@Override
public int hashCode() {
return getClass().hashCode() * 13 + getId().hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null) return false;
if (!obj.getClass().equals(this.getClass())) return false;
InvalidationEvent that = (InvalidationEvent) obj;
if (!that.getId().equals(getId())) return false;
return true;
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright 2016 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.models.cache.infinispan.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface RealmCacheInvalidationEvent {
void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations);
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2016 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.models.cache.infinispan.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class RealmRemovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String realmId;
private String realmName;
public static RealmRemovedEvent create(String realmId, String realmName) {
RealmRemovedEvent event = new RealmRemovedEvent();
event.realmId = realmId;
event.realmName = realmName;
return event;
}
@Override
public String getId() {
return realmId;
}
@Override
public String toString() {
return String.format("RealmRemovedEvent [ realmId=%s, realmName=%s ]", realmId, realmName);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.realmRemoval(realmId, realmName, invalidations);
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2016 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.models.cache.infinispan.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class RealmUpdatedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String realmId;
private String realmName;
public static RealmUpdatedEvent create(String realmId, String realmName) {
RealmUpdatedEvent event = new RealmUpdatedEvent();
event.realmId = realmId;
event.realmName = realmName;
return event;
}
@Override
public String getId() {
return realmId;
}
@Override
public String toString() {
return String.format("RealmUpdatedEvent [ realmId=%s, realmName=%s ]", realmId, realmName);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.realmUpdated(realmId, realmName, invalidations);
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2016 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.models.cache.infinispan.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class RoleAddedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String roleId;
private String containerId;
public static RoleAddedEvent create(String roleId, String containerId) {
RoleAddedEvent event = new RoleAddedEvent();
event.roleId = roleId;
event.containerId = containerId;
return event;
}
@Override
public String getId() {
return roleId;
}
@Override
public String toString() {
return String.format("RoleAddedEvent [ roleId=%s, containerId=%s ]", roleId, containerId);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.roleAdded(containerId, invalidations);
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright 2016 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.models.cache.infinispan.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class RoleRemovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String roleId;
private String roleName;
private String containerId;
public static RoleRemovedEvent create(String roleId, String roleName, String containerId) {
RoleRemovedEvent event = new RoleRemovedEvent();
event.roleId = roleId;
event.roleName = roleName;
event.containerId = containerId;
return event;
}
@Override
public String getId() {
return roleId;
}
@Override
public String toString() {
return String.format("RoleRemovedEvent [ roleId=%s, containerId=%s ]", roleId, containerId);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.roleRemoval(roleId, roleName, containerId, invalidations);
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright 2016 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.models.cache.infinispan.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class RoleUpdatedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String roleId;
private String roleName;
private String containerId;
public static RoleUpdatedEvent create(String roleId, String roleName, String containerId) {
RoleUpdatedEvent event = new RoleUpdatedEvent();
event.roleId = roleId;
event.roleName = roleName;
event.containerId = containerId;
return event;
}
@Override
public String getId() {
return roleId;
}
@Override
public String toString() {
return String.format("RoleUpdatedEvent [ roleId=%s, roleName=%s, containerId=%s ]", roleId, roleName, containerId);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.roleUpdated(containerId, roleName, invalidations);
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright 2016 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.models.cache.infinispan.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.UserCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface UserCacheInvalidationEvent {
void addInvalidations(UserCacheManager userCache, Set<String> invalidations);
}

View file

@ -0,0 +1,51 @@
/*
* Copyright 2016 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.models.cache.infinispan.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.UserCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class UserCacheRealmInvalidationEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
private String realmId;
public static UserCacheRealmInvalidationEvent create(String realmId) {
UserCacheRealmInvalidationEvent event = new UserCacheRealmInvalidationEvent();
event.realmId = realmId;
return event;
}
@Override
public String getId() {
return realmId; // Just a placeholder
}
@Override
public String toString() {
return String.format("UserCacheRealmInvalidationEvent [ realmId=%s ]", realmId);
}
@Override
public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
userCache.invalidateRealmUsers(realmId, invalidations);
}
}

View file

@ -0,0 +1,51 @@
/*
* Copyright 2016 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.models.cache.infinispan.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.UserCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class UserConsentsUpdatedEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
private String userId;
public static UserConsentsUpdatedEvent create(String userId) {
UserConsentsUpdatedEvent event = new UserConsentsUpdatedEvent();
event.userId = userId;
return event;
}
@Override
public String getId() {
return userId;
}
@Override
public String toString() {
return String.format("UserConsentsUpdatedEvent [ userId=%s ]", userId);
}
@Override
public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
userCache.consentInvalidation(userId, invalidations);
}
}

View file

@ -0,0 +1,72 @@
/*
* Copyright 2016 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.models.cache.infinispan.events;
import java.util.Set;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.cache.infinispan.UserCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class UserFederationLinkRemovedEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
private String userId;
private String realmId;
private String identityProviderId;
private String socialUserId;
public static UserFederationLinkRemovedEvent create(String userId, String realmId, FederatedIdentityModel socialLink) {
UserFederationLinkRemovedEvent event = new UserFederationLinkRemovedEvent();
event.userId = userId;
event.realmId = realmId;
if (socialLink != null) {
event.identityProviderId = socialLink.getIdentityProvider();
event.socialUserId = socialLink.getUserId();
}
return event;
}
@Override
public String getId() {
return userId;
}
public String getRealmId() {
return realmId;
}
public String getIdentityProviderId() {
return identityProviderId;
}
public String getSocialUserId() {
return socialUserId;
}
@Override
public String toString() {
return String.format("UserFederationLinkRemovedEvent [ userId=%s, identityProviderId=%s, socialUserId=%s ]", userId, identityProviderId, socialUserId);
}
@Override
public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
userCache.federatedIdentityLinkRemovedInvalidation(userId, realmId, identityProviderId, socialUserId, invalidations);
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright 2016 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.models.cache.infinispan.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.UserCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class UserFederationLinkUpdatedEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
private String userId;
public static UserFederationLinkUpdatedEvent create(String userId) {
UserFederationLinkUpdatedEvent event = new UserFederationLinkUpdatedEvent();
event.userId = userId;
return event;
}
@Override
public String getId() {
return userId;
}
@Override
public String toString() {
return String.format("UserFederationLinkUpdatedEvent [ userId=%s ]", userId);
}
@Override
public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
userCache.federatedIdentityLinkUpdatedInvalidation(userId, invalidations);
}
}

View file

@ -0,0 +1,78 @@
/*
* Copyright 2016 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.models.cache.infinispan.events;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.cache.infinispan.UserCacheManager;
/**
* Used when user added/removed
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class UserFullInvalidationEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
private String userId;
private String username;
private String email;
private String realmId;
private boolean identityFederationEnabled;
private Map<String, String> federatedIdentities;
public static UserFullInvalidationEvent create(String userId, String username, String email, String realmId, boolean identityFederationEnabled, Collection<FederatedIdentityModel> federatedIdentities) {
UserFullInvalidationEvent event = new UserFullInvalidationEvent();
event.userId = userId;
event.username = username;
event.email = email;
event.realmId = realmId;
event.identityFederationEnabled = identityFederationEnabled;
if (identityFederationEnabled) {
event.federatedIdentities = new HashMap<>();
for (FederatedIdentityModel socialLink : federatedIdentities) {
event.federatedIdentities.put(socialLink.getIdentityProvider(), socialLink.getUserId());
}
}
return event;
}
@Override
public String getId() {
return userId;
}
public Map<String, String> getFederatedIdentities() {
return federatedIdentities;
}
@Override
public String toString() {
return String.format("UserFullInvalidationEvent [ userId=%s, username=%s, email=%s ]", userId, username, email);
}
@Override
public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
userCache.fullUserInvalidation(userId, username, email, realmId, identityFederationEnabled, federatedIdentities, invalidations);
}
}

View file

@ -0,0 +1,57 @@
/*
* Copyright 2016 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.models.cache.infinispan.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.UserCacheManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class UserUpdatedEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
private String userId;
private String username;
private String email;
private String realmId;
public static UserUpdatedEvent create(String userId, String username, String email, String realmId) {
UserUpdatedEvent event = new UserUpdatedEvent();
event.userId = userId;
event.username = username;
event.email = email;
event.realmId = realmId;
return event;
}
@Override
public String getId() {
return userId;
}
@Override
public String toString() {
return String.format("UserUpdatedEvent [ userId=%s, username=%s, email=%s ]", userId, username, email);
}
@Override
public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
userCache.userUpdatedInvalidations(userId, username, email, realmId, invalidations);
}
}

View file

@ -1,48 +0,0 @@
package org.keycloak.models.cache.infinispan.stream;
import org.jboss.logging.Logger;
import org.keycloak.models.cache.infinispan.entities.ClientQuery;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
import java.io.Serializable;
import java.util.Map;
import java.util.function.Predicate;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ClientQueryPredicate implements Predicate<Map.Entry<String, Revisioned>>, Serializable {
protected static final Logger logger = Logger.getLogger(ClientQueryPredicate.class);
private String client;
private String inRealm;
public static ClientQueryPredicate create() {
return new ClientQueryPredicate();
}
public ClientQueryPredicate client(String client) {
this.client = client;
return this;
}
public ClientQueryPredicate inRealm(String inRealm) {
this.inRealm = inRealm;
return this;
}
@Override
public boolean test(Map.Entry<String, Revisioned> entry) {
Object value = entry.getValue();
if (value == null) return false;
if (!(value instanceof ClientQuery)) return false;
ClientQuery query = (ClientQuery)value;
if (client != null && !query.getClients().contains(client)) return false;
if (inRealm != null && !query.getRealm().equals(inRealm)) return false;
return true;
}
}

View file

@ -1,40 +0,0 @@
package org.keycloak.models.cache.infinispan.stream;
import org.keycloak.models.cache.infinispan.entities.ClientTemplateQuery;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
import java.io.Serializable;
import java.util.Map;
import java.util.function.Predicate;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ClientTemplateQueryPredicate implements Predicate<Map.Entry<String, Revisioned>>, Serializable {
private String template;
public static ClientTemplateQueryPredicate create() {
return new ClientTemplateQueryPredicate();
}
public ClientTemplateQueryPredicate template(String template) {
this.template = template;
return this;
}
@Override
public boolean test(Map.Entry<String, Revisioned> entry) {
Object value = entry.getValue();
if (value == null) return false;
if (!(value instanceof ClientTemplateQuery)) return false;
ClientTemplateQuery query = (ClientTemplateQuery)value;
return query.getTemplates().contains(template);
}
}

View file

@ -1,40 +0,0 @@
package org.keycloak.models.cache.infinispan.stream;
import org.keycloak.models.cache.infinispan.entities.GroupQuery;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
import java.io.Serializable;
import java.util.Map;
import java.util.function.Predicate;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class GroupQueryPredicate implements Predicate<Map.Entry<String, Revisioned>>, Serializable {
private String group;
public static GroupQueryPredicate create() {
return new GroupQueryPredicate();
}
public GroupQueryPredicate group(String group) {
this.group = group;
return this;
}
@Override
public boolean test(Map.Entry<String, Revisioned> entry) {
Object value = entry.getValue();
if (value == null) return false;
if (!(value instanceof GroupQuery)) return false;
GroupQuery query = (GroupQuery)value;
return query.getGroups().contains(group);
}
}

View file

@ -1,40 +0,0 @@
package org.keycloak.models.cache.infinispan.stream;
import org.keycloak.models.cache.infinispan.entities.RealmQuery;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
import java.io.Serializable;
import java.util.Map;
import java.util.function.Predicate;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class RealmQueryPredicate implements Predicate<Map.Entry<String, Revisioned>>, Serializable {
private String realm;
public static RealmQueryPredicate create() {
return new RealmQueryPredicate();
}
public RealmQueryPredicate realm(String realm) {
this.realm = realm;
return this;
}
@Override
public boolean test(Map.Entry<String, Revisioned> entry) {
Object value = entry.getValue();
if (value == null) return false;
if (!(value instanceof RealmQuery)) return false;
RealmQuery query = (RealmQuery)value;
return query.getRealms().contains(realm);
}
}

View file

@ -1,40 +0,0 @@
package org.keycloak.models.cache.infinispan.stream;
import org.keycloak.models.cache.infinispan.entities.Revisioned;
import org.keycloak.models.cache.infinispan.entities.RoleQuery;
import java.io.Serializable;
import java.util.Map;
import java.util.function.Predicate;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class RoleQueryPredicate implements Predicate<Map.Entry<String, Revisioned>>, Serializable {
private String role;
public static RoleQueryPredicate create() {
return new RoleQueryPredicate();
}
public RoleQueryPredicate role(String role) {
this.role = role;
return this;
}
@Override
public boolean test(Map.Entry<String, Revisioned> entry) {
Object value = entry.getValue();
if (value == null) return false;
if (!(value instanceof RoleQuery)) return false;
RoleQuery query = (RoleQuery)value;
return query.getRoles().contains(role);
}
}

View file

@ -431,8 +431,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
}
}
@Override
public void onUserRemoved(RealmModel realm, UserModel user) {
protected void onUserRemoved(RealmModel realm, UserModel user) {
removeUserSessions(realm, user, true);
removeUserSessions(realm, user, false);

View file

@ -24,6 +24,7 @@ import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.UserSessionProviderFactory;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
@ -45,7 +46,7 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
private Config.Scope config;
@Override
public UserSessionProvider create(KeycloakSession session) {
public InfinispanUserSessionProvider create(KeycloakSession session) {
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
Cache<String, SessionEntity> cache = connections.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
Cache<String, SessionEntity> offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
@ -73,6 +74,11 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
public void onEvent(ProviderEvent event) {
if (event instanceof PostMigrationEvent) {
loadPersistentSessions(factory, maxErrors, sessionsPerSegment);
} else if (event instanceof UserModel.UserRemovedEvent) {
UserModel.UserRemovedEvent userRemovedEvent = (UserModel.UserRemovedEvent) event;
InfinispanUserSessionProvider provider = (InfinispanUserSessionProvider) userRemovedEvent.getKeycloakSession().getProvider(UserSessionProvider.class, getId());
provider.onUserRemoved(userRemovedEvent.getRealm(), userRemovedEvent.getUser());
}
}
});

View file

@ -92,13 +92,13 @@ public class InfinispanUserSessionInitializer {
private boolean isFinished() {
InitializerState state = (InitializerState) workCache.get(stateKey);
InitializerState state = getStateFromCache();
return state != null && state.isFinished();
}
private InitializerState getOrCreateInitializerState() {
InitializerState state = (InitializerState) workCache.get(stateKey);
InitializerState state = getStateFromCache();
if (state == null) {
final int[] count = new int[1];
@ -128,6 +128,12 @@ public class InfinispanUserSessionInitializer {
}
private InitializerState getStateFromCache() {
// TODO: We ignore cacheStore for now, so that in Cross-DC scenario (with RemoteStore enabled) is the remoteStore ignored. This means that every DC needs to load offline sessions separately.
return (InitializerState) workCache.getAdvancedCache()
.withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD)
.get(stateKey);
}
private void saveStateToCache(final InitializerState state) {
@ -138,8 +144,9 @@ public class InfinispanUserSessionInitializer {
public void run() {
// Save this synchronously to ensure all nodes read correct state
// TODO: We ignore cacheStore for now, so that in Cross-DC scenario (with RemoteStore enabled) is the remoteStore ignored. This means that every DC needs to load offline sessions separately.
InfinispanUserSessionInitializer.this.workCache.getAdvancedCache().
withFlags(Flag.IGNORE_RETURN_VALUES, Flag.FORCE_SYNCHRONOUS)
withFlags(Flag.IGNORE_RETURN_VALUES, Flag.FORCE_SYNCHRONOUS, Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD)
.put(stateKey, state);
}

View file

@ -0,0 +1,231 @@
/*
* Copyright 2016 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.cluster.infinispan;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import org.infinispan.Cache;
import org.infinispan.client.hotrod.Flag;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated;
import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified;
import org.infinispan.client.hotrod.annotation.ClientListener;
import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent;
import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.global.GlobalConfigurationBuilder;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.persistence.manager.PersistenceManager;
import org.infinispan.persistence.remote.RemoteStore;
import org.infinispan.persistence.remote.configuration.ExhaustedAction;
import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
import org.junit.Ignore;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
/**
* Test concurrency for remoteStore (backed by HotRod RemoteCaches) against external JDG
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@Ignore
public class ConcurrencyJDGRemoteCacheTest {
private static Map<String, EntryInfo> state = new HashMap<>();
public static void main(String[] args) throws Exception {
// Init map somehow
for (int i=0 ; i<100 ; i++) {
String key = "key-" + i;
state.put(key, new EntryInfo());
}
// Create caches, listeners and finally worker threads
Worker worker1 = createWorker(1);
Worker worker2 = createWorker(2);
// Start and join workers
worker1.start();
worker2.start();
worker1.join();
worker2.join();
// Output
for (Map.Entry<String, EntryInfo> entry : state.entrySet()) {
System.out.println(entry.getKey() + ":::" + entry.getValue());
worker1.cache.remove(entry.getKey());
}
// Finish JVM
worker1.cache.getCacheManager().stop();
worker2.cache.getCacheManager().stop();
}
private static Worker createWorker(int threadId) {
EmbeddedCacheManager manager = createManager(threadId);
Cache<String, Integer> cache = manager.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
System.out.println("Retrieved cache: " + threadId);
RemoteStore remoteStore = cache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class).iterator().next();
HotRodListener listener = new HotRodListener();
remoteStore.getRemoteCache().addClientListener(listener);
return new Worker(cache, threadId);
}
private static EmbeddedCacheManager createManager(int threadId) {
System.setProperty("java.net.preferIPv4Stack", "true");
System.setProperty("jgroups.tcp.port", "53715");
GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
boolean clustered = false;
boolean async = false;
boolean allowDuplicateJMXDomains = true;
if (clustered) {
gcb = gcb.clusteredDefault();
gcb.transport().clusterName("test-clustering");
}
gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
EmbeddedCacheManager cacheManager = new DefaultCacheManager(gcb.build());
Configuration invalidationCacheConfiguration = getCacheBackedByRemoteStore(threadId);
cacheManager.defineConfiguration(InfinispanConnectionProvider.WORK_CACHE_NAME, invalidationCacheConfiguration);
return cacheManager;
}
private static Configuration getCacheBackedByRemoteStore(int threadId) {
ConfigurationBuilder cacheConfigBuilder = new ConfigurationBuilder();
// int port = threadId==1 ? 11222 : 11322;
int port = 11222;
return cacheConfigBuilder.persistence().addStore(RemoteStoreConfigurationBuilder.class)
.fetchPersistentState(false)
.ignoreModifications(false)
.purgeOnStartup(false)
.preload(false)
.shared(true)
.remoteCacheName(InfinispanConnectionProvider.WORK_CACHE_NAME)
.rawValues(true)
.forceReturnValues(false)
.addServer()
.host("localhost")
.port(port)
.connectionPool()
.maxActive(20)
.exhaustedAction(ExhaustedAction.CREATE_NEW)
.async()
. enabled(false).build();
}
@ClientListener
public static class HotRodListener {
//private AtomicInteger listenerCount = new AtomicInteger(0);
@ClientCacheEntryCreated
public void created(ClientCacheEntryCreatedEvent event) {
String cacheKey = (String) event.getKey();
state.get(cacheKey).successfulListenerWrites.incrementAndGet();
}
@ClientCacheEntryModified
public void updated(ClientCacheEntryModifiedEvent event) {
String cacheKey = (String) event.getKey();
state.get(cacheKey).successfulListenerWrites.incrementAndGet();
}
}
private static class Worker extends Thread {
private final Cache<String, Integer> cache;
private final int myThreadId;
private Worker(Cache<String, Integer> cache, int myThreadId) {
this.cache = cache;
this.myThreadId = myThreadId;
}
@Override
public void run() {
for (Map.Entry<String, EntryInfo> entry : state.entrySet()) {
String cacheKey = entry.getKey();
EntryInfo wrapper = state.get(cacheKey);
int val = getClusterStartupTime(this.cache, cacheKey, wrapper);
if (myThreadId == 1) {
wrapper.th1.set(val);
} else {
wrapper.th2.set(val);
}
}
System.out.println("Worker finished: " + myThreadId);
}
}
public static int getClusterStartupTime(Cache<String, Integer> cache, String cacheKey, EntryInfo wrapper) {
int startupTime = new Random().nextInt(1024);
// Concurrency doesn't work correctly with this
//Integer existingClusterStartTime = (Integer) cache.putIfAbsent(cacheKey, startupTime);
// Concurrency works fine with this
RemoteCache remoteCache = cache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class).iterator().next().getRemoteCache();
Integer existingClusterStartTime = (Integer) remoteCache.withFlags(Flag.FORCE_RETURN_VALUE).putIfAbsent(cacheKey, startupTime);
if (existingClusterStartTime == null) {
wrapper.successfulInitializations.incrementAndGet();
return startupTime;
} else {
return existingClusterStartTime;
}
}
private static class EntryInfo {
AtomicInteger successfulInitializations = new AtomicInteger(0);
AtomicInteger successfulListenerWrites = new AtomicInteger(0);
AtomicInteger th1 = new AtomicInteger();
AtomicInteger th2 = new AtomicInteger();
@Override
public String toString() {
return String.format("Inits: %d, listeners: %d, th1: %d, th2: %d", successfulInitializations.get(), successfulListenerWrites.get(), th1.get(), th2.get());
}
}
}

View file

@ -146,7 +146,8 @@ public class JpaRealmProvider implements RealmProvider {
query.setParameter("realm", realm.getId());
List<String> clients = query.getResultList();
for (String client : clients) {
session.realms().removeClient(client, adapter);
// No need to go through cache. Clients were already invalidated
removeClient(client, adapter);
}
for (ClientTemplateEntity a : new LinkedList<>(realm.getClientTemplates())) {
@ -154,7 +155,8 @@ public class JpaRealmProvider implements RealmProvider {
}
for (RoleModel role : adapter.getRoles()) {
session.realms().removeRole(adapter, role);
// No need to go through cache. Roles were already invalidated
removeRole(adapter, role);
}
@ -486,7 +488,8 @@ public class JpaRealmProvider implements RealmProvider {
session.users().preRemove(realm, client);
for (RoleModel role : client.getRoles()) {
client.removeRole(role);
// No need to go through cache. Roles were already invalidated
removeRole(realm, role);
}
ClientEntity clientEntity = ((ClientAdapter)client).getEntity();

View file

@ -124,17 +124,6 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
UserEntity userEntity = em.find(UserEntity.class, user.getId());
if (userEntity == null) return false;
removeUser(userEntity);
session.getKeycloakSessionFactory().publish(new UserModel.UserRemovedEvent() {
@Override
public UserModel getUser() {
return user;
}
@Override
public KeycloakSession getKeycloakSession() {
return session;
}
});
return true;
}

View file

@ -952,13 +952,6 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
return session.realms().getRoleById(id, this);
}
@Override
public boolean removeRoleById(String id) {
RoleModel role = getRoleById(id);
if (role == null) return false;
return role.getContainer().removeRole(role);
}
@Override
public PasswordPolicy getPasswordPolicy() {
if (passwordPolicy == null) {
@ -1932,12 +1925,6 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
return session.realms().createGroup(this, id, name);
}
@Override
public void addTopLevelGroup(GroupModel subGroup) {
session.realms().addTopLevelGroup(this, subGroup);
}
@Override
public void moveGroup(GroupModel group, GroupModel toParent) {
session.realms().moveGroup(this, group, toParent);

View file

@ -44,11 +44,6 @@ public class JpaUserSessionPersisterProviderFactory implements UserSessionPersis
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {

View file

@ -41,6 +41,7 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.UserManager;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.cache.CachedUserModel;
@ -50,6 +51,8 @@ import org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity;
import org.keycloak.models.mongo.keycloak.entities.UserConsentEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.storage.UserStorageProvider;
import java.util.ArrayList;
import java.util.Collections;
@ -630,7 +633,19 @@ public class MongoUserProvider implements UserProvider, UserCredentialStore {
@Override
public void preRemove(RealmModel realm, ComponentModel component) {
if (!component.getProviderType().equals(UserStorageProvider.class.getName())) return;
DBObject query = new QueryBuilder()
.and("federationLink").is(component.getId())
.get();
List<MongoUserEntity> mongoUsers = getMongoStore().loadEntities(MongoUserEntity.class, query, invocationContext);
UserManager userManager = new UserManager(session);
for (MongoUserEntity userEntity : mongoUsers) {
// Doing this way to ensure UserRemovedEvent triggered with proper callbacks.
UserAdapter user = new UserAdapter(session, realm, userEntity, invocationContext);
userManager.removeUser(realm, user, this);
}
}
@Override
@ -661,16 +676,18 @@ public class MongoUserProvider implements UserProvider, UserCredentialStore {
}
public MongoUserEntity getMongoUserEntity(UserModel user) {
UserAdapter adapter = null;
if (user instanceof CachedUserModel) {
adapter = (UserAdapter)((CachedUserModel)user).getDelegateForUpdate();
} else if (user instanceof UserAdapter ){
adapter = (UserAdapter)user;
if (user instanceof UserAdapter) {
UserAdapter adapter = (UserAdapter)user;
return adapter.getMongoEntity();
} else if (user instanceof CachedUserModel) {
UserModel delegate = ((CachedUserModel)user).getDelegateForUpdate();
return getMongoUserEntity(delegate);
} else if (user instanceof UserModelDelegate){
UserModel delegate = ((UserModelDelegate) user).getDelegate();
return getMongoUserEntity(delegate);
} else {
return getMongoStore().loadEntity(MongoUserEntity.class, user.getId(), invocationContext);
}
return adapter.getMongoEntity();
}
@Override

View file

@ -42,11 +42,6 @@ public class MongoUserSessionPersisterProviderFactory implements UserSessionPers
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {

View file

@ -23,7 +23,6 @@ import org.keycloak.common.enums.SslRequired;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.jose.jwk.JWKBuilder;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatorConfigModel;
@ -62,10 +61,6 @@ import org.keycloak.models.mongo.keycloak.entities.UserFederationProviderEntity;
import org.keycloak.models.utils.ComponentUtil;
import org.keycloak.models.utils.KeycloakModelUtils;
import java.security.Key;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -515,13 +510,6 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
return session.realms().removeRole(this, role);
}
@Override
public boolean removeRoleById(String id) {
RoleModel role = getRoleById(id);
if (role == null) return false;
return removeRole(role);
}
@Override
public Set<RoleModel> getRoles() {
DBObject query = new QueryBuilder()
@ -554,12 +542,6 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
return session.realms().createGroup(this, id, name);
}
@Override
public void addTopLevelGroup(GroupModel subGroup) {
session.realms().addTopLevelGroup(this, subGroup);
}
@Override
public void moveGroup(GroupModel group, GroupModel toParent) {
session.realms().moveGroup(this, group, toParent);
@ -2006,28 +1988,39 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
@Override
public void removeComponent(ComponentModel component) {
Iterator<ComponentEntity> it = realm.getComponentEntities().iterator();
ComponentEntity found = null;
while(it.hasNext()) {
if (it.next().getId().equals(component.getId())) {
session.users().preRemove(this, component);
removeComponents(component.getId());
it.remove();
ComponentEntity next = it.next();
if (next.getId().equals(component.getId())) {
found = next;
break;
}
}
updateRealm();
if (found != null) {
session.users().preRemove(this, component);
removeComponents(component.getId());
realm.getComponentEntities().remove(found);
updateRealm();
}
}
@Override
public void removeComponents(String parentId) {
Iterator<ComponentEntity> it = realm.getComponentEntities().iterator();
Set<ComponentEntity> toRemove = new HashSet<>();
while(it.hasNext()) {
ComponentEntity next = it.next();
if (next.getParentId().equals(parentId)) {
session.users().preRemove(this, entityToModel(next));
it.remove();
toRemove.add(next);
}
}
for (ComponentEntity toRem : toRemove) {
session.users().preRemove(this, entityToModel(toRem));
realm.getComponentEntities().remove(toRem);
}
updateRealm();
}

View file

@ -261,6 +261,7 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
@Override
public boolean isMemberOf(GroupModel group) {
if (user.getGroupIds() == null) return false;
if (user.getGroupIds().contains(group.getId())) return true;
Set<GroupModel> groups = getGroups();
return RoleUtils.isMember(groups, group);

13
pom.xml
View file

@ -80,7 +80,7 @@
<!-- Authorization Drools Policy Provider -->
<version.org.drools>6.4.0.Final</version.org.drools>
<version.jboss-integration-platform>6.0.6.Final</version.jboss-integration-platform>
<version.jboss-integration-platform>6.0.10.Final</version.jboss-integration-platform>
<!-- Others -->
<apacheds.version>2.0.0-M21</apacheds.version>
@ -98,7 +98,6 @@
<servlet.api.30.version>1.0.2.Final</servlet.api.30.version>
<twitter4j.version>4.0.4</twitter4j.version>
<jna.version>4.1.0</jna.version>
<jnr.version>0.14</jnr.version>
<!-- Test -->
<greenmail.version>1.3.1b</greenmail.version>
@ -634,6 +633,11 @@
<artifactId>infinispan-core</artifactId>
<version>${infinispan.version}</version>
</dependency>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-cachestore-remote</artifactId>
<version>${infinispan.version}</version>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
@ -702,11 +706,6 @@
<artifactId>jna</artifactId>
<version>${jna.version}</version>
</dependency>
<dependency>
<groupId>com.github.jnr</groupId>
<artifactId>jnr-unixsocket</artifactId>
<version>${jnr.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-ldap-federation</artifactId>

View file

@ -79,8 +79,9 @@ public enum JBossSAMLURIConstants {
"http://www.w3.org/2000/09/xmldsig#rsa-sha1"),
SAML_HTTP_POST_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"),
SAML_HTTP_SOAP_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:SOAP"),
SAML_HTTP_REDIRECT_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"),
SAML_SOAP_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:SOAP"),
SAML_PAOS_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:PAOS"),
SAML_11_NS("urn:oasis:names:tc:SAML:1.0:assertion"),

Some files were not shown because too many files have changed in this diff Show more