diff --git a/README.md b/README.md index 983d44cdb0..a4454a0fc9 100755 --- a/README.md +++ b/README.md @@ -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 diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java index 121adf1755..472afb7d45 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java @@ -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); } diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java index 9377b0b0db..0186e187c7 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java @@ -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 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 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 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); diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java index ff694bf54f..37b8f3dd30 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java @@ -105,7 +105,16 @@ public class PolicyEnforcer { } private List 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 { diff --git a/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlPrincipal.java b/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlPrincipal.java index e516e34fee..f9a1e77c29 100755 --- a/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlPrincipal.java +++ b/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlPrincipal.java @@ -105,7 +105,7 @@ public class SamlPrincipal implements Serializable, Principal { * @return */ public List getFriendlyAttributes(String friendlyName) { - List list = friendlyAttributes.get(name); + List list = friendlyAttributes.get(friendlyName); if (list != null) { return Collections.unmodifiableList(list); } else { diff --git a/authz/client/pom.xml b/authz/client/pom.xml index bf3f6b87d0..d053a99e77 100644 --- a/authz/client/pom.xml +++ b/authz/client/pom.xml @@ -18,6 +18,8 @@ KeyCloak AuthZ: Client API + 1.7 + 1.7 org.keycloak.authorization.client.* @@ -63,6 +65,14 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + + maven-jar-plugin @@ -98,4 +108,4 @@ - \ No newline at end of file + diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java index be83987750..d132620bac 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java +++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java @@ -48,7 +48,7 @@ public class HttpMethod { private HttpMethodResponse response; public HttpMethod(Configuration configuration, RequestBuilder builder) { - this(configuration, builder, new HashMap<>(), new HashMap<>()); + this(configuration, builder, new HashMap(), new HashMap()); } public HttpMethod(Configuration configuration, RequestBuilder builder, HashMap params, HashMap headers) { @@ -155,4 +155,4 @@ public class HttpMethod { } }; } -} \ No newline at end of file +} diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodResponse.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodResponse.java index 4155240f7a..fceca19d61 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodResponse.java +++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodResponse.java @@ -41,7 +41,7 @@ public class HttpMethodResponse { }); } - public HttpMethodResponse json(Class responseType) { + public HttpMethodResponse json(final Class responseType) { return new HttpMethodResponse(this.method) { @Override public R execute() { diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java index 301bb7b9b9..67de87a7cb 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java @@ -45,7 +45,7 @@ public class RolePolicyProviderFactory implements PolicyProviderFactory { @Override public String getName() { - return "Role-Based"; + return "Role"; } @Override diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProviderFactory.java index 946147474f..fdeeac0b17 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProviderFactory.java @@ -43,7 +43,7 @@ public class UserPolicyProviderFactory implements PolicyProviderFactory { @Override public String getName() { - return "User-Based"; + return "User"; } @Override diff --git a/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProviderFactory.java b/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProviderFactory.java index 0e1e2089c7..b3305aeb3e 100644 --- a/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProviderFactory.java +++ b/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProviderFactory.java @@ -30,7 +30,7 @@ public class DroolsPolicyProviderFactory implements PolicyProviderFactory { @Override public String getName() { - return "Drools"; + return "Rule"; } @Override diff --git a/core/src/main/java/org/keycloak/AuthorizationContext.java b/core/src/main/java/org/keycloak/AuthorizationContext.java index 05bb97d7a3..a14594bc46 100644 --- a/core/src/main/java/org/keycloak/AuthorizationContext.java +++ b/core/src/main/java/org/keycloak/AuthorizationContext.java @@ -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 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() { diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java index 0c3faf8d53..db874c096e 100644 --- a/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java +++ b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java @@ -122,6 +122,9 @@ public class PolicyEnforcerConfig { private List 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 + '\'' + '}'; } diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml index 526a0cf057..3fdf4895bf 100755 --- a/dependencies/server-all/pom.xml +++ b/dependencies/server-all/pom.xml @@ -255,6 +255,10 @@ + + org.sonatype.sisu.inject + guice-servlet + org.sonatype.plexus plexus-cipher diff --git a/distribution/adapters/as7-eap6-adapter/as7-adapter-zip/assembly.xml b/distribution/adapters/as7-eap6-adapter/as7-adapter-zip/assembly.xml index 15c4b6abdf..7419fa29f1 100755 --- a/distribution/adapters/as7-eap6-adapter/as7-adapter-zip/assembly.xml +++ b/distribution/adapters/as7-eap6-adapter/as7-adapter-zip/assembly.xml @@ -1,3 +1,4 @@ + + org/keycloak/keycloak-authz-client/** **/*.war diff --git a/distribution/adapters/as7-eap6-adapter/as7-modules/build.xml b/distribution/adapters/as7-eap6-adapter/as7-modules/build.xml index 829a4d634f..d22273f845 100755 --- a/distribution/adapters/as7-eap6-adapter/as7-modules/build.xml +++ b/distribution/adapters/as7-eap6-adapter/as7-modules/build.xml @@ -91,6 +91,10 @@ + + + + diff --git a/distribution/adapters/as7-eap6-adapter/as7-modules/pom.xml b/distribution/adapters/as7-eap6-adapter/as7-modules/pom.xml index 732e7c72cf..e484d8dc5e 100755 --- a/distribution/adapters/as7-eap6-adapter/as7-modules/pom.xml +++ b/distribution/adapters/as7-eap6-adapter/as7-modules/pom.xml @@ -102,6 +102,11 @@ com.fasterxml.jackson.jaxrs jackson-jaxrs-json-provider + + + org.keycloak + keycloak-authz-client + diff --git a/distribution/adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-core/main/module.xml b/distribution/adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-core/main/module.xml index 984cb50bad..21ea5ed0dd 100755 --- a/distribution/adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-core/main/module.xml +++ b/distribution/adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-core/main/module.xml @@ -34,6 +34,7 @@ + diff --git a/distribution/adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-authz-client/main/module.xml b/distribution/adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-authz-client/main/module.xml new file mode 100644 index 0000000000..3cd1abd063 --- /dev/null +++ b/distribution/adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-authz-client/main/module.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/distribution/adapters/as7-eap6-adapter/eap6-adapter-zip/assembly.xml b/distribution/adapters/as7-eap6-adapter/eap6-adapter-zip/assembly.xml index c69ea6b13b..1fafc6341c 100755 --- a/distribution/adapters/as7-eap6-adapter/eap6-adapter-zip/assembly.xml +++ b/distribution/adapters/as7-eap6-adapter/eap6-adapter-zip/assembly.xml @@ -39,6 +39,9 @@ org/keycloak/keycloak-as7-subsystem/** org/keycloak/keycloak-adapter-subsystem/** org/keycloak/keycloak-servlet-oauth-client/** + + + org/keycloak/keycloak-authz-client/** **/*.war diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/eclipse/sisu/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/eclipse/sisu/main/module.xml index 3861d46c43..55b92dc251 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/eclipse/sisu/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/eclipse/sisu/main/module.xml @@ -22,6 +22,7 @@ + diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml index a80a008a32..e7fdb8abed 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml @@ -30,6 +30,9 @@ + + + diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml index 9db480c9d0..cd71511382 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml @@ -30,7 +30,7 @@ - + diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-sssd-federation/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-sssd-federation/main/module.xml index 58939eb49c..6d56d6e995 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-sssd-federation/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-sssd-federation/main/module.xml @@ -22,6 +22,7 @@ + diff --git a/distribution/server-overlay/src/main/cli/keycloak-install-ha.cli b/distribution/server-overlay/src/main/cli/keycloak-install-ha.cli index 17fd5f0d25..a3b85f17c7 100644 --- a/distribution/server-overlay/src/main/cli/keycloak-install-ha.cli +++ b/distribution/server-overlay/src/main/cli/keycloak-install-ha.cli @@ -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") diff --git a/examples/providers/rest/README.md b/examples/providers/rest/README.md index d78e9bc958..9d26627a46 100644 --- a/examples/providers/rest/README.md +++ b/examples/providers/rest/README.md @@ -12,5 +12,5 @@ Then registering the provider by editing `standalone/configuration/standalone.xm module:org.keycloak.examples.hello-rest-example -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. \ No newline at end of file +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. diff --git a/examples/saml/servlet-filter/src/main/webapp/META-INF/jboss-deployment-structure.xml b/examples/saml/servlet-filter/src/main/webapp/META-INF/jboss-deployment-structure.xml new file mode 100644 index 0000000000..b2ee9668db --- /dev/null +++ b/examples/saml/servlet-filter/src/main/webapp/META-INF/jboss-deployment-structure.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/federation/sssd/pom.xml b/federation/sssd/pom.xml index 29113f7c8e..a9029c4e93 100644 --- a/federation/sssd/pom.xml +++ b/federation/sssd/pom.xml @@ -49,6 +49,7 @@ net.java.dev.jna jna + provided org.keycloak @@ -70,10 +71,6 @@ jboss-logging provided - - com.github.jnr - jnr-unixsocket - diff --git a/federation/sssd/src/main/java/cx/ath/matthew/LibraryLoader.java b/federation/sssd/src/main/java/cx/ath/matthew/LibraryLoader.java new file mode 100644 index 0000000000..4088d46ebb --- /dev/null +++ b/federation/sssd/src/main/java/cx/ath/matthew/LibraryLoader.java @@ -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 Bruno Oliveira. + */ +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; + } +} diff --git a/federation/sssd/src/main/java/cx/ath/matthew/unix/USInputStream.java b/federation/sssd/src/main/java/cx/ath/matthew/unix/USInputStream.java index b11609f8a2..eb143fe6a9 100644 --- a/federation/sssd/src/main/java/cx/ath/matthew/unix/USInputStream.java +++ b/federation/sssd/src/main/java/cx/ath/matthew/unix/USInputStream.java @@ -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 RubyUNIXSocket.java - */ - 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; - } } diff --git a/federation/sssd/src/main/java/cx/ath/matthew/unix/USOutputStream.java b/federation/sssd/src/main/java/cx/ath/matthew/unix/USOutputStream.java index 1855b26a44..d8c85a7718 100644 --- a/federation/sssd/src/main/java/cx/ath/matthew/unix/USOutputStream.java +++ b/federation/sssd/src/main/java/cx/ath/matthew/unix/USOutputStream.java @@ -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 RubyUNIXSocket.java - */ - 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); - } } diff --git a/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixIOException.java b/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixIOException.java new file mode 100644 index 0000000000..24fd20cd1f --- /dev/null +++ b/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixIOException.java @@ -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; + } +} diff --git a/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixSocket.java b/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixSocket.java index 7537f018ef..8851637436 100644 --- a/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixSocket.java +++ b/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixSocket.java @@ -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; } /** diff --git a/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixSocketAddress.java b/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixSocketAddress.java new file mode 100644 index 0000000000..0baba479bb --- /dev/null +++ b/federation/sssd/src/main/java/cx/ath/matthew/unix/UnixSocketAddress.java @@ -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(); + } +} diff --git a/federation/sssd/src/main/java/org/freedesktop/dbus/MessageWriter.java b/federation/sssd/src/main/java/org/freedesktop/dbus/MessageWriter.java index 2a534263a3..45e8cb75f5 100644 --- a/federation/sssd/src/main/java/org/freedesktop/dbus/MessageWriter.java +++ b/federation/sssd/src/main/java/org/freedesktop/dbus/MessageWriter.java @@ -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(); } diff --git a/federation/sssd/src/main/java/org/freedesktop/dbus/Transport.java b/federation/sssd/src/main/java/org/freedesktop/dbus/Transport.java index 0c997bd021..1745bcfec2 100644 --- a/federation/sssd/src/main/java/org/freedesktop/dbus/Transport.java +++ b/federation/sssd/src/main/java/org/freedesktop/dbus/Transport.java @@ -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 { diff --git a/misc/CrossDataCenter.md b/misc/CrossDataCenter.md new file mode 100644 index 0000000000..4146eaa9e6 --- /dev/null +++ b/misc/CrossDataCenter.md @@ -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" : + +``` + +``` + +- 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: + +``` + +``` + +3) Configure `work` cache to use remoteStore. You should use this: + +``` + + + true + org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory + + +``` + +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: + +``` + + + +``` + +5) Optional: Configure logging in standalone.xml to see what invalidation events were send: +```` + + + + + + +```` + +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 : + +``` + +``` diff --git a/model/infinispan/pom.xml b/model/infinispan/pom.xml index f10fa60751..fba921c627 100755 --- a/model/infinispan/pom.xml +++ b/model/infinispan/pom.xml @@ -48,6 +48,10 @@ org.infinispan infinispan-core + + org.infinispan + infinispan-cachestore-remote + junit junit diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java new file mode 100644 index 0000000000..17795ca213 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java @@ -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 Marek Posolda + */ +abstract class CrossDCAwareCacheFactory { + + protected static final Logger logger = Logger.getLogger(CrossDCAwareCacheFactory.class); + + + abstract BasicCache getCache(); + + + static CrossDCAwareCacheFactory getFactory(Cache workCache, Set 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 workCache; + + InfinispanCacheWrapperFactory(Cache workCache) { + this.workCache = workCache; + } + + @Override + BasicCache getCache() { + return workCache; + } + + } + + + // We have external JDG configured. Cross-DC should be enabled + private static class RemoteCacheWrapperFactory extends CrossDCAwareCacheFactory { + + private final RemoteCache remoteCache; + + RemoteCacheWrapperFactory(RemoteCache remoteCache) { + this.remoteCache = remoteCache; + } + + @Override + BasicCache getCache() { + // Flags are per-invocation! + return remoteCache.withFlags(Flag.FORCE_RETURN_VALUE); + } + + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java index 8b77c25b6d..5a4bdb744b 100644 --- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java @@ -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 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 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 cache) { - Transport transport = cache.getCacheManager().getTransport(); - return transport==null ? "local" : transport.getAddress().toString(); - } - - - private LockEntry createLockEntry(Cache 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; diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java index 75aef45327..a96621d7b2 100644 --- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java @@ -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 Marek Posolda */ 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 workCache; - private Map 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 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 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 } - void registerListener(String taskKey, ClusterListener task) { - listeners.put(taskKey, task); - } - - @Listener - public class CacheEntryListener { - - @CacheEntryCreated - public void cacheEntryCreated(CacheEntryCreatedEvent event) { - if (!event.isPre()) { - trigger(event.getKey(), event.getValue()); - } - } - - @CacheEntryModified - public void cacheEntryModified(CacheEntryModifiedEvent 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); - } - } - } - } diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java new file mode 100644 index 0000000000..57cc003a53 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java @@ -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 Marek Posolda + */ +public class InfinispanNotificationsManager { + + protected static final Logger logger = Logger.getLogger(InfinispanNotificationsManager.class); + + private final MultivaluedHashMap listeners = new MultivaluedHashMap<>(); + + private final Cache workCache; + + private final String myAddress; + + + protected InfinispanNotificationsManager(Cache workCache, String myAddress) { + this.workCache = workCache; + this.myAddress = myAddress; + } + + + // Create and init manager including all listeners etc + public static InfinispanNotificationsManager create(Cache workCache, String myAddress, Set 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 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 event) { + eventReceived(event.getKey(), event.getValue()); + } + + @CacheEntryModified + public void cacheEntryModified(CacheEntryModifiedEvent event) { + eventReceived(event.getKey(), event.getValue()); + } + } + + + @ClientListener + public class HotRodListener { + + private final RemoteCache remoteCache; + + public HotRodListener(RemoteCache 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 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); + } + } + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/KeycloakHotRodMarshallerFactory.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/KeycloakHotRodMarshallerFactory.java new file mode 100644 index 0000000000..4a73bf3fdf --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/KeycloakHotRodMarshallerFactory.java @@ -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 Marek Posolda + */ +public class KeycloakHotRodMarshallerFactory { + + public static GenericJBossMarshaller getInstance() { + return new GenericJBossMarshaller(KeycloakHotRodMarshallerFactory.class.getClassLoader()); + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java new file mode 100644 index 0000000000..b03dd70c0a --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java @@ -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 Marek Posolda + */ +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()); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java index 8ad75fd6a8..7781e3ac7d 100755 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java @@ -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); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java index ad1ba26fab..c254ea7164 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java @@ -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 revisions; protected final Cache cache; protected final UpdateCounter counter = new UpdateCounter(); @@ -63,9 +62,10 @@ public abstract class CacheManager { public CacheManager(Cache cache, Cache revisions) { this.cache = cache; this.revisions = revisions; - this.cache.addListener(this); } + protected abstract Logger getLogger(); + public Cache 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 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> predicate = getInvalidationPredicate(object); - if (predicate != null) runEvictions(predicate); - RealmCacheManager.logger.tracev("invalidating: {0}" + object.getClass().getName()); - } + public void sendInvalidationEvents(KeycloakSession session, Collection 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 event) { - if (!event.isPre()) - for (Map.Entry entry : event.getEntries().entrySet()) { - Object object = entry.getValue(); - bumpVersion(entry.getKey()); - if (object == null) continue; - RealmCacheManager.logger.tracev("evicting: {0}" + object.getClass().getName()); - Predicate> 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 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> current) { - Set 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 invalidations); - protected abstract Predicate> getInvalidationPredicate(Object object); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java index 980957a04d..11c1c62ac7 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java @@ -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 diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java index 4bbe4c73db..c2ad8cef40 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java @@ -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 Bill Burke @@ -54,14 +54,23 @@ public class InfinispanCacheRealmProviderFactory implements CacheRealmProviderFa Cache cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.REALM_CACHE_NAME); Cache 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"); } } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java index 14d420b778..e8c2ba14fb 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java @@ -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 Stian Thorgersen @@ -55,13 +55,25 @@ public class InfinispanUserCacheProviderFactory implements UserCacheProviderFact Cache cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_CACHE_NAME); Cache 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"); } } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java index 069b34aec5..1748e3ca27 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java @@ -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); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java index 55e3b38777..b01dbabf31 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java @@ -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 Stian Thorgersen */ -@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 cache, Cache revisions) { super(cache, revisions); } - public void realmInvalidation(String id, Set invalidations) { - Predicate> predicate = getRealmInvalidationPredicate(id); - addInvalidations(predicate, invalidations); + public void realmUpdated(String id, String name, Set invalidations) { + invalidations.add(id); + invalidations.add(RealmCacheSession.getRealmByNameCacheKey(name)); } - public Predicate> getRealmInvalidationPredicate(String id) { - return RealmQueryPredicate.create().realm(id); + public void realmRemoval(String id, String name, Set invalidations) { + realmUpdated(id, name, invalidations); + + addInvalidations(InRealmPredicate.create().realm(id), invalidations); } - public void clientInvalidation(String id, Set invalidations) { - addInvalidations(getClientInvalidationPredicate(id), invalidations); + public void roleAdded(String roleContainerId, Set invalidations) { + invalidations.add(RealmCacheSession.getRolesCacheKey(roleContainerId)); } - public Predicate> getClientInvalidationPredicate(String id) { - return ClientQueryPredicate.create().client(id); + public void roleUpdated(String roleContainerId, String roleName, Set invalidations) { + invalidations.add(RealmCacheSession.getRoleByNameCacheKey(roleContainerId, roleName)); } - public void roleInvalidation(String id, Set invalidations) { - addInvalidations(getRoleInvalidationPredicate(id), invalidations); + public void roleRemoval(String id, String roleName, String roleContainerId, Set invalidations) { + invalidations.add(RealmCacheSession.getRolesCacheKey(roleContainerId)); + invalidations.add(RealmCacheSession.getRoleByNameCacheKey(roleContainerId, roleName)); + addInvalidations(HasRolePredicate.create().role(id), invalidations); } - public Predicate> getRoleInvalidationPredicate(String id) { - return HasRolePredicate.create().role(id); + public void groupQueriesInvalidations(String realmId, Set 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 invalidations) { - addInvalidations(getGroupInvalidationPredicate(id), invalidations); - + public void clientAdded(String realmId, String clientUUID, String clientId, Set invalidations) { + invalidations.add(RealmCacheSession.getRealmClientsQueryCacheKey(realmId)); } - public Predicate> getGroupInvalidationPredicate(String id) { - return GroupQueryPredicate.create().group(id); + public void clientUpdated(String realmId, String clientUuid, String clientId, Set invalidations) { + invalidations.add(RealmCacheSession.getClientByClientIdCacheKey(clientId, realmId)); } - public void clientTemplateInvalidation(String id, Set invalidations) { - addInvalidations(getClientTemplateInvalidationPredicate(id), invalidations); + // Client roles invalidated separately + public void clientRemoval(String realmId, String clientUUID, String clientId, Set invalidations) { + invalidations.add(RealmCacheSession.getRealmClientsQueryCacheKey(realmId)); + invalidations.add(RealmCacheSession.getClientByClientIdCacheKey(clientId, realmId)); + addInvalidations(InClientPredicate.create().client(clientUUID), invalidations); } - public Predicate> getClientTemplateInvalidationPredicate(String id) { - return ClientTemplateQueryPredicate.create().template(id); - } - - public void realmRemoval(String id, Set invalidations) { - Predicate> predicate = getRealmRemovalPredicate(id); - addInvalidations(predicate, invalidations); - } - - public Predicate> getRealmRemovalPredicate(String id) { - Predicate> predicate = null; - predicate = RealmQueryPredicate.create().realm(id) - .or(InRealmPredicate.create().realm(id)); - return predicate; - } - - public void clientAdded(String realmId, String id, Set invalidations) { - addInvalidations(getClientAddedPredicate(realmId), invalidations); - } - - public Predicate> getClientAddedPredicate(String realmId) { - return ClientQueryPredicate.create().inRealm(realmId); - } - - public void clientRemoval(String realmId, String id, Set invalidations) { - Predicate> predicate = null; - predicate = getClientRemovalPredicate(realmId, id); - addInvalidations(predicate, invalidations); - } - - public Predicate> getClientRemovalPredicate(String realmId, String id) { - Predicate> predicate; - predicate = ClientQueryPredicate.create().inRealm(realmId) - .or(ClientQueryPredicate.create().client(id)) - .or(InClientPredicate.create().client(id)); - return predicate; - } - - public void roleRemoval(String id, Set invalidations) { - addInvalidations(getRoleRemovalPredicate(id), invalidations); - - } - - public Predicate> getRoleRemovalPredicate(String id) { - return getRoleInvalidationPredicate(id); - } @Override - protected Predicate> 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> 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 invalidations) { + if (event instanceof RealmCacheInvalidationEvent) { + invalidations.add(event.getId()); + + ((RealmCacheInvalidationEvent) event).addInvalidations(this, invalidations); } - return null; } + } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java index 9321f47197..d61a611926 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java @@ -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 managedGroups = new HashMap<>(); protected Set listInvalidations = new HashSet<>(); protected Set invalidations = new HashSet<>(); + protected Set 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 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 diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java index a43aeb82f4..b6862f55e8 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java @@ -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"); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java index ee8dc8b86a..e9493144e1 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java @@ -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 Stian Thorgersen */ -@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 cache, Cache 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 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 federatedIdentities, Set invalidations) { + userUpdatedInvalidations(userId, username, email, realmId, invalidations); + + if (identityFederationEnabled) { + // Invalidate all keys for lookup this user by any identityProvider link + for (Map.Entry 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 invalidations) { + invalidations.add(UserCacheSession.getFederatedIdentityLinksCacheKey(userId)); + } + + public void federatedIdentityLinkRemovedInvalidation(String userId, String realmId, String identityProviderId, String socialUserId, Set invalidations) { + invalidations.add(UserCacheSession.getFederatedIdentityLinksCacheKey(userId)); + if (identityProviderId != null) { + invalidations.add(UserCacheSession.getUserByFederatedIdentityCacheKey(realmId, identityProviderId, socialUserId)); + } + } + + public void consentInvalidation(String userId, Set invalidations) { + invalidations.add(UserCacheSession.getConsentCacheKey(userId)); + } + + @Override - protected Predicate> getInvalidationPredicate(Object object) { - return null; + protected void addInvalidationsFromEvent(InvalidationEvent event, Set invalidations) { + if (event instanceof UserCacheInvalidationEvent) { + ((UserCacheInvalidationEvent) event).addInvalidations(this, invalidations); + } } public void invalidateRealmUsers(String realm, Set invalidations) { - addInvalidations(InRealmPredicate.create().realm(realm), invalidations); + InRealmPredicate inRealmPredicate = getInRealmPredicate(realm); + addInvalidations(inRealmPredicate, invalidations); + } + + private InRealmPredicate getInRealmPredicate(String realmId) { + return InRealmPredicate.create().realm(realmId); } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java index 5531de1753..cb8c0a84ec 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java @@ -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 invalidations = new HashSet<>(); protected Set realmInvalidations = new HashSet<>(); + protected Set invalidationEvents = new HashSet<>(); // Events to be sent across cluster protected Map 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 federatedIdentities = realm.isIdentityFederationEnabled() ? getFederatedIdentities(user, realm) : null; - if (realm.isIdentityFederationEnabled()) { - // Invalidate all keys for lookup this user by any identityProvider link - Set 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)); + } + } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java index 4647f74f68..5dd4bacaf7 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java @@ -135,7 +135,6 @@ public class CachedRealm extends AbstractExtendableRevisioned { } protected List defaultGroups = new LinkedList(); - protected Set groups = new HashSet(); protected List clientTemplates= new LinkedList<>(); protected boolean internationalizationEnabled; protected Set 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 getGroups() { - return groups; - } - public List getDefaultGroups() { return defaultGroups; } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/ClientTemplateQuery.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/ClientTemplateQuery.java deleted file mode 100755 index a6fcebf836..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/ClientTemplateQuery.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.keycloak.models.cache.infinispan.entities; - -import java.util.Set; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public interface ClientTemplateQuery extends InRealm { - Set getTemplates(); -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupListQuery.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/GroupListQuery.java similarity index 83% rename from model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupListQuery.java rename to model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/GroupListQuery.java index 1fa44115de..1e0c664e1e 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupListQuery.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/GroupListQuery.java @@ -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; diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/RoleListQuery.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/RoleListQuery.java index 21a73adf6e..e924c0595b 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/RoleListQuery.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/RoleListQuery.java @@ -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 + '\'' + '}'; } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientAddedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientAddedEvent.java new file mode 100644 index 0000000000..1b022ca4cf --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientAddedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + realmCache.clientAdded(realmId, clientUuid, clientId, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientRemovedEvent.java new file mode 100644 index 0000000000..2e620db06e --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientRemovedEvent.java @@ -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 Marek Posolda + */ +public class ClientRemovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent { + + private String clientUuid; + private String clientId; + private String realmId; + // roleId -> roleName + private Map 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 invalidations) { + realmCache.clientRemoval(realmId, clientUuid, clientId, invalidations); + + // Separate iteration for all client roles to invalidate records dependent on them + for (Map.Entry clientRole : clientRoles.entrySet()) { + String roleId = clientRole.getKey(); + String roleName = clientRole.getValue(); + realmCache.roleRemoval(roleId, roleName, clientUuid, invalidations); + } + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientTemplateEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientTemplateEvent.java new file mode 100644 index 0000000000..7bb13a9388 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientTemplateEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + // Nothing. ID was already invalidated + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientUpdatedEvent.java new file mode 100644 index 0000000000..cc6c263fe0 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientUpdatedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + realmCache.clientUpdated(realmId, clientUuid, clientId, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupAddedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupAddedEvent.java new file mode 100644 index 0000000000..77dcf69ad2 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupAddedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + realmCache.groupQueriesInvalidations(realmId, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupMovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupMovedEvent.java new file mode 100644 index 0000000000..2f5566aed3 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupMovedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + realmCache.groupQueriesInvalidations(realmId, invalidations); + if (newParentId != null) { + invalidations.add(newParentId); + } + if (oldParentId != null) { + invalidations.add(oldParentId); + } + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupRemovedEvent.java new file mode 100644 index 0000000000..37689faca5 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupRemovedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + realmCache.groupQueriesInvalidations(realmId, invalidations); + if (parentId != null) { + invalidations.add(parentId); + } + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupUpdatedEvent.java new file mode 100644 index 0000000000..c59021b446 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupUpdatedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + // Nothing. ID already invalidated + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/InvalidationEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/InvalidationEvent.java new file mode 100644 index 0000000000..ea59ff5f1f --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/InvalidationEvent.java @@ -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 Marek Posolda + */ +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; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmCacheInvalidationEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmCacheInvalidationEvent.java new file mode 100644 index 0000000000..2876e08e3d --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmCacheInvalidationEvent.java @@ -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 Marek Posolda + */ +public interface RealmCacheInvalidationEvent { + + void addInvalidations(RealmCacheManager realmCache, Set invalidations); + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmRemovedEvent.java new file mode 100644 index 0000000000..355875734f --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmRemovedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + realmCache.realmRemoval(realmId, realmName, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmUpdatedEvent.java new file mode 100644 index 0000000000..624fc6da93 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmUpdatedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + realmCache.realmUpdated(realmId, realmName, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleAddedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleAddedEvent.java new file mode 100644 index 0000000000..cb393e5be8 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleAddedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + realmCache.roleAdded(containerId, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleRemovedEvent.java new file mode 100644 index 0000000000..6137b1bdae --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleRemovedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + realmCache.roleRemoval(roleId, roleName, containerId, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleUpdatedEvent.java new file mode 100644 index 0000000000..4b2ae5b2df --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleUpdatedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + realmCache.roleUpdated(containerId, roleName, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheInvalidationEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheInvalidationEvent.java new file mode 100644 index 0000000000..964e97ab43 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheInvalidationEvent.java @@ -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 Marek Posolda + */ +public interface UserCacheInvalidationEvent { + + void addInvalidations(UserCacheManager userCache, Set invalidations); + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheRealmInvalidationEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheRealmInvalidationEvent.java new file mode 100644 index 0000000000..39961815d4 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheRealmInvalidationEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + userCache.invalidateRealmUsers(realmId, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserConsentsUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserConsentsUpdatedEvent.java new file mode 100644 index 0000000000..021e84180f --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserConsentsUpdatedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + userCache.consentInvalidation(userId, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkRemovedEvent.java new file mode 100644 index 0000000000..15704df27e --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkRemovedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + userCache.federatedIdentityLinkRemovedInvalidation(userId, realmId, identityProviderId, socialUserId, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkUpdatedEvent.java new file mode 100644 index 0000000000..8bbfb41210 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkUpdatedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + userCache.federatedIdentityLinkUpdatedInvalidation(userId, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFullInvalidationEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFullInvalidationEvent.java new file mode 100644 index 0000000000..d637ac2f54 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFullInvalidationEvent.java @@ -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 Marek Posolda + */ +public class UserFullInvalidationEvent extends InvalidationEvent implements UserCacheInvalidationEvent { + + private String userId; + private String username; + private String email; + private String realmId; + private boolean identityFederationEnabled; + private Map federatedIdentities; + + public static UserFullInvalidationEvent create(String userId, String username, String email, String realmId, boolean identityFederationEnabled, Collection 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 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 invalidations) { + userCache.fullUserInvalidation(userId, username, email, realmId, identityFederationEnabled, federatedIdentities, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserUpdatedEvent.java new file mode 100644 index 0000000000..429b4af1dd --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserUpdatedEvent.java @@ -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 Marek Posolda + */ +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 invalidations) { + userCache.userUpdatedInvalidations(userId, username, email, realmId, invalidations); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/ClientQueryPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/ClientQueryPredicate.java deleted file mode 100755 index bf4ade8156..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/ClientQueryPredicate.java +++ /dev/null @@ -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 Bill Burke - * @version $Revision: 1 $ - */ -public class ClientQueryPredicate implements Predicate>, 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 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; - } -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/ClientTemplateQueryPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/ClientTemplateQueryPredicate.java deleted file mode 100755 index fba0c02ff6..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/ClientTemplateQueryPredicate.java +++ /dev/null @@ -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 Bill Burke - * @version $Revision: 1 $ - */ -public class ClientTemplateQueryPredicate implements Predicate>, 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 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); - } -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/GroupQueryPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/GroupQueryPredicate.java deleted file mode 100755 index 855930e0d3..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/GroupQueryPredicate.java +++ /dev/null @@ -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 Bill Burke - * @version $Revision: 1 $ - */ -public class GroupQueryPredicate implements Predicate>, 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 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); - } -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/RealmQueryPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/RealmQueryPredicate.java deleted file mode 100755 index dbb64f5b79..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/RealmQueryPredicate.java +++ /dev/null @@ -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 Bill Burke - * @version $Revision: 1 $ - */ -public class RealmQueryPredicate implements Predicate>, 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 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); - } -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/RoleQueryPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/RoleQueryPredicate.java deleted file mode 100755 index 5e37d59e60..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/RoleQueryPredicate.java +++ /dev/null @@ -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 Bill Burke - * @version $Revision: 1 $ - */ -public class RoleQueryPredicate implements Predicate>, 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 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); - } -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java index 44419cd8da..c21f787098 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java @@ -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); diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java index 343f2f0e1c..663a4b2e7d 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java @@ -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 cache = connections.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); Cache 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()); } } }); diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java index 1485da837b..c332eea8fc 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java @@ -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); } diff --git a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java new file mode 100644 index 0000000000..e7c1337934 --- /dev/null +++ b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java @@ -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 Marek Posolda + */ +@Ignore +public class ConcurrencyJDGRemoteCacheTest { + + private static Map 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 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 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 cache; + + private final int myThreadId; + + private Worker(Cache cache, int myThreadId) { + this.cache = cache; + this.myThreadId = myThreadId; + } + + @Override + public void run() { + for (Map.Entry 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 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()); + } + } + + + +} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java index f5d266602d..55e4108675 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java @@ -146,7 +146,8 @@ public class JpaRealmProvider implements RealmProvider { query.setParameter("realm", realm.getId()); List 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(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index b0ea73a6cc..9633f845ca 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -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; } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index 97aa4bd55d..f5b9d7df51 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -952,13 +952,6 @@ public class RealmAdapter implements RealmModel, JpaModel { 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 { 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); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProviderFactory.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProviderFactory.java index 254412bd13..35265afe30 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProviderFactory.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProviderFactory.java @@ -44,11 +44,6 @@ public class JpaUserSessionPersisterProviderFactory implements UserSessionPersis } - @Override - public void postInit(KeycloakSessionFactory factory) { - - } - @Override public void close() { diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java index 7b46c0c102..6ef597e662 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java @@ -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 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 diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProviderFactory.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProviderFactory.java index b4028a2f2e..083a0e6749 100644 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProviderFactory.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProviderFactory.java @@ -42,11 +42,6 @@ public class MongoUserSessionPersisterProviderFactory implements UserSessionPers } - @Override - public void postInit(KeycloakSessionFactory factory) { - - } - @Override public void close() { diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java index a79c478bc7..119c7df38e 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java @@ -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 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 getRoles() { DBObject query = new QueryBuilder() @@ -554,12 +542,6 @@ public class RealmAdapter extends AbstractMongoAdapter 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 impleme @Override public void removeComponent(ComponentModel component) { Iterator 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 it = realm.getComponentEntities().iterator(); + Set 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(); } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java index 9d5ad7c6f2..e5440cc4cd 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java @@ -261,6 +261,7 @@ public class UserAdapter extends AbstractMongoAdapter implement @Override public boolean isMemberOf(GroupModel group) { + if (user.getGroupIds() == null) return false; if (user.getGroupIds().contains(group.getId())) return true; Set groups = getGroups(); return RoleUtils.isMember(groups, group); diff --git a/pom.xml b/pom.xml index 7918c6dade..afd4212a9f 100755 --- a/pom.xml +++ b/pom.xml @@ -80,7 +80,7 @@ 6.4.0.Final - 6.0.6.Final + 6.0.10.Final 2.0.0-M21 @@ -98,7 +98,6 @@ 1.0.2.Final 4.0.4 4.1.0 - 0.14 1.3.1b @@ -634,6 +633,11 @@ infinispan-core ${infinispan.version} + + org.infinispan + infinispan-cachestore-remote + ${infinispan.version} + org.liquibase liquibase-core @@ -702,11 +706,6 @@ jna ${jna.version} - - com.github.jnr - jnr-unixsocket - ${jnr.version} - org.keycloak keycloak-ldap-federation diff --git a/saml-core-api/src/main/java/org/keycloak/saml/common/constants/JBossSAMLURIConstants.java b/saml-core-api/src/main/java/org/keycloak/saml/common/constants/JBossSAMLURIConstants.java index cf3b782e76..147949514e 100755 --- a/saml-core-api/src/main/java/org/keycloak/saml/common/constants/JBossSAMLURIConstants.java +++ b/saml-core-api/src/main/java/org/keycloak/saml/common/constants/JBossSAMLURIConstants.java @@ -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"), diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLEncryptionUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLEncryptionUtil.java index 245cff957a..f81ea03384 100755 --- a/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLEncryptionUtil.java +++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLEncryptionUtil.java @@ -20,6 +20,7 @@ import org.apache.xml.security.encryption.EncryptedData; import org.apache.xml.security.encryption.EncryptedKey; import org.apache.xml.security.encryption.XMLCipher; import org.apache.xml.security.encryption.XMLEncryptionException; +import org.apache.xml.security.utils.EncryptionConstants; import org.keycloak.saml.common.PicketLinkLogger; import org.keycloak.saml.common.PicketLinkLoggerFactory; @@ -38,8 +39,9 @@ import javax.xml.namespace.QName; import java.security.Key; import java.security.PrivateKey; import java.security.PublicKey; -import java.util.HashMap; import java.util.Objects; +import javax.xml.XMLConstants; +import javax.xml.crypto.dsig.XMLSignature; /** * Utility for XML Encryption Note: This utility is currently using Apache XML Security library API. JSR-106 is @@ -58,77 +60,12 @@ public class XMLEncryptionUtil { org.apache.xml.security.Init.init(); } - public static final String CIPHER_DATA_LOCALNAME = "CipherData"; - - public static final String ENCRYPTED_KEY_LOCALNAME = "EncryptedKey"; - public static final String DS_KEY_INFO = "ds:KeyInfo"; - public static final String XMLNS = "http://www.w3.org/2000/xmlns/"; - - public static final String XMLSIG_NS = "http://www.w3.org/2000/09/xmldsig#"; - - public static final String XMLENC_NS = "http://www.w3.org/2001/04/xmlenc#"; - - private static HashMap algorithms = new HashMap(4); - private static final String RSA_ENCRYPTION_SCHEME = Objects.equals(System.getProperty("keycloak.saml.key_trans.rsa_v1.5"), "true") ? XMLCipher.RSA_v1dot5 : XMLCipher.RSA_OAEP; - private static class EncryptionAlgorithm { - - EncryptionAlgorithm(String jceName, String xmlSecName, int size) { - this.jceName = jceName; - this.xmlSecName = xmlSecName; - this.size = size; - } - - @SuppressWarnings("unused") - public String jceName; - - public String xmlSecName; - - public int size; - } - - static { - algorithms.put("aes-128", new EncryptionAlgorithm("AES", XMLCipher.AES_128, 128)); - algorithms.put("aes-192", new EncryptionAlgorithm("AES", XMLCipher.AES_192, 192)); - algorithms.put("aes-256", new EncryptionAlgorithm("AES", XMLCipher.AES_256, 256)); - algorithms.put("aes", new EncryptionAlgorithm("AES", XMLCipher.AES_256, 256)); - - algorithms.put("tripledes", new EncryptionAlgorithm("TripleDes", XMLCipher.TRIPLEDES, 168)); - } - - /** - * Given the JCE algorithm, get the XML Encryption URL - * - * @param certAlgo - * - * @return - */ - public static String getEncryptionURL(String certAlgo) { - EncryptionAlgorithm ea = algorithms.get(certAlgo); - if (ea == null) - throw logger.encryptUnknownAlgoError(certAlgo); - return ea.xmlSecName; - } - - /** - * Given the JCE algorithm, get the XML Encryption KeySize - * - * @param certAlgo - * - * @return - */ - public static int getEncryptionKeySize(String certAlgo) { - EncryptionAlgorithm ea = algorithms.get(certAlgo); - if (ea == null) - throw logger.encryptUnknownAlgoError(certAlgo); - return ea.size; - } - /** *

* Encrypt the Key to be transported @@ -151,7 +88,7 @@ public class XMLEncryptionUtil { */ public static EncryptedKey encryptKey(Document document, SecretKey keyToBeEncrypted, PublicKey keyUsedToEncryptSecretKey, int keySize) throws ProcessingException { - XMLCipher keyCipher = null; + XMLCipher keyCipher; String pubKeyAlg = keyUsedToEncryptSecretKey.getAlgorithm(); try { @@ -170,14 +107,13 @@ public class XMLEncryptionUtil { * data * * @param elementQName QName of the element that we like to encrypt + * @param document * @param publicKey * @param secretKey * @param keySize * @param wrappingElementQName A QName of an element that will wrap the encrypted element * @param addEncryptedKeyInKeyInfo Need for the EncryptedKey to be placed in ds:KeyInfo * - * @return - * * @throws ProcessingException */ public static void encryptElement(QName elementQName, Document document, PublicKey publicKey, SecretKey secretKey, @@ -187,7 +123,7 @@ public class XMLEncryptionUtil { if (document == null) throw logger.nullArgumentError("document"); String wrappingElementPrefix = wrappingElementQName.getPrefix(); - if (wrappingElementPrefix == null || wrappingElementPrefix == "") + if (wrappingElementPrefix == null || "".equals(wrappingElementPrefix)) throw logger.wrongTypeError("Wrapping element prefix invalid"); Element documentElement = DocumentUtil.getElement(document, elementQName); @@ -217,18 +153,22 @@ public class XMLEncryptionUtil { // The EncryptedKey element is added Element encryptedKeyElement = cipher.martial(document, encryptedKey); - String wrappingElementName = wrappingElementPrefix + ":" + wrappingElementQName.getLocalPart(); - - // Create the wrapping element and set its attribute NS - Element wrappingElement = encryptedDoc.createElementNS(wrappingElementQName.getNamespaceURI(), wrappingElementName); + final String wrappingElementName; if (StringUtil.isNullOrEmpty(wrappingElementPrefix)) { wrappingElementName = wrappingElementQName.getLocalPart(); + } else { + wrappingElementName = wrappingElementPrefix + ":" + wrappingElementQName.getLocalPart(); + } + // Create the wrapping element and set its attribute NS + Element wrappingElement = encryptedDoc.createElementNS(wrappingElementQName.getNamespaceURI(), wrappingElementName); + + if (! StringUtil.isNullOrEmpty(wrappingElementPrefix)) { + wrappingElement.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:" + wrappingElementPrefix, wrappingElementQName.getNamespaceURI()); } - wrappingElement.setAttributeNS(XMLNS, "xmlns:" + wrappingElementPrefix, wrappingElementQName.getNamespaceURI()); // Get Hold of the Cipher Data - NodeList cipherElements = encryptedDoc.getElementsByTagNameNS(XMLENC_NS, "EncryptedData"); + NodeList cipherElements = encryptedDoc.getElementsByTagNameNS(EncryptionConstants.EncryptionSpecNS, EncryptionConstants._TAG_ENCRYPTEDDATA); if (cipherElements == null || cipherElements.getLength() == 0) throw logger.domMissingElementError("xenc:EncryptedData"); Element encryptedDataElement = (Element) cipherElements.item(0); @@ -240,12 +180,12 @@ public class XMLEncryptionUtil { if (addEncryptedKeyInKeyInfo) { // Outer ds:KeyInfo Element to hold the EncryptionKey - Element sigElement = encryptedDoc.createElementNS(XMLSIG_NS, DS_KEY_INFO); - sigElement.setAttributeNS(XMLNS, "xmlns:ds", XMLSIG_NS); + Element sigElement = encryptedDoc.createElementNS(XMLSignature.XMLNS, DS_KEY_INFO); + sigElement.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:ds", XMLSignature.XMLNS); sigElement.appendChild(encryptedKeyElement); // Insert the Encrypted key before the CipherData element - NodeList nodeList = encryptedDoc.getElementsByTagNameNS(XMLENC_NS, CIPHER_DATA_LOCALNAME); + NodeList nodeList = encryptedDoc.getElementsByTagNameNS(EncryptionConstants.EncryptionSpecNS, EncryptionConstants._TAG_CIPHERDATA); if (nodeList == null || nodeList.getLength() == 0) throw logger.domMissingElementError("xenc:CipherData"); Element cipherDataElement = (Element) nodeList.item(0); @@ -328,12 +268,12 @@ public class XMLEncryptionUtil { Element encryptedKeyElement = cipher.martial(document, encryptedKey); // Outer ds:KeyInfo Element to hold the EncryptionKey - Element sigElement = encryptedDoc.createElementNS(XMLSIG_NS, DS_KEY_INFO); - sigElement.setAttributeNS(XMLNS, "xmlns:ds", XMLSIG_NS); + Element sigElement = encryptedDoc.createElementNS(XMLSignature.XMLNS, DS_KEY_INFO); + sigElement.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:ds", XMLSignature.XMLNS); sigElement.appendChild(encryptedKeyElement); // Insert the Encrypted key before the CipherData element - NodeList nodeList = encryptedDoc.getElementsByTagNameNS(XMLENC_NS, CIPHER_DATA_LOCALNAME); + NodeList nodeList = encryptedDoc.getElementsByTagNameNS(EncryptionConstants.EncryptionSpecNS, EncryptionConstants._TAG_CIPHERDATA); if (nodeList == null || nodeList.getLength() == 0) throw logger.domMissingElementError("xenc:CipherData"); Element cipherDataElement = (Element) nodeList.item(0); @@ -342,7 +282,7 @@ public class XMLEncryptionUtil { } /** - * Encrypt the root document element inside a Document. NOTE: The document root element will be replaced by + * Encrypt the root document element inside a Document. NOTE: The document root element will be replaced by * the * wrapping element. * @@ -361,7 +301,7 @@ public class XMLEncryptionUtil { public static Element encryptElementInDocument(Document document, PublicKey publicKey, SecretKey secretKey, int keySize, QName wrappingElementQName, boolean addEncryptedKeyInKeyInfo) throws ProcessingException, ConfigurationException { String wrappingElementPrefix = wrappingElementQName.getPrefix(); - if (wrappingElementPrefix == null || wrappingElementPrefix == "") + if (wrappingElementPrefix == null || "".equals(wrappingElementPrefix)) throw logger.wrongTypeError("Wrapping element prefix invalid"); XMLCipher cipher = null; @@ -386,15 +326,19 @@ public class XMLEncryptionUtil { // The EncryptedKey element is added Element encryptedKeyElement = cipher.martial(document, encryptedKey); - String wrappingElementName = wrappingElementPrefix + ":" + wrappingElementQName.getLocalPart(); - - // Create the wrapping element and set its attribute NS - Element wrappingElement = encryptedDoc.createElementNS(wrappingElementQName.getNamespaceURI(), wrappingElementName); + final String wrappingElementName; if (StringUtil.isNullOrEmpty(wrappingElementPrefix)) { wrappingElementName = wrappingElementQName.getLocalPart(); + } else { + wrappingElementName = wrappingElementPrefix + ":" + wrappingElementQName.getLocalPart(); + } + // Create the wrapping element and set its attribute NS + Element wrappingElement = encryptedDoc.createElementNS(wrappingElementQName.getNamespaceURI(), wrappingElementName); + + if (! StringUtil.isNullOrEmpty(wrappingElementPrefix)) { + wrappingElement.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:" + wrappingElementPrefix, wrappingElementQName.getNamespaceURI()); } - wrappingElement.setAttributeNS(XMLNS, "xmlns:" + wrappingElementPrefix, wrappingElementQName.getNamespaceURI()); Element encryptedDocRootElement = encryptedDoc.getDocumentElement(); // Bring in the encrypted wrapping element to wrap the root node @@ -404,12 +348,12 @@ public class XMLEncryptionUtil { if (addEncryptedKeyInKeyInfo) { // Outer ds:KeyInfo Element to hold the EncryptionKey - Element sigElement = encryptedDoc.createElementNS(XMLSIG_NS, DS_KEY_INFO); - sigElement.setAttributeNS(XMLNS, "xmlns:ds", XMLSIG_NS); + Element sigElement = encryptedDoc.createElementNS(XMLSignature.XMLNS, DS_KEY_INFO); + sigElement.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:ds", XMLSignature.XMLNS); sigElement.appendChild(encryptedKeyElement); // Insert the Encrypted key before the CipherData element - NodeList nodeList = encryptedDocRootElement.getElementsByTagNameNS(XMLENC_NS, CIPHER_DATA_LOCALNAME); + NodeList nodeList = encryptedDocRootElement.getElementsByTagNameNS(EncryptionConstants.EncryptionSpecNS, EncryptionConstants._TAG_CIPHERDATA); if (nodeList == null || nodeList.getLength() == 0) throw logger.domMissingElementError("xenc:CipherData"); @@ -430,9 +374,6 @@ public class XMLEncryptionUtil { * @param privateKey key need to unwrap the encryption key * * @return the document with the encrypted element replaced by the data element - * - * @throws XMLEncryptionException - * @throws ProcessingException */ public static Element decryptElementInDocument(Document documentWithEncryptedElement, PrivateKey privateKey) throws ProcessingException { @@ -449,7 +390,7 @@ public class XMLEncryptionUtil { Element encKeyElement = getNextElementNode(encDataElement.getNextSibling()); if (encKeyElement == null) { // Search the enc data element for enc key - NodeList nodeList = encDataElement.getElementsByTagNameNS(XMLENC_NS, ENCRYPTED_KEY_LOCALNAME); + NodeList nodeList = encDataElement.getElementsByTagNameNS(EncryptionConstants.EncryptionSpecNS, EncryptionConstants._TAG_ENCRYPTEDKEY); if (nodeList == null || nodeList.getLength() == 0) throw logger.nullValueError("Encrypted Key not found in the enc data"); @@ -522,8 +463,6 @@ public class XMLEncryptionUtil { } if (publicKeyAlgo.contains("RSA")) return RSA_ENCRYPTION_SCHEME; - if (publicKeyAlgo.contains("DES")) - return XMLCipher.TRIPLEDES_KeyWrap; throw logger.unsupportedType("unsupported publicKey Algo:" + publicKeyAlgo); } @@ -548,8 +487,6 @@ public class XMLEncryptionUtil { } if (algo.contains("RSA")) return XMLCipher.RSA_v1dot5; - if (algo.contains("DES")) - return XMLCipher.TRIPLEDES_KeyWrap; throw logger.unsupportedType("Secret Key with unsupported algo:" + algo); } diff --git a/server-spi-private/src/main/java/org/keycloak/cluster/ClusterListener.java b/server-spi-private/src/main/java/org/keycloak/cluster/ClusterListener.java index 41c65c03d5..2c07377e36 100644 --- a/server-spi-private/src/main/java/org/keycloak/cluster/ClusterListener.java +++ b/server-spi-private/src/main/java/org/keycloak/cluster/ClusterListener.java @@ -29,6 +29,6 @@ public interface ClusterListener { * * @param event value of notification (Object added into the cache) */ - void run(ClusterEvent event); + void eventReceived(ClusterEvent event); } diff --git a/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java b/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java index 6c22056c99..abed174520 100644 --- a/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/cluster/ClusterProvider.java @@ -48,7 +48,8 @@ public interface ClusterProvider extends Provider { /** - * Register task (listener) under given key. When this key will be put to the cache on any cluster node, the task will be executed + * Register task (listener) under given key. When this key will be put to the cache on any cluster node, the task will be executed. + * When using {@link #ALL} as the taskKey, then listener will be always triggered for any value put into the cache. * * @param taskKey * @param task @@ -57,10 +58,18 @@ public interface ClusterProvider extends Provider { /** - * Notify registered listeners on all cluster nodes + * Notify registered listeners on all cluster nodes. It will notify listeners registered under given taskKey AND also listeners registered with {@link #ALL} key (those are always executed) * * @param taskKey * @param event + * @param ignoreSender if true, then sender node itself won't receive the notification */ - void notify(String taskKey, ClusterEvent event); + void notify(String taskKey, ClusterEvent event, boolean ignoreSender); + + + /** + * Special value to be used with {@link #registerListener} to specify that particular listener will be always triggered for all notifications + * with any key. + */ + String ALL = "ALL"; } diff --git a/server-spi/src/main/java/org/keycloak/credential/CredentialProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/credential/CredentialProviderFactory.java similarity index 100% rename from server-spi/src/main/java/org/keycloak/credential/CredentialProviderFactory.java rename to server-spi-private/src/main/java/org/keycloak/credential/CredentialProviderFactory.java diff --git a/server-spi/src/main/java/org/keycloak/credential/CredentialSpi.java b/server-spi-private/src/main/java/org/keycloak/credential/CredentialSpi.java similarity index 100% rename from server-spi/src/main/java/org/keycloak/credential/CredentialSpi.java rename to server-spi-private/src/main/java/org/keycloak/credential/CredentialSpi.java diff --git a/server-spi/src/main/java/org/keycloak/credential/hash/PasswordHashProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/credential/hash/PasswordHashProviderFactory.java similarity index 100% rename from server-spi/src/main/java/org/keycloak/credential/hash/PasswordHashProviderFactory.java rename to server-spi-private/src/main/java/org/keycloak/credential/hash/PasswordHashProviderFactory.java diff --git a/server-spi/src/main/java/org/keycloak/credential/hash/PasswordHashSpi.java b/server-spi-private/src/main/java/org/keycloak/credential/hash/PasswordHashSpi.java similarity index 100% rename from server-spi/src/main/java/org/keycloak/credential/hash/PasswordHashSpi.java rename to server-spi-private/src/main/java/org/keycloak/credential/hash/PasswordHashSpi.java diff --git a/server-spi/src/main/java/org/keycloak/credential/hash/Pbkdf2PasswordHashProvider.java b/server-spi-private/src/main/java/org/keycloak/credential/hash/Pbkdf2PasswordHashProvider.java similarity index 100% rename from server-spi/src/main/java/org/keycloak/credential/hash/Pbkdf2PasswordHashProvider.java rename to server-spi-private/src/main/java/org/keycloak/credential/hash/Pbkdf2PasswordHashProvider.java diff --git a/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java b/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java index 7bc1299764..61ae1beabc 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java @@ -27,12 +27,12 @@ public interface CacheRealmProvider extends RealmProvider { void clear(); RealmProvider getDelegate(); - void registerRealmInvalidation(String id); + void registerRealmInvalidation(String id, String name); - void registerClientInvalidation(String id); + void registerClientInvalidation(String id, String clientId, String realmId); void registerClientTemplateInvalidation(String id); - void registerRoleInvalidation(String id); + void registerRoleInvalidation(String id, String roleName, String roleContainerId); void registerGroupInvalidation(String id); } diff --git a/server-spi/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java b/server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java similarity index 100% rename from server-spi/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java rename to server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java diff --git a/server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProviderFactory.java index 2c0b98c514..350f4f977e 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProviderFactory.java +++ b/server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProviderFactory.java @@ -17,10 +17,32 @@ package org.keycloak.models.session; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.UserModel; +import org.keycloak.provider.ProviderEvent; +import org.keycloak.provider.ProviderEventListener; import org.keycloak.provider.ProviderFactory; /** * @author Marek Posolda */ public interface UserSessionPersisterProviderFactory extends ProviderFactory { + + @Override + default void postInit(KeycloakSessionFactory factory) { + factory.register(new ProviderEventListener() { + + @Override + public void onEvent(ProviderEvent event) { + if (event instanceof UserModel.UserRemovedEvent) { + UserModel.UserRemovedEvent userRemovedEvent = (UserModel.UserRemovedEvent) event; + + UserSessionPersisterProvider provider = userRemovedEvent.getKeycloakSession().getProvider(UserSessionPersisterProvider.class, getId()); + provider.onUserRemoved(userRemovedEvent.getRealm(), userRemovedEvent.getUser()); + } + } + + }); + } + } diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java index e9df047418..09720a7b5f 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java @@ -351,8 +351,6 @@ public interface RealmModel extends RoleContainerModel { void setNotBefore(int notBefore); - boolean removeRoleById(String id); - boolean isEventsEnabled(); void setEventsEnabled(boolean enabled); @@ -397,13 +395,6 @@ public interface RealmModel extends RoleContainerModel { GroupModel createGroup(String name); GroupModel createGroup(String id, String name); - /** - * Move Group to top realm level. Basically just sets group parent to null. You need to call this though - * to make sure caches are set properly - * - * @param subGroup - */ - void addTopLevelGroup(GroupModel subGroup); GroupModel getGroupById(String id); List getGroups(); List getTopLevelGroups(); diff --git a/server-spi/src/main/java/org/keycloak/models/RoleMapperModel.java b/server-spi/src/main/java/org/keycloak/models/RoleMapperModel.java index 85f1fd3dfa..46c31988b4 100755 --- a/server-spi/src/main/java/org/keycloak/models/RoleMapperModel.java +++ b/server-spi/src/main/java/org/keycloak/models/RoleMapperModel.java @@ -24,8 +24,17 @@ import java.util.Set; * @version $Revision: 1 $ */ public interface RoleMapperModel { + /** + * Returns set of realm roles that are directly set to this object. + * @return see description + */ Set getRealmRoleMappings(); + /** + * Returns set of client roles that are directly set to this object for the given client. + * @param app Client to get the roles for + * @return see description + */ Set getClientRoleMappings(ClientModel app); /** @@ -48,7 +57,15 @@ public interface RoleMapperModel { */ void grantRole(RoleModel role); + /** + * Returns set of all role (both realm all client) that are directly set to this object. + * @return + */ Set getRoleMappings(); + /** + * Removes the given role mapping from this object. + * @param role Role to remove + */ void deleteRoleMapping(RoleModel role); } diff --git a/server-spi/src/main/java/org/keycloak/models/UserManager.java b/server-spi/src/main/java/org/keycloak/models/UserManager.java index 81b2b51a98..d606dfc917 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserManager.java +++ b/server-spi/src/main/java/org/keycloak/models/UserManager.java @@ -17,8 +17,6 @@ package org.keycloak.models; -import org.keycloak.models.session.UserSessionPersisterProvider; - /** * @author Stian Thorgersen */ @@ -35,17 +33,25 @@ public class UserManager { } public boolean removeUser(RealmModel realm, UserModel user, UserProvider userProvider) { - UserSessionProvider sessions = session.sessions(); - if (sessions != null) { - sessions.onUserRemoved(realm, user); - } - - UserSessionPersisterProvider sessionsPersister = session.getProvider(UserSessionPersisterProvider.class); - if (sessionsPersister != null) { - sessionsPersister.onUserRemoved(realm, user); - } - if (userProvider.removeUser(realm, user)) { + session.getKeycloakSessionFactory().publish(new UserModel.UserRemovedEvent() { + + @Override + public RealmModel getRealm() { + return realm; + } + + @Override + public UserModel getUser() { + return user; + } + + @Override + public KeycloakSession getKeycloakSession() { + return session; + } + + }); return true; } return false; diff --git a/server-spi/src/main/java/org/keycloak/models/UserModel.java b/server-spi/src/main/java/org/keycloak/models/UserModel.java index 233c8a8614..15cc296ebd 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserModel.java +++ b/server-spi/src/main/java/org/keycloak/models/UserModel.java @@ -35,6 +35,7 @@ public interface UserModel extends RoleMapperModel { String LOCALE = "locale"; interface UserRemovedEvent extends ProviderEvent { + RealmModel getRealm(); UserModel getUser(); KeycloakSession getKeycloakSession(); } diff --git a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java index 585558c102..4102de1f32 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java @@ -55,7 +55,6 @@ public interface UserSessionProvider extends Provider { void onRealmRemoved(RealmModel realm); void onClientRemoved(RealmModel realm, ClientModel client); - void onUserRemoved(RealmModel realm, UserModel user); UserSessionModel createOfflineUserSession(UserSessionModel userSession); UserSessionModel getOfflineUserSession(RealmModel realm, String userSessionId); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ConditionalOtpFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ConditionalOtpFormAuthenticator.java index ef17476e33..1696a1d920 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ConditionalOtpFormAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ConditionalOtpFormAuthenticator.java @@ -18,6 +18,9 @@ package org.keycloak.authentication.authenticators.browser; import org.keycloak.authentication.AuthenticationFlowContext; +import org.keycloak.models.AuthenticatorConfigModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.RoleUtils; @@ -106,15 +109,15 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator { Map config = context.getAuthenticatorConfig().getConfig(); - if (tryConcludeBasedOn(voteForUserOtpControlAttribute(context, config), context)) { + if (tryConcludeBasedOn(voteForUserOtpControlAttribute(context.getUser(), config), context)) { return; } - if (tryConcludeBasedOn(voteForUserRole(context, config), context)) { + if (tryConcludeBasedOn(voteForUserRole(context.getRealm(), context.getUser(), config), context)) { return; } - if (tryConcludeBasedOn(voteForHttpHeaderMatchesPattern(context, config), context)) { + if (tryConcludeBasedOn(voteForHttpHeaderMatchesPattern(context.getHttpRequest().getHttpHeaders().getRequestHeaders(), config), context)) { return; } @@ -158,11 +161,26 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator { } } + private boolean tryConcludeBasedOn(OtpDecision state) { + + switch (state) { + + case SHOW_OTP: + return true; + + case SKIP_OTP: + return false; + + default: + return false; + } + } + private void showOtpForm(AuthenticationFlowContext context) { super.authenticate(context); } - private OtpDecision voteForUserOtpControlAttribute(AuthenticationFlowContext context, Map config) { + private OtpDecision voteForUserOtpControlAttribute(UserModel user, Map config) { if (!config.containsKey(OTP_CONTROL_USER_ATTRIBUTE)) { return ABSTAIN; @@ -173,7 +191,7 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator { return ABSTAIN; } - List values = context.getUser().getAttribute(attributeName); + List values = user.getAttribute(attributeName); if (values.isEmpty()) { return ABSTAIN; @@ -191,14 +209,12 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator { } } - private OtpDecision voteForHttpHeaderMatchesPattern(AuthenticationFlowContext context, Map config) { + private OtpDecision voteForHttpHeaderMatchesPattern(MultivaluedMap requestHeaders, Map config) { if (!config.containsKey(FORCE_OTP_FOR_HTTP_HEADER) && !config.containsKey(SKIP_OTP_FOR_HTTP_HEADER)) { return ABSTAIN; } - MultivaluedMap requestHeaders = context.getHttpRequest().getHttpHeaders().getRequestHeaders(); - //Inverted to allow white-lists, e.g. for specifying trusted remote hosts: X-Forwarded-Host: (1.2.3.4|1.2.3.5) if (containsMatchingRequestHeader(requestHeaders, config.get(SKIP_OTP_FOR_HTTP_HEADER))) { return SKIP_OTP; @@ -238,32 +254,62 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator { return false; } - private OtpDecision voteForUserRole(AuthenticationFlowContext context, Map config) { + private OtpDecision voteForUserRole(RealmModel realm, UserModel user, Map config) { if (!config.containsKey(SKIP_OTP_ROLE) && !config.containsKey(FORCE_OTP_ROLE)) { return ABSTAIN; } - if (userHasRole(context, config.get(SKIP_OTP_ROLE))) { + if (userHasRole(realm, user, config.get(SKIP_OTP_ROLE))) { return SKIP_OTP; } - if (userHasRole(context, config.get(FORCE_OTP_ROLE))) { + if (userHasRole(realm, user, config.get(FORCE_OTP_ROLE))) { return SHOW_OTP; } return ABSTAIN; } - private boolean userHasRole(AuthenticationFlowContext context, String roleName) { + private boolean userHasRole(RealmModel realm, UserModel user, String roleName) { if (roleName == null) { return false; } - RoleModel role = getRoleFromString(context.getRealm(), roleName); - UserModel user = context.getUser(); + RoleModel role = getRoleFromString(realm, roleName); return RoleUtils.hasRole(user.getRoleMappings(), role); } + + private boolean isOTPRequired(KeycloakSession session, RealmModel realm, UserModel user) { + MultivaluedMap requestHeaders = session.getContext().getRequestHeaders().getRequestHeaders(); + for (AuthenticatorConfigModel configModel : realm.getAuthenticatorConfigs()) { + + if (tryConcludeBasedOn(voteForUserOtpControlAttribute(user, configModel.getConfig()))) { + return true; + } + if (tryConcludeBasedOn(voteForUserRole(realm, user, configModel.getConfig()))) { + return true; + } + if (tryConcludeBasedOn(voteForHttpHeaderMatchesPattern(requestHeaders, configModel.getConfig()))) { + return true; + } + if (configModel.getConfig().get(DEFAULT_OTP_OUTCOME) != null + && configModel.getConfig().get(DEFAULT_OTP_OUTCOME).equals(FORCE) + && configModel.getConfig().size() <= 1) { + return true; + } + } + return false; + } + + @Override + public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { + if (!isOTPRequired(session, realm, user)) { + user.removeRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP); + } else if (!user.getRequiredActions().contains(UserModel.RequiredAction.CONFIGURE_TOTP.name())) { + user.addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP.name()); + } + } } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java index 9df33fca68..91266895db 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java @@ -37,8 +37,6 @@ import javax.ws.rs.core.Response; * @version $Revision: 1 $ */ public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator implements Authenticator { - public static final String TOTP_FORM_ACTION = "totp"; - @Override public void action(AuthenticationFlowContext context) { validateOTP(context); @@ -99,8 +97,6 @@ public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator impl } - - @Override public void close() { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java index 5d2d054fbd..605047f7f4 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java @@ -75,7 +75,7 @@ public class LoginStatusIframeEndpoint { if (client != null) { Set validWebOrigins = WebOriginsUtils.resolveValidWebOrigins(uriInfo, client); validWebOrigins.add(UriUtils.getOrigin(uriInfo.getRequestUri())); - if (validWebOrigins.contains(origin)) { + if (validWebOrigins.contains("*") || validWebOrigins.contains(origin)) { return Response.noContent().build(); } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java index de4d0548b3..4de3720422 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java @@ -17,18 +17,19 @@ package org.keycloak.protocol.oidc.mappers; -import org.keycloak.models.ClientSessionModel; -import org.keycloak.models.KeycloakSession; +import org.keycloak.models.GroupModel; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; -import org.keycloak.representations.AccessToken; import org.keycloak.representations.IDToken; import java.util.ArrayDeque; import java.util.Deque; -import java.util.LinkedHashSet; import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Base class for mapping of user role mappings to an ID and Access Token claim. @@ -38,39 +39,95 @@ import java.util.Set; abstract class AbstractUserRoleMappingMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper { /** - * Returns the role names extracted from the given {@code roleModels} while recursively traversing "Composite Roles". - *

- * Optionally prefixes each role name with the given {@code prefix}. - *

- * - * @param roleModels - * @param prefix the prefix to apply, may be {@literal null} + * Returns a stream with roles that come from: + *
    + *
  • Direct assignment of the role to the user
  • + *
  • Direct assignment of the role to any group of the user or any of its parent group
  • + *
  • Composite roles are expanded recursively, the composite role itself is also contained in the returned stream
  • + *
+ * @param user User to enumerate the roles for * @return */ - protected Set flattenRoleModelToRoleNames(Set roleModels, String prefix) { + public static Stream getAllUserRolesStream(UserModel user) { + return Stream.concat( + user.getRoleMappings().stream(), + user.getGroups().stream() + .flatMap(g -> groupAndItsParentsStream(g)) + .flatMap(g -> g.getRoleMappings().stream())) + .flatMap(role -> expandCompositeRolesStream(role)); + } - Set roleNames = new LinkedHashSet<>(); + /** + * Returns stream of the given group and its parents (recursively). + * @param group + * @return + */ + private static Stream groupAndItsParentsStream(GroupModel group) { + Stream.Builder sb = Stream.builder(); + while (group != null) { + sb.add(group); + group = group.getParent(); + } + return sb.build(); + } - Deque stack = new ArrayDeque<>(roleModels); - while (!stack.isEmpty()) { + /** + * Recursively expands composite roles into their composite. + * @param role + * @return Stream of containing all of the composite roles and their components. + */ + private static Stream expandCompositeRolesStream(RoleModel role) { + Stream.Builder sb = Stream.builder(); + Deque stack = new ArrayDeque<>(); + stack.add(role); + + while (! stack.isEmpty()) { RoleModel current = stack.pop(); + sb.add(current); if (current.isComposite()) { - for (RoleModel compositeRoleModel : current.getComposites()) { - stack.push(compositeRoleModel); - } + stack.addAll(current.getComposites()); } - - String roleName = current.getName(); - - if (prefix != null && !prefix.trim().isEmpty()) { - roleName = prefix.trim() + roleName; - } - - roleNames.add(roleName); } - return roleNames; + return sb.build(); + } + + /** + * Retrieves all roles of the current user based on direct roles set to the user, its groups and their parent groups. + * Then it recursively expands all composite roles, and restricts according to the given predicate {@code restriction}. + * If the current client sessions is restricted (i.e. no client found in active user session has full scope allowed), + * the final list of roles is also restricted by the client scope. Finally, the list is mapped to the token into + * a claim. + * + * @param token + * @param mappingModel + * @param userSession + * @param restriction + * @param prefix + */ + protected static void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, + Predicate restriction, String prefix) { + String rolePrefix = prefix == null ? "" : prefix; + UserModel user = userSession.getUser(); + + // get a set of all realm roles assigned to the user or its group + Stream clientUserRoles = getAllUserRolesStream(user).filter(restriction); + + boolean dontLimitScope = userSession.getClientSessions().stream().anyMatch(cs -> cs.getClient().isFullScopeAllowed()); + if (! dontLimitScope) { + Set clientRoles = userSession.getClientSessions().stream() + .flatMap(cs -> cs.getClient().getScopeMappings().stream()) + .collect(Collectors.toSet()); + + clientUserRoles = clientUserRoles.filter(clientRoles::contains); + } + + Set realmRoleNames = clientUserRoles + .map(m -> rolePrefix + m.getName()) + .collect(Collectors.toSet()); + + OIDCAttributeMapperHelper.mapClaim(token, mappingModel, realmRoleNames); } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java index 99b261074b..8b64aef891 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java @@ -100,6 +100,9 @@ public class OIDCAttributeMapperHelper { if (attributeValue == null) return; String protocolClaim = mappingModel.getConfig().get(TOKEN_CLAIM_NAME); + if (protocolClaim == null) { + return; + } String[] split = protocolClaim.split("\\."); Map jsonObject = token.getOtherClaims(); for (int i = 0; i < split.length; i++) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserClientRoleMappingMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserClientRoleMappingMapper.java index 01d47e13d7..5a88c2add8 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserClientRoleMappingMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserClientRoleMappingMapper.java @@ -18,17 +18,20 @@ package org.keycloak.protocol.oidc.mappers; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; -import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.IDToken; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.function.Predicate; /** * Allows mapping of user client role mappings to an ID and Access Token claim. @@ -39,7 +42,7 @@ public class UserClientRoleMappingMapper extends AbstractUserRoleMappingMapper { public static final String PROVIDER_ID = "oidc-usermodel-client-role-mapper"; - private static final List CONFIG_PROPERTIES = new ArrayList(); + private static final List CONFIG_PROPERTIES = new ArrayList<>(); static { @@ -60,6 +63,7 @@ public class UserClientRoleMappingMapper extends AbstractUserRoleMappingMapper { OIDCAttributeMapperHelper.addAttributeConfig(CONFIG_PROPERTIES, UserClientRoleMappingMapper.class); } + @Override public List getConfigProperties() { return CONFIG_PROPERTIES; } @@ -84,23 +88,51 @@ public class UserClientRoleMappingMapper extends AbstractUserRoleMappingMapper { return "Map a user client role to a token claim."; } + @Override protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) { - - UserModel user = userSession.getUser(); - String clientId = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID); - if (clientId != null) { + String rolePrefix = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_ROLE_PREFIX); - ClientModel clientModel = userSession.getRealm().getClientByClientId(clientId.trim()); - Set clientRoleMappings = user.getClientRoleMappings(clientModel); - - String rolePrefix = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_ROLE_PREFIX); - Set clientRoleNames = flattenRoleModelToRoleNames(clientRoleMappings, rolePrefix); - - OIDCAttributeMapperHelper.mapClaim(token, mappingModel, clientRoleNames); - } + setClaim(token, mappingModel, userSession, getClientRoleFilter(clientId, userSession), rolePrefix); } + private static Predicate getClientRoleFilter(String clientId, UserSessionModel userSession) { + if (clientId == null) { + return RoleModel::isClientRole; + } + + RealmModel clientRealm = userSession.getRealm(); + ClientModel client = clientRealm.getClientByClientId(clientId.trim()); + + if (client == null) { + return RoleModel::isClientRole; + } + + ClientTemplateModel template = client.getClientTemplate(); + boolean useTemplateScope = template != null && client.useTemplateScope(); + boolean fullScopeAllowed = (useTemplateScope && template.isFullScopeAllowed()) || client.isFullScopeAllowed(); + + Set clientRoleMappings = client.getRoles(); + if (fullScopeAllowed) { + return clientRoleMappings::contains; + } + + Set scopeMappings = new HashSet<>(); + + if (useTemplateScope) { + Set templateScopeMappings = template.getScopeMappings(); + if (templateScopeMappings != null) { + scopeMappings.addAll(templateScopeMappings); + } + } + + Set clientScopeMappings = client.getScopeMappings(); + if (clientScopeMappings != null) { + scopeMappings.addAll(clientScopeMappings); + } + + return role -> clientRoleMappings.contains(role) && scopeMappings.contains(role); + } public static ProtocolMapperModel create(String clientId, String clientRolePrefix, String name, diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserRealmRoleMappingMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserRealmRoleMappingMapper.java index ef9818227b..f978b08151 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserRealmRoleMappingMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserRealmRoleMappingMapper.java @@ -18,18 +18,13 @@ package org.keycloak.protocol.oidc.mappers; import org.keycloak.models.ProtocolMapperModel; -import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.ProtocolMapperUtils; -import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.IDToken; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Set; /** * Allows mapping of user realm role mappings to an ID and Access Token claim. @@ -40,7 +35,7 @@ public class UserRealmRoleMappingMapper extends AbstractUserRoleMappingMapper { public static final String PROVIDER_ID = "oidc-usermodel-realm-role-mapper"; - private static final List CONFIG_PROPERTIES = new ArrayList(); + private static final List CONFIG_PROPERTIES = new ArrayList<>(); static { @@ -54,6 +49,7 @@ public class UserRealmRoleMappingMapper extends AbstractUserRoleMappingMapper { OIDCAttributeMapperHelper.addAttributeConfig(CONFIG_PROPERTIES, UserRealmRoleMappingMapper.class); } + @Override public List getConfigProperties() { return CONFIG_PROPERTIES; } @@ -78,17 +74,12 @@ public class UserRealmRoleMappingMapper extends AbstractUserRoleMappingMapper { return "Map a user realm role to a token claim."; } + @Override protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) { - - UserModel user = userSession.getUser(); - String rolePrefix = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_REALM_ROLE_MAPPING_ROLE_PREFIX); - Set realmRoleNames = flattenRoleModelToRoleNames(user.getRealmRoleMappings(), rolePrefix); - - OIDCAttributeMapperHelper.mapClaim(token, mappingModel, realmRoleNames); + AbstractUserRoleMappingMapper.setClaim(token, mappingModel, userSession, role -> ! role.isClientRole(), rolePrefix); } - public static ProtocolMapperModel create(String realmRolePrefix, String name, String tokenClaimName, boolean accessToken, boolean idToken) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/WebOriginsUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/WebOriginsUtils.java index f606bfc602..83f90f05a0 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/utils/WebOriginsUtils.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/WebOriginsUtils.java @@ -21,6 +21,7 @@ import org.keycloak.common.util.UriUtils; import org.keycloak.models.ClientModel; import javax.ws.rs.core.UriInfo; +import java.util.HashSet; import java.util.Set; /** @@ -31,17 +32,20 @@ public class WebOriginsUtils { public static final String INCLUDE_REDIRECTS = "+"; public static Set resolveValidWebOrigins(UriInfo uriInfo, ClientModel client) { - Set webOrigins = client.getWebOrigins(); - if (webOrigins != null && webOrigins.contains("+")) { - webOrigins.remove(INCLUDE_REDIRECTS); + Set origins = new HashSet<>(); + if (client.getWebOrigins() != null) { + origins.addAll(client.getWebOrigins()); + } + if (origins.contains("+")) { + origins.remove(INCLUDE_REDIRECTS); client.getRedirectUris(); for (String redirectUri : RedirectUtils.resolveValidRedirects(uriInfo, client.getRootUrl(), client.getRedirectUris())) { if (redirectUri.startsWith("http://") || redirectUri.startsWith("https://")) { - webOrigins.add(UriUtils.getOrigin(redirectUri)); + origins.add(UriUtils.getOrigin(redirectUri)); } } } - return webOrigins; + return origins; } } diff --git a/services/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java b/services/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java index 3d62a27eae..d3cd9041d3 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java +++ b/services/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java @@ -123,6 +123,14 @@ public class EntityDescriptorDescriptionConverter implements ClientDescriptionCo attributes.put(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE, assertionConsumerServiceRedirectBinding); redirectUris.add(assertionConsumerServiceRedirectBinding); } + String assertionConsumerServiceSoapBinding = CoreConfigUtil.getServiceURL(spDescriptorType, JBossSAMLURIConstants.SAML_SOAP_BINDING.get()); + if (assertionConsumerServiceSoapBinding != null) { + redirectUris.add(assertionConsumerServiceSoapBinding); + } + String assertionConsumerServicePaosBinding = CoreConfigUtil.getServiceURL(spDescriptorType, JBossSAMLURIConstants.SAML_PAOS_BINDING.get()); + if (assertionConsumerServicePaosBinding != null) { + redirectUris.add(assertionConsumerServicePaosBinding); + } if (spDescriptorType.getNameIDFormat() != null) { for (String format : spDescriptorType.getNameIDFormat()) { String attribute = SamlClient.samlNameIDFormatToClientAttribute(format); diff --git a/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java b/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java index 05b07f2ed4..b1114fabe2 100755 --- a/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java +++ b/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java @@ -172,7 +172,7 @@ public class UserStorageSyncManager { } UserStorageProviderClusterEvent event = UserStorageProviderClusterEvent.createEvent(removed, realm.getId(), provider); - session.getProvider(ClusterProvider.class).notify(USER_STORAGE_TASK_KEY, event); + session.getProvider(ClusterProvider.class).notify(USER_STORAGE_TASK_KEY, event, false); } @@ -282,7 +282,7 @@ public class UserStorageSyncManager { } @Override - public void run(ClusterEvent event) { + public void eventReceived(ClusterEvent event) { final UserStorageProviderClusterEvent fedEvent = (UserStorageProviderClusterEvent) event; KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { diff --git a/services/src/main/java/org/keycloak/services/managers/UsersSyncManager.java b/services/src/main/java/org/keycloak/services/managers/UsersSyncManager.java index d8c5dee5ad..21b0cad243 100755 --- a/services/src/main/java/org/keycloak/services/managers/UsersSyncManager.java +++ b/services/src/main/java/org/keycloak/services/managers/UsersSyncManager.java @@ -155,7 +155,7 @@ public class UsersSyncManager { // Ensure all cluster nodes are notified public void notifyToRefreshPeriodicSync(KeycloakSession session, RealmModel realm, UserFederationProviderModel federationProvider, boolean removed) { FederationProviderClusterEvent event = FederationProviderClusterEvent.createEvent(removed, realm.getId(), federationProvider); - session.getProvider(ClusterProvider.class).notify(FEDERATION_TASK_KEY, event); + session.getProvider(ClusterProvider.class).notify(FEDERATION_TASK_KEY, event, false); } @@ -265,7 +265,7 @@ public class UsersSyncManager { } @Override - public void run(ClusterEvent event) { + public void eventReceived(ClusterEvent event) { final FederationProviderClusterEvent fedEvent = (FederationProviderClusterEvent) event; KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java index 363d6f4774..2ea9992fd2 100644 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -51,6 +51,7 @@ import org.keycloak.services.resources.admin.AdminRoot; import org.keycloak.services.scheduled.ClearExpiredEvents; import org.keycloak.services.scheduled.ClearExpiredUserSessions; import org.keycloak.services.scheduled.ClusterAwareScheduledTaskRunner; +import org.keycloak.services.scheduled.ScheduledTaskRunner; import org.keycloak.services.util.JsonConfigProvider; import org.keycloak.services.util.ObjectMapperResolver; import org.keycloak.timer.TimerProvider; @@ -321,7 +322,7 @@ public class KeycloakApplication extends Application { try { TimerProvider timer = session.getProvider(TimerProvider.class); timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredEvents(), interval), interval, "ClearExpiredEvents"); - timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredUserSessions(), interval), interval, "ClearExpiredUserSessions"); + timer.schedule(new ScheduledTaskRunner(sessionFactory, new ClearExpiredUserSessions()), interval, "ClearExpiredUserSessions"); new UsersSyncManager().bootstrapPeriodic(sessionFactory, timer); new UserStorageSyncManager().bootstrapPeriodic(sessionFactory, timer); } finally { diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java index cd4d881cf4..2b796c54e0 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java @@ -319,7 +319,7 @@ public class AdminConsole { @Path("messages.json") @Produces(MediaType.APPLICATION_JSON) public Properties getMessages(@QueryParam("lang") String lang) { - return AdminRoot.getMessages(session, realm, "admin-messages", lang); + return AdminRoot.getMessages(session, realm, lang, "admin-messages"); } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java index b7dcddf90f..5db1ea4475 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java @@ -288,7 +288,16 @@ public class AdminRoot { } } - public static Properties getMessages(KeycloakSession session, RealmModel realm, String bundle, String lang) { + public static Properties getMessages(KeycloakSession session, RealmModel realm, String lang, String... bundles) { + Properties compound = new Properties(); + for (String bundle : bundles) { + Properties current = getMessages(session, realm, lang, bundle); + compound.putAll(current); + } + return compound; + } + + private static Properties getMessages(KeycloakSession session, RealmModel realm, String lang, String bundle) { try { Theme theme = getTheme(session, realm); Locale locale = lang != null ? Locale.forLanguageTag(lang) : Locale.ENGLISH; diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java index 087022fca3..0128425953 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java @@ -251,7 +251,11 @@ public class AuthenticationManagementResource { @NoCache public void deleteFlow(@PathParam("id") String id) { auth.requireManage(); - + + deleteFlow(id, true); + } + + private void deleteFlow(String id, boolean isTopMostLevel) { AuthenticationFlowModel flow = realm.getAuthenticationFlowById(id); if (flow == null) { throw new NotFoundException("Could not find flow with id"); @@ -259,18 +263,17 @@ public class AuthenticationManagementResource { if (flow.isBuiltIn()) { throw new BadRequestException("Can't delete built in flow"); } + List executions = realm.getAuthenticationExecutions(id); for (AuthenticationExecutionModel execution : executions) { - if(execution.getFlowId() != null) { - AuthenticationFlowModel nonTopLevelFlow = realm.getAuthenticationFlowById(execution.getFlowId()); - realm.removeAuthenticationFlow(nonTopLevelFlow); - } - realm.removeAuthenticatorExecution(execution); + if(execution.getFlowId() != null) { + deleteFlow(execution.getFlowId(), false); + } } realm.removeAuthenticationFlow(flow); // Use just one event for top-level flow. Using separate events won't work properly for flows of depth 2 or bigger - adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success(); + if (isTopMostLevel) adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success(); } /** diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java index 56c7ce74fd..d3ac358f46 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java @@ -188,7 +188,7 @@ public class ComponentResource { } private Response localizedErrorResponse(ComponentValidationException cve) { - Properties messages = AdminRoot.getMessages(session, realm, "admin-messages", auth.getAuth().getToken().getLocale()); + Properties messages = AdminRoot.getMessages(session, realm, auth.getAuth().getToken().getLocale(), "admin-messages", "messages"); Object[] localizedParameters = cve.getParameters()==null ? null : Arrays.asList(cve.getParameters()).stream().map((Object parameter) -> { diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProvidersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProvidersResource.java index 8854a7bbd1..fa1e139cc7 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProvidersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProvidersResource.java @@ -21,6 +21,7 @@ import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.common.constants.KerberosConstants; +import org.keycloak.component.ComponentModel; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; import org.keycloak.mappers.FederationConfigValidationException; @@ -60,8 +61,10 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import java.text.MessageFormat; import java.util.ArrayList; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Properties; /** @@ -263,6 +266,33 @@ public class UserFederationProvidersResource { return instanceResource; } + // TODO: This endpoint exists, so that admin console can lookup userFederation provider OR userStorage provider by federationLink. + // TODO: Endpoint should be removed once UserFederation SPI is removed as fallback is not needed anymore than + @GET + @Path("instances-with-fallback/{id}") + @Produces(MediaType.APPLICATION_JSON) + @NoCache + public Map getUserFederationInstanceWithFallback(@PathParam("id") String id) { + this.auth.requireView(); + + Map result = new HashMap<>(); + UserFederationProviderModel model = KeycloakModelUtils.findUserFederationProviderById(id, realm); + if (model != null) { + result.put("federationLinkName", model.getDisplayName()); + result.put("federationLink", "#/realms/" + realm.getName() + "/user-federation/providers/" + model.getProviderName() + "/" + model.getId()); + return result; + } else { + ComponentModel userStorage = KeycloakModelUtils.findUserStorageProviderById(id, realm); + if (userStorage != null) { + result.put("federationLinkName", userStorage.getName()); + result.put("federationLink", "#/realms/" + realm.getName() + "/user-storage/providers/" + userStorage.getProviderId() + "/" + userStorage.getId()); + return result; + } else { + throw new NotFoundException("Could not find federation provider or userStorage provider"); + } + } + } + private ConfigPropertyRepresentation toConfigPropertyRepresentation(ProviderConfigProperty prop) { return ModelToRepresentation.toRepresentation(prop); diff --git a/testsuite/integration-arquillian/pom.xml b/testsuite/integration-arquillian/pom.xml index c5adb34d02..dc30d1040e 100644 --- a/testsuite/integration-arquillian/pom.xml +++ b/testsuite/integration-arquillian/pom.xml @@ -139,12 +139,40 @@ ${migration.project.version} + + + + + maven-surefire-plugin + + + ${migrated.auth.server.version} + + + + + + test-product-migration ${migration.product.version} + + + + + maven-surefire-plugin + + + ${migrated.auth.server.version} + + + + + + diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingExportImportResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingExportImportResource.java index a9ff31d91b..56c76efbd5 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingExportImportResource.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingExportImportResource.java @@ -36,7 +36,9 @@ import static org.keycloak.exportimport.ExportImportConfig.DIR; import static org.keycloak.exportimport.ExportImportConfig.FILE; import static org.keycloak.exportimport.ExportImportConfig.PROVIDER; import static org.keycloak.exportimport.ExportImportConfig.REALM_NAME; +import static org.keycloak.exportimport.ExportImportConfig.STRATEGY; import static org.keycloak.exportimport.ExportImportConfig.USERS_PER_FILE; +import org.keycloak.exportimport.Strategy; /** * @author Marek Posolda @@ -97,6 +99,13 @@ public class TestingExportImportResource { return System.setProperty(DIR, dir); } + @PUT + @Path("/set-import-strategy") + @Consumes(MediaType.APPLICATION_JSON) + public void setStrategy(@QueryParam("importStrategy") Strategy strategy) { + System.setProperty(STRATEGY, strategy.name()); + } + @PUT @Path("/export-import-provider") @Consumes(MediaType.APPLICATION_JSON) diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java index 7dd6b244a1..81c5a533a4 100644 --- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java @@ -34,6 +34,8 @@ import java.util.Set; @Transaction public class AlbumService { + private static volatile long nextId = 0; + public static final String SCOPE_ALBUM_VIEW = "urn:photoz.com:scopes:album:view"; public static final String SCOPE_ALBUM_CREATE = "urn:photoz.com:scopes:album:create"; public static final String SCOPE_ALBUM_DELETE = "urn:photoz.com:scopes:album:delete"; @@ -53,6 +55,8 @@ public class AlbumService { @POST @Consumes("application/json") public Response create(Album newAlbum) { + newAlbum.setId(++nextId); + Principal userPrincipal = request.getUserPrincipal(); newAlbum.setUserId(userPrincipal.getName()); diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java index 978bdeabb5..cc8bea26fc 100644 --- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java @@ -23,6 +23,7 @@ import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.OneToMany; +import javax.persistence.GenerationType; import java.util.ArrayList; import java.util.List; @@ -33,7 +34,6 @@ import java.util.List; public class Album { @Id - @GeneratedValue private Long id; @Column(nullable = false) diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/migration/MigrationTestExecutionDecider.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/migration/MigrationTestExecutionDecider.java index 3b457ec81d..b8aa42c88f 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/migration/MigrationTestExecutionDecider.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/migration/MigrationTestExecutionDecider.java @@ -20,6 +20,7 @@ import org.jboss.arquillian.test.spi.execution.ExecutionDecision; import org.jboss.arquillian.test.spi.execution.TestExecutionDecider; import java.lang.reflect.Method; +import org.jboss.logging.Logger; /** * @author Vlastislav Ramik @@ -27,6 +28,7 @@ import java.lang.reflect.Method; */ public class MigrationTestExecutionDecider implements TestExecutionDecider { + private final Logger log = Logger.getLogger(MigrationTestExecutionDecider.class); private static final String MIGRATED_AUTH_SERVER_VERSION_PROPERTY = "migrated.auth.server.version"; @Override @@ -35,8 +37,10 @@ public class MigrationTestExecutionDecider implements TestExecutionDecider { String migratedAuthServerVersion = System.getProperty(MIGRATED_AUTH_SERVER_VERSION_PROPERTY); boolean migrationTest = migratedAuthServerVersion != null; Migration migrationAnnotation = method.getAnnotation(Migration.class); - - if (migrationTest && migrationAnnotation != null) { + + if (migrationTest && migrationAnnotation != null) { + log.info("migration from version: " + migratedAuthServerVersion); + String versionFrom = migrationAnnotation.versionFrom(); if (migratedAuthServerVersion.contains(versionFrom)) { diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingExportImportResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingExportImportResource.java index 27fa3604c4..0c2f106b24 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingExportImportResource.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingExportImportResource.java @@ -25,6 +25,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import org.keycloak.exportimport.Strategy; /** * @author Marek Posolda @@ -64,6 +65,11 @@ public interface TestingExportImportResource { @Produces(MediaType.APPLICATION_JSON) public String setDir(@QueryParam("dir") String dir); + @PUT + @Path("/set-import-strategy") + @Consumes(MediaType.APPLICATION_JSON) + public void setStrategy(@QueryParam("importStrategy") Strategy strategy); + @PUT @Path("/export-import-provider") @Consumes(MediaType.APPLICATION_JSON) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java index 89d5c5b36e..87950ab156 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java @@ -257,39 +257,22 @@ public abstract class AbstractKeycloakTest { adminClient.realms().create(realm); } - public void removeRealm(RealmRepresentation realm) { + public void removeRealm(String realmName) { + log.info("removing realm: " + realmName); try { - adminClient.realms().realm(realm.getRealm()).remove(); + adminClient.realms().realm(realmName).remove(); } catch (NotFoundException e) { } } + + public void removeRealm(RealmRepresentation realm) { + removeRealm(realm.getRealm()); + } public RealmsResource realmsResouce() { return adminClient.realms(); } - public void createRealm(String realm) { - try { - RealmResource realmResource = adminClient.realms().realm(realm); - // Throws NotFoundException in case the realm does not exist! Ugly but there - // does not seem to be a way to this just by asking. - RealmRepresentation realmRepresentation = realmResource.toRepresentation(); - } catch (NotFoundException ex) { - RealmRepresentation realmRepresentation = new RealmRepresentation(); - realmRepresentation.setRealm(realm); - realmRepresentation.setEnabled(true); - realmRepresentation.setRegistrationAllowed(true); - adminClient.realms().create(realmRepresentation); - -// List requiredActions = adminClient.realm(realm).flows().getRequiredActions(); -// for (RequiredActionProviderRepresentation a : requiredActions) { -// a.setEnabled(false); -// a.setDefaultAction(false); -// adminClient.realm(realm).flows().updateRequiredAction(a.getAlias(), a); -// } - } - } - /** * Creates a user in the given realm and returns its ID. * @param realm Realm name diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java index 65a2e9bf6d..90bf62bd8b 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java @@ -58,6 +58,8 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import static org.hamcrest.Matchers.*; + /** * @author Stian Thorgersen * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. @@ -756,7 +758,7 @@ public class AccountTest extends TestRealmKeycloakTest { Assert.assertTrue(applicationsPage.isCurrent()); Map apps = applicationsPage.getApplications(); - Assert.assertEquals(4, apps.size()); + Assert.assertThat(apps.keySet(), containsInAnyOrder("Account", "test-app", "test-app-scope", "third-party", "test-app-authz")); AccountApplicationsPage.AppEntry accountEntry = apps.get("Account"); Assert.assertEquals(2, accountEntry.getRolesAvailable().size()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomAuthFlowOTPTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomAuthFlowOTPTest.java index 02308420ff..e492672b18 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomAuthFlowOTPTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomAuthFlowOTPTest.java @@ -17,7 +17,6 @@ package org.keycloak.testsuite.account.custom; import org.jboss.arquillian.graphene.page.Page; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.keycloak.models.AuthenticationExecutionModel.Requirement; @@ -28,6 +27,7 @@ import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.testsuite.admin.Users; import org.keycloak.testsuite.auth.page.login.OneTimeCode; +import org.keycloak.testsuite.pages.LoginConfigTotpPage; import javax.ws.rs.core.Response; import java.util.ArrayList; @@ -35,6 +35,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.DEFAULT_OTP_OUTCOME; import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.FORCE; @@ -58,6 +59,9 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest { @Page private OneTimeCode testLoginOneTimeCodePage; + + @Page + private LoginConfigTotpPage loginConfigTotpPage; @Override public void setDefaultPageUriParameters() { @@ -69,12 +73,17 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest { @Override public void beforeTest() { super.beforeTest(); + } + + private void configureRequiredActions() { //set configure TOTP as required action to test user List requiredActions = new ArrayList<>(); requiredActions.add(CONFIGURE_TOTP.name()); testUser.setRequiredActions(requiredActions); testRealmResource().users().get(testUser.getId()).update(testUser); - + } + + private void configureOTP() { //configure OTP for test user testRealmAccountManagementPage.navigateTo(); testRealmLoginPage.form().login(testUser); @@ -83,7 +92,7 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest { testRealmLoginPage.form().totpForm().setTotp(totp.generateTOTP(totpSecret)); testRealmLoginPage.form().totpForm().submit(); testRealmAccountManagementPage.signOut(); - + //verify that user has OTP configured testUser = testRealmResource().users().get(testUser.getId()).toRepresentation(); Users.setPasswordFor(testUser, PASSWORD); @@ -92,40 +101,45 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest { @Test public void requireOTPTest() { - + updateRequirement("browser", "auth-otp-form", Requirement.REQUIRED); - testRealmAccountManagementPage.navigateTo(); testRealmLoginPage.form().login(testUser); + assertTrue(loginConfigTotpPage.isCurrent()); + + configureOTP(); + testRealmLoginPage.form().login(testUser); testRealmLoginPage.form().totpForm().waitForTotpInputFieldPresent(); - + //verify that the page is login page, not totp setup assertCurrentUrlStartsWith(testLoginOneTimeCodePage); } - + @Test public void conditionalOTPNoDefault() { + configureRequiredActions(); + configureOTP(); //prepare config - no configuration specified Map config = new HashMap<>(); setConditionalOTPForm(config); - + //test OTP is required testRealmAccountManagementPage.navigateTo(); testRealmLoginPage.form().login(testUser); testRealmLoginPage.form().totpForm().waitForTotpInputFieldPresent(); - + //verify that the page is login page, not totp setup assertCurrentUrlStartsWith(testLoginOneTimeCodePage); } - + @Test public void conditionalOTPDefaultSkip() { //prepare config - default skip Map config = new HashMap<>(); config.put(DEFAULT_OTP_OUTCOME, SKIP); - + setConditionalOTPForm(config); - + //test OTP is skipped testRealmAccountManagementPage.navigateTo(); testRealmLoginPage.form().login(testUser); @@ -134,6 +148,7 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest { @Test public void conditionalOTPDefaultForce() { + //prepare config - default force Map config = new HashMap<>(); config.put(DEFAULT_OTP_OUTCOME, FORCE); @@ -143,8 +158,12 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest { //test OTP is forced testRealmAccountManagementPage.navigateTo(); testRealmLoginPage.form().login(testUser); + assertTrue(loginConfigTotpPage.isCurrent()); + + configureOTP(); + testRealmLoginPage.form().login(testUser); testRealmLoginPage.form().totpForm().waitForTotpInputFieldPresent(); - + //verify that the page is login page, not totp setup assertCurrentUrlStartsWith(testLoginOneTimeCodePage); } @@ -155,48 +174,54 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest { Map config = new HashMap<>(); config.put(OTP_CONTROL_USER_ATTRIBUTE, "userSkipAttribute"); config.put(DEFAULT_OTP_OUTCOME, FORCE); - + setConditionalOTPForm(config); //add skip user attribute to user testUser.singleAttribute("userSkipAttribute", "skip"); testRealmResource().users().get(testUser.getId()).update(testUser); - + //test OTP is skipped testRealmAccountManagementPage.navigateTo(); testRealmLoginPage.form().login(testUser); + assertCurrentUrlStartsWith(testRealmAccountManagementPage); } - + @Test public void conditionalOTPUserAttributeForce() { + //prepare config - user attribute, default to skip Map config = new HashMap<>(); config.put(OTP_CONTROL_USER_ATTRIBUTE, "userSkipAttribute"); config.put(DEFAULT_OTP_OUTCOME, SKIP); - + setConditionalOTPForm(config); //add force user attribute to user testUser.singleAttribute("userSkipAttribute", "force"); testRealmResource().users().get(testUser.getId()).update(testUser); - + //test OTP is required testRealmAccountManagementPage.navigateTo(); testRealmLoginPage.form().login(testUser); + assertTrue(loginConfigTotpPage.isCurrent()); + + configureOTP(); + testRealmLoginPage.form().login(testUser); testRealmLoginPage.form().totpForm().waitForTotpInputFieldPresent(); - + //verify that the page is login page, not totp setup assertCurrentUrlStartsWith(testLoginOneTimeCodePage); } - + @Test public void conditionalOTPRoleSkip() { //prepare config - role, default to force Map config = new HashMap<>(); config.put(SKIP_OTP_ROLE, "otp_role"); config.put(DEFAULT_OTP_OUTCOME, FORCE); - + setConditionalOTPForm(config); //create role @@ -208,20 +233,20 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest { List realmRoles = new ArrayList<>(); realmRoles.add(role); testRealmResource().users().get(testUser.getId()).roles().realmLevel().add(realmRoles); - + //test OTP is skipped testRealmAccountManagementPage.navigateTo(); testRealmLoginPage.form().login(testUser); assertCurrentUrlStartsWith(testRealmAccountManagementPage); } - + @Test public void conditionalOTPRoleForce() { //prepare config - role, default to skip Map config = new HashMap<>(); config.put(FORCE_OTP_ROLE, "otp_role"); config.put(DEFAULT_OTP_OUTCOME, SKIP); - + setConditionalOTPForm(config); //create role @@ -233,16 +258,21 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest { List realmRoles = new ArrayList<>(); realmRoles.add(role); testRealmResource().users().get(testUser.getId()).roles().realmLevel().add(realmRoles); - + //test OTP is required testRealmAccountManagementPage.navigateTo(); testRealmLoginPage.form().login(testUser); + + assertTrue(loginConfigTotpPage.isCurrent()); + + configureOTP(); + testRealmLoginPage.form().login(testUser); testRealmLoginPage.form().totpForm().waitForTotpInputFieldPresent(); - + //verify that the page is login page, not totp setup assertCurrentUrlStartsWith(testLoginOneTimeCodePage); } - + @Test public void conditionalOTPRequestHeaderSkip() { //prepare config - request header skip, default to force @@ -250,7 +280,7 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest { String port = System.getProperty("auth.server.http.port", "8180"); config.put(SKIP_OTP_FOR_HTTP_HEADER, "Host: localhost:" + port); config.put(DEFAULT_OTP_OUTCOME, FORCE); - + setConditionalOTPForm(config); //test OTP is skipped @@ -258,7 +288,7 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest { testRealmLoginPage.form().login(testUser); assertCurrentUrlStartsWith(testRealmAccountManagementPage); } - + @Test public void conditionalOTPRequestHeaderForce() { //prepare config - equest header force, default to skip @@ -266,18 +296,22 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest { String port = System.getProperty("auth.server.http.port", "8180"); config.put(FORCE_OTP_FOR_HTTP_HEADER, "Host: localhost:" + port); config.put(DEFAULT_OTP_OUTCOME, SKIP); - + setConditionalOTPForm(config); //test OTP is required testRealmAccountManagementPage.navigateTo(); testRealmLoginPage.form().login(testUser); + assertEquals(driver.getTitle(), "Mobile Authenticator Setup"); + + configureOTP(); + testRealmLoginPage.form().login(testUser); testRealmLoginPage.form().totpForm().waitForTotpInputFieldPresent(); - + //verify that the page is login page, not totp setup assertCurrentUrlStartsWith(testLoginOneTimeCodePage); } - + private void setConditionalOTPForm(Map config) { String flowAlias = "ConditionalOTPFlow"; String provider = "auth-conditional-otp-form"; @@ -291,7 +325,7 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest { flow.setBuiltIn(false); Response response = getAuthMgmtResource().createFlow(flow); - Assert.assertEquals(flowAlias + " create success", 201, response.getStatus()); + assertEquals(flowAlias + " create success", 201, response.getStatus()); response.close(); //add execution - username-password form @@ -322,10 +356,10 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest { AuthenticatorConfigRepresentation authConfig = new AuthenticatorConfigRepresentation(); authConfig.setAlias("Config alias"); authConfig.setConfig(config); - + //add auth config to the execution response = getAuthMgmtResource().newExecutionConfig(executionId, authConfig); - Assert.assertEquals("new execution success", 201, response.getStatus()); + assertEquals("new execution success", 201, response.getStatus()); response.close(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java index 81af8c6843..545fbff0f1 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java @@ -817,7 +817,7 @@ public class UserTest extends AbstractAdminTest { // List realm roles assertNames(roles.realmLevel().listAll(), "realm-role", "realm-composite", "user", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION); - assertNames(roles.realmLevel().listAvailable(), "admin", "customer-user-premium"); + assertNames(roles.realmLevel().listAvailable(), "admin", "customer-user-premium", "realm-composite-role", "sample-realm-role"); assertNames(roles.realmLevel().listEffective(), "realm-role", "realm-composite", "realm-child", "user", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION); // List client roles diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/FlowTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/FlowTest.java index 90f88745a9..bce911703f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/FlowTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/FlowTest.java @@ -37,6 +37,33 @@ import java.util.Map; */ public class FlowTest extends AbstractAuthenticationTest { + // KEYCLOAK-3681: Delete top flow doesn't delete all subflows + @Test + public void testRemoveSubflows() { + authMgmtResource.createFlow(newFlow("Foo", "Foo flow", "generic", true, false)); + addFlowToParent("Foo", "child"); + addFlowToParent("child", "grandchild"); + + List flows = authMgmtResource.getFlows(); + AuthenticationFlowRepresentation found = findFlowByAlias("Foo", flows); + authMgmtResource.deleteFlow(found.getId()); + + authMgmtResource.createFlow(newFlow("Foo", "Foo flow", "generic", true, false)); + addFlowToParent("Foo", "child"); + + // Under the old code, this would throw an error because "grandchild" + // was left in the database + addFlowToParent("child", "grandchild"); + } + + private void addFlowToParent(String parentAlias, String childAlias) { + Map data = new HashMap<>(); + data.put("alias", childAlias); + data.put("type", "generic"); + data.put("description", childAlias + " flow"); + authMgmtResource.addExecutionFlow(parentAlias, data); + } + @Test public void testAddRemoveFlow() { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java index 7e61f511db..1f0274e9cb 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java @@ -438,7 +438,7 @@ public class GroupTest extends AbstractGroupTest { // List realm roles assertNames(roles.realmLevel().listAll(), "realm-role", "realm-composite"); - assertNames(roles.realmLevel().listAvailable(), "admin", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION, "user", "customer-user-premium"); + assertNames(roles.realmLevel().listAvailable(), "admin", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION, "user", "customer-user-premium", "realm-composite-role", "sample-realm-role"); assertNames(roles.realmLevel().listEffective(), "realm-role", "realm-composite", "realm-child"); // List client roles diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/SAMLClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/SAMLClientRegistrationTest.java index c3e60a258c..6efbe9adcd 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/SAMLClientRegistrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/SAMLClientRegistrationTest.java @@ -28,8 +28,8 @@ import org.keycloak.representations.idm.ClientRepresentation; import java.io.IOException; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.hamcrest.Matchers.*; /** * @author Stian Thorgersen @@ -49,10 +49,14 @@ public class SAMLClientRegistrationTest extends AbstractClientRegistrationTest { String entityDescriptor = IOUtils.toString(getClass().getResourceAsStream("/clientreg-test/saml-entity-descriptor.xml")); ClientRepresentation response = reg.saml().create(entityDescriptor); - assertNotNull(response.getRegistrationAccessToken()); - assertEquals("loadbalancer-9.siroe.com", response.getClientId()); - assertEquals(1, response.getRedirectUris().size()); - assertEquals("https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp", response.getRedirectUris().get(0)); + assertThat(response.getRegistrationAccessToken(), notNullValue()); + assertThat(response.getClientId(), is("loadbalancer-9.siroe.com")); + assertThat(response.getRedirectUris(), containsInAnyOrder( + "https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/post", + "https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/soap", + "https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/paos", + "https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/redirect" + )); // No redirect URI for ARTIFACT binding which is unsupported } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java index 90dda5935d..2abf0447d8 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java @@ -141,11 +141,7 @@ public class ExportImportTest extends AbstractExportImportTest { ExportImportUtil.assertDataImportedInRealm(adminClient, testingClient, testRealmRealm.toRepresentation()); } - - private void removeRealm(String realmName) { - adminClient.realm(realmName).remove(); - } - + private void testFullExportImport() throws LifecycleException { testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_EXPORT); testingClient.testing().exportImport().setRealmName(""); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/LegacyImportTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/LegacyImportTest.java index 45808a3a5a..e7ec574c32 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/LegacyImportTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/LegacyImportTest.java @@ -20,7 +20,6 @@ package org.keycloak.testsuite.exportimport; import org.jboss.arquillian.container.spi.client.container.LifecycleException; import org.junit.After; import org.junit.Assert; -import org.junit.Ignore; import org.junit.Test; import org.keycloak.Config; import org.keycloak.admin.client.resource.ClientResource; @@ -39,6 +38,11 @@ import java.net.URL; import java.util.Collection; import java.util.List; import java.util.Set; +import static org.junit.Assert.assertNotNull; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.exportimport.Strategy; +import static org.keycloak.testsuite.Assert.assertNames; +import static org.keycloak.testsuite.migration.MigrationTest.MIGRATION; /** * Test importing JSON files exported from previous adminClient versions @@ -57,46 +61,82 @@ public class LegacyImportTest extends AbstractExportImportTest { public void addTestRealms(List testRealms) { } + @Test + public void importPreviousProject() throws Exception { - @Ignore // TODO: Restart and set system properties doesn't work on wildfly ATM. Figure and re-enable + String projectVersion = System.getProperty("migration.project.version"); + assertNotNull(projectVersion); + + testLegacyImport(projectVersion); + } + + @Test + public void importPreviousProduct() throws Exception { + + String productVersion = System.getProperty("migration.product.version"); + assertNotNull(productVersion); + + testLegacyImport(productVersion); + } + + private void testLegacyImport(String version) { + String file = "/migration-test/migration-realm-" + version + ".json"; + + URL url = LegacyImportTest.class.getResource(file); + String targetFilePath = new File(url.getFile()).getAbsolutePath(); + testingClient.testing().exportImport().setFile(targetFilePath); + testingClient.testing().exportImport().setProvider(SingleFileExportProviderFactory.PROVIDER_ID); + testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_IMPORT); + testingClient.testing().exportImport().setRealmName(MIGRATION); + testingClient.testing().exportImport().setStrategy(Strategy.IGNORE_EXISTING); + + try { + testingClient.testing().exportImport().runImport(); + + RealmResource imported = adminClient.realm(MIGRATION); + + assertNames(imported.roles().list(), "offline_access", "uma_authorization", "migration-test-realm-role"); + assertNames(imported.clients().findAll(), "account", "admin-cli", "broker", "migration-test-client", "realm-management", "security-admin-console"); + String id = imported.clients().findByClientId("migration-test-client").get(0).getId(); + assertNames(imported.clients().get(id).roles().list(), "migration-test-client-role"); + assertNames(imported.users().search("", 0, 5), "migration-test-user"); + assertNames(imported.groups().groups(), "migration-test-group"); + } finally { + removeRealm(MIGRATION); + } + } + + //KEYCLOAK-1982 @Test public void importFrom11() throws LifecycleException { - // Setup system properties for import ( TODO: Set properly with external-container ) - ExportImportConfig.setProvider(SingleFileExportProviderFactory.PROVIDER_ID); URL url = LegacyImportTest.class.getResource("/exportimport-test/kc11-exported-realm.json"); String targetFilePath = new File(url.getFile()).getAbsolutePath(); - ExportImportConfig.setFile(targetFilePath); - ExportImportConfig.setAction(ExportImportConfig.ACTION_IMPORT); + testingClient.testing().exportImport().setFile(targetFilePath); + testingClient.testing().exportImport().setProvider(SingleFileExportProviderFactory.PROVIDER_ID); + testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_IMPORT); - // Restart to enforce full import - restartServer(); + try { + testingClient.testing().exportImport().runImport(); + // Assert "locale" mapper available in security-admin-console client + ClientResource foo11AdminConsoleClient = adminClient.realm("foo11").clients().get("a9ca4217-74a8-4658-92c8-c2f9ed48a474"); + assertLocaleMapperPresent(foo11AdminConsoleClient); - // Assert "locale" mapper available in security-admin-console client for both master and foo11 realm - ClientResource foo11AdminConsoleClient = adminClient.realm("foo11").clients().get("a9ca4217-74a8-4658-92c8-c2f9ed48a474"); - assertLocaleMapperPresent(foo11AdminConsoleClient); + // Assert "realm-management" role correctly set and contains all admin roles. + ClientResource foo11RealmManagementClient = adminClient.realm("foo11").clients().get("c7a9cf59-feeb-44a4-a467-e008e157efa2"); + List roles = foo11RealmManagementClient.roles().list(); + assertRolesAvailable(roles); - ClientResource masterAdminConsoleClient = adminClient.realm(Config.getAdminRealm()).clients().get("22ed594d-8c21-43f0-a080-c8879a411f94"); - assertLocaleMapperPresent(masterAdminConsoleClient); + // Assert all admin roles are also available as composites of "realm-admin" + Set realmAdminComposites = foo11RealmManagementClient.roles().get(AdminRoles.REALM_ADMIN).getRoleComposites(); + assertRolesAvailable(realmAdminComposites); - - // Assert "realm-management" role correctly set and contains all admin roles. - ClientResource foo11RealmManagementClient = adminClient.realm("foo11").clients().get("c7a9cf59-feeb-44a4-a467-e008e157efa2"); - List roles = foo11RealmManagementClient.roles().list(); - assertRolesAvailable(roles); - - // Assert all admin roles are also available as composites of "realm-admin" - Set realmAdminComposites = foo11RealmManagementClient.roles().get(AdminRoles.REALM_ADMIN).getRoleComposites(); - assertRolesAvailable(realmAdminComposites); - - // Assert "foo11-master" client correctly set and contains all admin roles. - ClientResource foo11MasterAdminClient = adminClient.realm(Config.getAdminRealm()).clients().get("c9c3bd5f-b69d-4640-8b27-45d4f3866a36"); - roles = foo11MasterAdminClient.roles().list(); - assertRolesAvailable(roles); - - // Assert all admin roles are also available as composites of "admin" role - Set masterAdminComposites = adminClient.realm(Config.getAdminRealm()).roles().get(AdminRoles.ADMIN).getRoleComposites(); - assertRolesAvailable(masterAdminComposites); + // Assert all admin roles are also available as composites of "admin" role + Set masterAdminComposites = adminClient.realm(Config.getAdminRealm()).roles().get(AdminRoles.ADMIN).getRoleComposites(); + assertRolesAvailable(masterAdminComposites); + } finally { + removeRealm("foo11"); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java index 72d0c70c74..fe789ace43 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java @@ -52,7 +52,7 @@ import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER; */ public class MigrationTest extends AbstractKeycloakTest { - private final String MIGRATION = "Migration"; + public static final String MIGRATION = "Migration"; private RealmResource migrationRealm; private RealmResource masterRealm; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java index 4bb437c0b9..7a01e4e291 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java @@ -31,12 +31,15 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicNameValuePair; import org.junit.Test; +import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.models.Constants; +import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; import java.io.IOException; import java.net.URLEncoder; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.regex.Matcher; @@ -159,6 +162,31 @@ public class LoginStatusIframeEndpointTest extends AbstractKeycloakTest { } } + @Test + public void checkIframeWildcardOrigin() throws IOException { + String id = adminClient.realm("master").clients().findByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID).get(0).getId(); + ClientResource master = adminClient.realm("master").clients().get(id); + ClientRepresentation rep = master.toRepresentation(); + List org = rep.getWebOrigins(); + CloseableHttpClient client = HttpClients.createDefault(); + try { + rep.setWebOrigins(Collections.singletonList("*")); + master.update(rep); + + HttpGet get = new HttpGet(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/master/protocol/openid-connect/login-status-iframe.html/init?" + + "client_id=" + Constants.ADMIN_CONSOLE_CLIENT_ID + + "&origin=" + "http://anything" + ); + CloseableHttpResponse response = client.execute(get); + assertEquals(204, response.getStatusLine().getStatusCode()); + response.close(); + } finally { + rep.setWebOrigins(org); + master.update(rep); + client.close(); + } + } + @Override public void addTestRealms(List testRealms) { } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java index d317af7de1..ac86d2023a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java @@ -43,10 +43,8 @@ import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.ProtocolMapperUtil; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId; import static org.keycloak.testsuite.admin.ApiUtil.findClientResourceByClientId; @@ -222,11 +220,152 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest { // Verify attribute is filled Map roleMappings = (Map)idToken.getOtherClaims().get("roles-custom"); - Assert.assertEquals(2, roleMappings.size()); + Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm", "test-app")); String realmRoleMappings = (String) roleMappings.get("realm"); String testAppMappings = (String) roleMappings.get("test-app"); - Assert.assertTrue(realmRoleMappings.contains("pref.user")); - Assert.assertEquals("[customer-user]", testAppMappings); + assertRoles(realmRoleMappings, + "pref.user", // from direct assignment in user definition + "pref.offline_access" // from direct assignment in user definition + ); + assertRoles(testAppMappings, + "customer-user" // from direct assignment in user definition + ); + } + + + @Test + public void testUserGroupRoleToAttributeMappers() throws Exception { + // Add mapper for realm roles + String clientId = "test-app"; + ProtocolMapperRepresentation realmMapper = ProtocolMapperUtil.createUserRealmRoleMappingMapper("pref.", "Realm roles mapper", "roles-custom.realm", true, true); + ProtocolMapperRepresentation clientMapper = ProtocolMapperUtil.createUserClientRoleMappingMapper(clientId, "ta.", "Client roles mapper", "roles-custom.test-app", true, true); + + ProtocolMappersResource protocolMappers = ApiUtil.findClientResourceByClientId(adminClient.realm("test"), clientId).getProtocolMappers(); + protocolMappers.createMapper(Arrays.asList(realmMapper, clientMapper)); + + // Login user + OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "rich.roles@redhat.com", "password"); + IDToken idToken = oauth.verifyIDToken(response.getIdToken()); + + // Verify attribute is filled + Map roleMappings = (Map)idToken.getOtherClaims().get("roles-custom"); + Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm", clientId)); + String realmRoleMappings = (String) roleMappings.get("realm"); + String testAppMappings = (String) roleMappings.get(clientId); + assertRoles(realmRoleMappings, + "pref.admin", // from direct assignment to /roleRichGroup/level2group + "pref.user", // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup + "pref.customer-user-premium", // from client role customer-admin-composite-role - realm role for test-app + "pref.realm-composite-role", // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup + "pref.sample-realm-role" // from realm role realm-composite-role + ); + assertRoles(testAppMappings, + "ta.customer-user", // from direct assignment to /roleRichGroup/level2group + "ta.customer-admin-composite-role", // from direct assignment to /roleRichGroup/level2group + "ta.customer-admin", // from client role customer-admin-composite-role - client role for test-app + "ta.sample-client-role" // from realm role realm-composite-role - client role for test-app + ); + } + + @Test + public void testUserGroupRoleToAttributeMappersNotScopedOtherApp() throws Exception { + String clientId = "test-app-authz"; + ProtocolMapperRepresentation realmMapper = ProtocolMapperUtil.createUserRealmRoleMappingMapper("pref.", "Realm roles mapper", "roles-custom.realm", true, true); + ProtocolMapperRepresentation clientMapper = ProtocolMapperUtil.createUserClientRoleMappingMapper(clientId, null, "Client roles mapper", "roles-custom." + clientId, true, true); + + ProtocolMappersResource protocolMappers = ApiUtil.findClientResourceByClientId(adminClient.realm("test"), clientId).getProtocolMappers(); + protocolMappers.createMapper(Arrays.asList(realmMapper, clientMapper)); + + // Login user + ClientManager.realm(adminClient.realm("test")).clientId(clientId).directAccessGrant(true); + oauth.clientId(clientId); + OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "rich.roles@redhat.com", "password"); + IDToken idToken = oauth.verifyIDToken(response.getIdToken()); + + // Verify attribute is filled + Map roleMappings = (Map)idToken.getOtherClaims().get("roles-custom"); + Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm", clientId)); + String realmRoleMappings = (String) roleMappings.get("realm"); + String testAppAuthzMappings = (String) roleMappings.get(clientId); + assertRoles(realmRoleMappings, + "pref.admin", // from direct assignment to /roleRichGroup/level2group + "pref.user", // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup + "pref.customer-user-premium", // from client role customer-admin-composite-role - realm role for test-app + "pref.realm-composite-role", // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup + "pref.sample-realm-role" // from realm role realm-composite-role + ); + assertRoles(testAppAuthzMappings); // There is no client role defined for test-app-authz + } + + @Test + public void testUserGroupRoleToAttributeMappersScoped() throws Exception { + String clientId = "test-app-scope"; + ProtocolMapperRepresentation realmMapper = ProtocolMapperUtil.createUserRealmRoleMappingMapper("pref.", "Realm roles mapper", "roles-custom.realm", true, true); + ProtocolMapperRepresentation clientMapper = ProtocolMapperUtil.createUserClientRoleMappingMapper(clientId, null, "Client roles mapper", "roles-custom.test-app-scope", true, true); + + ProtocolMappersResource protocolMappers = ApiUtil.findClientResourceByClientId(adminClient.realm("test"), clientId).getProtocolMappers(); + protocolMappers.createMapper(Arrays.asList(realmMapper, clientMapper)); + + // Login user + ClientManager.realm(adminClient.realm("test")).clientId(clientId).directAccessGrant(true); + oauth.clientId(clientId); + OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "rich.roles@redhat.com", "password"); + IDToken idToken = oauth.verifyIDToken(response.getIdToken()); + + // Verify attribute is filled + Map roleMappings = (Map)idToken.getOtherClaims().get("roles-custom"); + Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm", clientId)); + String realmRoleMappings = (String) roleMappings.get("realm"); + String testAppScopeMappings = (String) roleMappings.get(clientId); + assertRoles(realmRoleMappings, + "pref.admin", // from direct assignment to /roleRichGroup/level2group + "pref.user" // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup + ); + assertRoles(testAppScopeMappings, + "test-app-allowed-by-scope" // from direct assignment to roleRichUser, present as scope allows it + ); + } + + @Test + public void testUserGroupRoleToAttributeMappersScopedClientNotSet() throws Exception { + String clientId = "test-app-scope"; + ProtocolMapperRepresentation realmMapper = ProtocolMapperUtil.createUserRealmRoleMappingMapper("pref.", "Realm roles mapper", "roles-custom.realm", true, true); + ProtocolMapperRepresentation clientMapper = ProtocolMapperUtil.createUserClientRoleMappingMapper(null, null, "Client roles mapper", "roles-custom.test-app-scope", true, true); + + ProtocolMappersResource protocolMappers = ApiUtil.findClientResourceByClientId(adminClient.realm("test"), clientId).getProtocolMappers(); + protocolMappers.createMapper(Arrays.asList(realmMapper, clientMapper)); + + // Login user + ClientManager.realm(adminClient.realm("test")).clientId(clientId).directAccessGrant(true); + oauth.clientId(clientId); + OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "rich.roles@redhat.com", "password"); + IDToken idToken = oauth.verifyIDToken(response.getIdToken()); + + // Verify attribute is filled + Map roleMappings = (Map)idToken.getOtherClaims().get("roles-custom"); + Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm", clientId)); + String realmRoleMappings = (String) roleMappings.get("realm"); + String testAppScopeMappings = (String) roleMappings.get(clientId); + assertRoles(realmRoleMappings, + "pref.admin", // from direct assignment to /roleRichGroup/level2group + "pref.user" // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup + ); + assertRoles(testAppScopeMappings, + "test-app-allowed-by-scope", // from direct assignment to roleRichUser, present as scope allows it + "customer-admin-composite-role" // from direct assignment to /roleRichGroup/level2group, present as scope allows it + ); + } + + private void assertRoles(String actualRoleString, String...expectedRoles) { + String[] roles; + Assert.assertThat(actualRoleString.matches("^\\[.*\\]$"), is(true)); + roles = actualRoleString.substring(1, actualRoleString.length() - 1).split(",\\s*"); + + if (expectedRoles == null || expectedRoles.length == 0) { + Assert.assertThat(roles, arrayContainingInAnyOrder("")); + } else { + Assert.assertThat(roles, arrayContainingInAnyOrder(expectedRoles)); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json index d8c4dc1b0c..deb5b64dee 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json @@ -119,7 +119,10 @@ "default": { "clustered": "${keycloak.connectionsInfinispan.clustered:false}", "async": "${keycloak.connectionsInfinispan.async:true}", - "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}" + "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}", + "remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:false}", + "remoteStoreHost": "${keycloak.connectionsInfinispan.remoteStoreHost:localhost}", + "remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}" } }, diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/clientreg-test/saml-entity-descriptor.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/clientreg-test/saml-entity-descriptor.xml index 0bfce4b308..694bb82281 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/clientreg-test/saml-entity-descriptor.xml +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/clientreg-test/saml-entity-descriptor.xml @@ -90,10 +90,22 @@ x5Ql0ejivIJAYcMGUyA+/YwJg2FGoA== isDefault="true" index="0" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" - Location="https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp"/> + Location="https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/artifact"/> + Location="https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/post"/> + + + diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/exportimport-test/kc11-exported-realm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/exportimport-test/kc11-exported-realm.json index d421fe4c04..9e76d608d7 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/exportimport-test/kc11-exported-realm.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/exportimport-test/kc11-exported-realm.json @@ -1,286 +1,4 @@ -[ { - "id" : "master", - "realm" : "master", - "notBefore" : 0, - "accessTokenLifespan" : 60, - "ssoSessionIdleTimeout" : 1800, - "ssoSessionMaxLifespan" : 36000, - "accessCodeLifespan" : 60, - "accessCodeLifespanUserAction" : 300, - "enabled" : true, - "sslRequired" : "external", - "passwordCredentialGrantAllowed" : false, - "registrationAllowed" : false, - "rememberMe" : false, - "verifyEmail" : false, - "resetPasswordAllowed" : false, - "social" : false, - "updateProfileOnInitialSocialLogin" : false, - "bruteForceProtected" : false, - "maxFailureWaitSeconds" : 900, - "minimumQuickLoginWaitSeconds" : 60, - "waitIncrementSeconds" : 60, - "quickLoginCheckMilliSeconds" : 1000, - "maxDeltaTimeSeconds" : 43200, - "failureFactor" : 30, - "privateKey" : "MIICXQIBAAKBgQC5lddWO92keqWg+QmMUj/jxA2kwH22UZ0iE9454Ail9JnOvwOTXSP8M92JN7D7DSJM/J45E2Kju5RrQ/QM8bBwYPk/vZlQkJcKbnrkQFtUdBrjoaMQlDvoaqIx1u4irSj2phRPR8teT72A867JGnW2clIwScl2dznZs2Br+jCN3QIDAQABAoGBAKdfFMqnyRfKqM+JaewMTaR7rxZTp8yixET0iCnH++S3uXM03+OqT4bnu7dB67IuwS2Pcp7k9cPWq18l9NcrrcPQCS5knpoNzDO2RuLfXDUCGG/N3MMmthRAeILHun8/CBSfBbcdJESn67g4RV5AldWf8dSgwUcwN4RxbnfUdIbdAkEA9ko38bhfszg9VRea/XVNIpUBQZXpsHt951GoL1Sz0u5iUADyDc/lLgV+eNA9mclvBpg+S+2jcAWMY1rN34wU5wJBAMDm78sYQK8ert+bJV8OSl+6Rpu3cLSdBWNnHZWBpDUHO9JlD2GQblDR3MoL+2j0W/F+7MLhT/LZPQkvMCM+KpsCQGoS+RlQcVc9B51Yd1ZmaPxV9J6MtINgDI/OKYOJFZHpPcp7PcUZHvm9QAVEmuNbUEgk1d/Zz6R1n0tDVpvLN00CQQCH0tNq3DPHWkJlXXdN2+EQUDehMuOfuKPvns5c08CMOgCsHs5asviJ3YqplRA7kTsf6m/ItB637rAkRF6PohkbAkBi9CUTSy32o0AKBuhPDVJOgTqfvlNqmraa/0V65IDhactJ3hmgJXpUI7F0u42NU0uXgU5QMFwHet1sSWxnGcaa", - "publicKey" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC5lddWO92keqWg+QmMUj/jxA2kwH22UZ0iE9454Ail9JnOvwOTXSP8M92JN7D7DSJM/J45E2Kju5RrQ/QM8bBwYPk/vZlQkJcKbnrkQFtUdBrjoaMQlDvoaqIx1u4irSj2phRPR8teT72A867JGnW2clIwScl2dznZs2Br+jCN3QIDAQAB", - "certificate" : "MIIBlTCB/wIGAVPnGdy9MA0GCSqGSIb3DQEBCwUAMBExDzANBgNVBAMTBm1hc3RlcjAeFw0xNjA0MDUxNTQ0MDVaFw0yNjA0MDUxNTQ1NDVaMBExDzANBgNVBAMTBm1hc3RlcjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuZXXVjvdpHqloPkJjFI/48QNpMB9tlGdIhPeOeAIpfSZzr8Dk10j/DPdiTew+w0iTPyeORNio7uUa0P0DPGwcGD5P72ZUJCXCm565EBbVHQa46GjEJQ76GqiMdbuIq0o9qYUT0fLXk+9gPOuyRp1tnJSMEnJdnc52bNga/owjd0CAwEAATANBgkqhkiG9w0BAQsFAAOBgQBqQFaqBy50CddfEHPhlf5YDUmTwZIoX/rh74vTESl7thzRQpQ6LhKVI3hfBNI91Xcr58J1WEA3Lm93T7yC5/ShsGbDJi8RJTDhQYY6LBhxT2ZSq+RLFaWyloFLa5V7hTY4F73yml4IM5mKLMmvcxr4xIZvPkKsvR0C+y9yb4dEzg==", - "codeSecret" : "8669f9cf-6715-48f0-929d-ec5d66a6efcf", - "roles" : { - "realm" : [ { - "id" : "db49ad9b-6784-4eb8-bedd-07ff716098c0", - "name" : "admin", - "composite" : true, - "composites" : { - "realm" : [ "create-realm" ], - "application" : { - "foo11-realm" : [ "view-events", "view-realm", "manage-events", "manage-clients", "manage-realm", "view-clients", "view-users", "manage-applications", "manage-users", "view-applications" ], - "master-realm" : [ "view-realm", "manage-applications", "manage-realm", "manage-users", "view-events", "manage-events", "view-applications", "view-users", "view-clients", "manage-clients" ] - } - } - }, { - "id" : "f6b11ea0-0287-4631-9ce1-df4c5998f840", - "name" : "create-realm", - "composite" : false - } ], - "application" : { - "security-admin-console" : [ ], - "foo11-realm" : [ { - "id" : "90a00c88-2ad5-4b38-81b2-3ba4583c67c9", - "name" : "manage-clients", - "composite" : false - }, { - "id" : "d103fd4a-55f2-409f-8357-5f9645463ac3", - "name" : "view-events", - "composite" : false - }, { - "id" : "76952522-6671-4abb-90a9-e6256386b8d3", - "name" : "manage-realm", - "composite" : false - }, { - "id" : "973ebcfb-37b2-43ce-af5a-acbc48429c86", - "name" : "view-clients", - "composite" : false - }, { - "id" : "b32deca4-a345-4fb6-a6ce-f8666e653c16", - "name" : "view-users", - "composite" : false - }, { - "id" : "f030bd3b-3ef8-496c-9c75-f370f19f7a56", - "name" : "manage-applications", - "composite" : false - }, { - "id" : "b196345c-07ca-4dea-8a35-84f5aa41f177", - "name" : "view-realm", - "composite" : false - }, { - "id" : "747c7af4-60a0-4be4-9c7a-33969572f3e1", - "name" : "manage-users", - "composite" : false - }, { - "id" : "ff468d9b-4d5a-4a03-9640-24b0a94a238f", - "name" : "manage-events", - "composite" : false - }, { - "id" : "61f9766c-44c2-4195-b9b8-c23d63409c16", - "name" : "view-applications", - "composite" : false - } ], - "master-realm" : [ { - "id" : "21866bbb-60de-4248-879f-ceb11a75f4e6", - "name" : "view-applications", - "composite" : false - }, { - "id" : "267071a5-170f-4438-b333-3d00a0ec268f", - "name" : "view-realm", - "composite" : false - }, { - "id" : "53a53160-92b3-43a4-9ba1-a0c19eaf1ad9", - "name" : "manage-applications", - "composite" : false - }, { - "id" : "2ce8b8ba-5e15-4a04-bedb-96d74784fd54", - "name" : "manage-realm", - "composite" : false - }, { - "id" : "d7045c16-29cb-4e88-bd61-7d6fd77e6c7d", - "name" : "manage-users", - "composite" : false - }, { - "id" : "6f933ebd-bbf5-4fea-b4e1-ace854667b9b", - "name" : "view-events", - "composite" : false - }, { - "id" : "3588ffcb-96cc-4263-8244-1b71d441202a", - "name" : "view-users", - "composite" : false - }, { - "id" : "5a4bcd8f-8cc9-4a01-94d1-3b8a86e228af", - "name" : "view-clients", - "composite" : false - }, { - "id" : "5c42606c-f3ec-4abd-aad0-9ec98d6fa39f", - "name" : "manage-events", - "composite" : false - }, { - "id" : "678d5c25-b5b0-4447-95c1-b3dc14fa0e3f", - "name" : "manage-clients", - "composite" : false - } ], - "account" : [ { - "id" : "700d3f40-8e11-47d7-b3f1-14d07a7da647", - "name" : "manage-account", - "composite" : false - }, { - "id" : "a9d81246-ec6c-4b71-912a-7a1518ec64d5", - "name" : "view-profile", - "composite" : false - } ] - } - }, - "requiredCredentials" : [ "password" ], - "users" : [ { - "id" : "d678f579-29f4-46d5-a124-8bcdbeeeb55d", - "username" : "admin", - "enabled" : true, - "totp" : false, - "emailVerified" : false, - "credentials" : [ { - "type" : "password", - "hashedSaltedValue" : "VIw4dTFMrU8aw3xvsI6Kqh2gA5Y0P2TJEyEmgplkColwuXUC2G+RTsahsOgqwG9yIgyrFS9Fe+GlPNUQWxO1Sw==", - "salt" : "5IsVTxiv9At7xTHoTN17+g==", - "hashIterations" : 1, - "temporary" : false - } ], - "requiredActions" : [ ], - "realmRoles" : [ "admin" ], - "applicationRoles" : { - "account" : [ "manage-account", "view-profile" ] - } - } ], - "scopeMappings" : [ { - "client" : "security-admin-console", - "roles" : [ "admin" ] - } ], - "applications" : [ { - "id" : "4fe35549-1d84-440e-83c6-48cad624aba4", - "name" : "master-realm", - "surrogateAuthRequired" : false, - "enabled" : true, - "secret" : "0da9f8c5-ee7a-4d4b-9c93-944ac72b7ef0", - "redirectUris" : [ ], - "webOrigins" : [ ], - "claims" : { - "name" : true, - "username" : true, - "profile" : true, - "picture" : true, - "website" : true, - "email" : true, - "gender" : true, - "locale" : true, - "address" : true, - "phone" : true - }, - "notBefore" : 0, - "bearerOnly" : true, - "publicClient" : false, - "attributes" : { }, - "fullScopeAllowed" : true, - "nodeReRegistrationTimeout" : 0 - }, { - "id" : "5b2a2ae8-f0b9-40cc-a586-4adf46379a49", - "name" : "account", - "baseUrl" : "/auth/realms/master/account", - "surrogateAuthRequired" : false, - "enabled" : true, - "secret" : "f055644e-e59e-462f-98fd-9a5b7c22e03a", - "defaultRoles" : [ "view-profile", "manage-account" ], - "redirectUris" : [ "/auth/realms/master/account/*" ], - "webOrigins" : [ ], - "claims" : { - "name" : true, - "username" : true, - "profile" : true, - "picture" : true, - "website" : true, - "email" : true, - "gender" : true, - "locale" : true, - "address" : true, - "phone" : true - }, - "notBefore" : 0, - "bearerOnly" : false, - "publicClient" : false, - "attributes" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0 - }, { - "id" : "22ed594d-8c21-43f0-a080-c8879a411f94", - "name" : "security-admin-console", - "baseUrl" : "/auth/admin/master/console/index.html", - "surrogateAuthRequired" : false, - "enabled" : true, - "secret" : "bf9b9f8b-0a85-42da-bc14-befab4305298", - "redirectUris" : [ "/auth/admin/master/console/*" ], - "webOrigins" : [ ], - "claims" : { - "name" : true, - "username" : true, - "profile" : true, - "picture" : true, - "website" : true, - "email" : true, - "gender" : true, - "locale" : true, - "address" : true, - "phone" : true - }, - "notBefore" : 0, - "bearerOnly" : false, - "publicClient" : true, - "attributes" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0 - }, { - "id" : "c9c3bd5f-b69d-4640-8b27-45d4f3866a36", - "name" : "foo11-realm", - "surrogateAuthRequired" : false, - "enabled" : true, - "secret" : "aba746f8-fafd-4d6d-af65-e0bb669b1afc", - "redirectUris" : [ ], - "webOrigins" : [ ], - "claims" : { - "name" : true, - "username" : true, - "profile" : true, - "picture" : true, - "website" : true, - "email" : true, - "gender" : true, - "locale" : true, - "address" : true, - "phone" : true - }, - "notBefore" : 0, - "bearerOnly" : true, - "publicClient" : false, - "attributes" : { }, - "fullScopeAllowed" : true, - "nodeReRegistrationTimeout" : 0 - } ], - "oauthClients" : [ ], - "browserSecurityHeaders" : { - "xFrameOptions" : "SAMEORIGIN", - "contentSecurityPolicy" : "frame-src 'self'" - }, - "socialProviders" : { }, - "smtpServer" : { }, - "eventsEnabled" : false, - "eventsListeners" : [ ] -}, { +{ "id" : "14e6923c-f5fb-44aa-8982-35d4976c56c5", "realm" : "foo11", "notBefore" : 0, @@ -491,4 +209,4 @@ "smtpServer" : { }, "eventsEnabled" : false, "eventsListeners" : [ ] -} ] \ No newline at end of file +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json index c0b2b6c6dc..b0e87679b3 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json @@ -85,6 +85,21 @@ "groups": [ "/topGroup/level2group" ] + }, + { + "username" : "roleRichUser", + "enabled": true, + "email" : "rich.roles@redhat.com", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "groups": [ + "/roleRichGroup/level2group" + ], + "clientRoles": { + "test-app-scope": [ "test-app-allowed-by-scope", "test-app-disallowed-by-scope" ] + } } ], "scopeMappings": [ @@ -95,6 +110,10 @@ { "client": "test-app", "roles": ["user"] + }, + { + "client": "test-app-scope", + "roles": ["user", "admin"] } ], "clients": [ @@ -108,6 +127,16 @@ "adminUrl": "http://localhost:8180/auth/realms/master/app/admin", "secret": "password" }, + { + "clientId" : "test-app-scope", + "enabled": true, + + "redirectUris": [ + "http://localhost:8180/auth/realms/master/app/*" + ], + "secret": "password", + "fullScopeAllowed": "false" + }, { "clientId" : "third-party", "enabled": true, @@ -290,6 +319,22 @@ { "name": "customer-user-premium", "description": "Have User Premium privileges" + }, + { + "name": "sample-realm-role", + "description": "Sample realm role" + }, + { + "name": "realm-composite-role", + "description": "Realm composite role containing client role", + "composite" : true, + "composites" : { + "realm" : [ "sample-realm-role" ], + "client" : { + "test-app" : [ "sample-client-role" ], + "account" : [ "view-profile" ] + } + } } ], "client" : { @@ -301,6 +346,31 @@ { "name": "customer-admin", "description": "Have Customer Admin privileges" + }, + { + "name": "sample-client-role", + "description": "Sample client role" + }, + { + "name": "customer-admin-composite-role", + "description": "Have Customer Admin privileges via composite role", + "composite" : true, + "composites" : { + "realm" : [ "customer-user-premium" ], + "client" : { + "test-app" : [ "customer-admin" ] + } + } + } + ], + "test-app-scope" : [ + { + "name": "test-app-allowed-by-scope", + "description": "Role allowed by scope in test-app-scope" + }, + { + "name": "test-app-disallowed-by-scope", + "description": "Role disallowed by scope in test-app-scope" } ] } @@ -325,6 +395,31 @@ "attributes": { "level2Attribute": ["true"] + } + } + ] + }, + { + "name": "roleRichGroup", + "attributes": { + "topAttribute": ["true"] + + }, + "realmRoles": ["user", "realm-composite-role"], + "clientRoles": { + "account": ["manage-account"] + }, + + "subGroups": [ + { + "name": "level2group", + "realmRoles": ["admin"], + "clientRoles": { + "test-app": ["customer-user", "customer-admin-composite-role"] + }, + "attributes": { + "level2Attribute": ["true"] + } } ] @@ -337,6 +432,16 @@ { "client": "third-party", "roles": ["customer-user"] + }, + { + "client": "test-app-scope", + "roles": ["customer-admin-composite-role"] + } + ], + "test-app-scope": [ + { + "client": "test-app-scope", + "roles": ["test-app-allowed-by-scope"] } ] }, diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap/src/test/java/org/keycloak/testsuite/adapter/EAPOIDCKerberosLdapAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap/src/test/java/org/keycloak/testsuite/adapter/EAPOIDCKerberosLdapAdapterTest.java deleted file mode 100644 index f9bdaf4221..0000000000 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap/src/test/java/org/keycloak/testsuite/adapter/EAPOIDCKerberosLdapAdapterTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.keycloak.testsuite.adapter; - -import org.keycloak.testsuite.adapter.federation.AbstractKerberosLdapAdapterTest; -import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; - -/** - * - * @author tkyjovsk - */ -@AppServerContainer("app-server-eap") -public class EAPOIDCKerberosLdapAdapterTest extends AbstractKerberosLdapAdapterTest { - -} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/EAP6OIDCKerberosLdapAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/EAP6OIDCKerberosLdapAdapterTest.java deleted file mode 100644 index 4dc41e5a32..0000000000 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/EAP6OIDCKerberosLdapAdapterTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.keycloak.testsuite.adapter; - -import org.keycloak.testsuite.adapter.federation.AbstractKerberosLdapAdapterTest; -import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; - -/** - * - * @author pdrozd - */ -@AppServerContainer("app-server-eap6") -public class EAP6OIDCKerberosLdapAdapterTest extends AbstractKerberosLdapAdapterTest { - -} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EAP6DefaultAuthzConfigAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EAP6DefaultAuthzConfigAdapterTest.java new file mode 100644 index 0000000000..d7fe93af5d --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EAP6DefaultAuthzConfigAdapterTest.java @@ -0,0 +1,28 @@ +/* + * 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.testsuite.adapter.example.authorization; + +import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; + +/** + * + * @author tkyjovsk + */ +@AppServerContainer("app-server-eap6") +public class EAP6DefaultAuthzConfigAdapterTest extends AbstractDefaultAuthzConfigAdapterTest { + +} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EAP6PhotozExampleAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EAP6PhotozExampleAdapterTest.java new file mode 100644 index 0000000000..7319dce21d --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EAP6PhotozExampleAdapterTest.java @@ -0,0 +1,29 @@ +/* + * 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.testsuite.adapter.example.authorization; + +import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; + +/** + * + * @author tkyjovsk + */ +@AppServerContainer("app-server-eap6") +//@AdapterLibsLocationProperty("adapter.libs.wildfly") +public class EAP6PhotozExampleAdapterTest extends AbstractPhotozExampleAdapterTest { + +} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EAP6ServletAuthzAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EAP6ServletAuthzAdapterTest.java new file mode 100644 index 0000000000..5833b298ab --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EAP6ServletAuthzAdapterTest.java @@ -0,0 +1,30 @@ +/* + * 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.testsuite.adapter.example.authorization; + +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; + +/** + * + * @author tkyjovsk + */ +@RunAsClient +@AppServerContainer("app-server-eap6") +public class EAP6ServletAuthzAdapterTest extends AbstractServletAuthzAdapterTest { + +} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/WildflyOIDCKerberosLdapAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/WildflyOIDCKerberosLdapAdapterTest.java deleted file mode 100644 index 166ae2c491..0000000000 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/WildflyOIDCKerberosLdapAdapterTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.keycloak.testsuite.adapter; - -import org.keycloak.testsuite.adapter.federation.AbstractKerberosLdapAdapterTest; -import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; - -/** - * - * @author pdrozd - */ -@AppServerContainer("app-server-wildfly") -public class WildflyOIDCKerberosLdapAdapterTest extends AbstractKerberosLdapAdapterTest { - -} diff --git a/testsuite/integration-arquillian/tests/other/clean-start/pom.xml b/testsuite/integration-arquillian/tests/other/clean-start/pom.xml index 65e00bdd62..06f6ffe040 100644 --- a/testsuite/integration-arquillian/tests/other/clean-start/pom.xml +++ b/testsuite/integration-arquillian/tests/other/clean-start/pom.xml @@ -23,7 +23,7 @@ org.keycloak.testsuite integration-arquillian-tests-other - 2.1.0-SNAPSHOT + 2.4.0.CR1-SNAPSHOT integration-arquillian-tests-smoke-clean-start diff --git a/testsuite/integration-arquillian/tests/other/pom.xml b/testsuite/integration-arquillian/tests/other/pom.xml index a36c64155c..ed5e1195ad 100644 --- a/testsuite/integration-arquillian/tests/other/pom.xml +++ b/testsuite/integration-arquillian/tests/other/pom.xml @@ -118,6 +118,12 @@
+ + clean-start + + clean-start + + console-ui-tests diff --git a/testsuite/integration-arquillian/tests/other/sssd/src/test/java/org/keycloak/testsuite/sssd/SSSDTest.java b/testsuite/integration-arquillian/tests/other/sssd/src/test/java/org/keycloak/testsuite/sssd/SSSDTest.java index b11d5e43b4..68488cce7d 100644 --- a/testsuite/integration-arquillian/tests/other/sssd/src/test/java/org/keycloak/testsuite/sssd/SSSDTest.java +++ b/testsuite/integration-arquillian/tests/other/sssd/src/test/java/org/keycloak/testsuite/sssd/SSSDTest.java @@ -2,6 +2,7 @@ package org.keycloak.testsuite.sssd; import org.jboss.arquillian.graphene.page.Page; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.keycloak.representations.idm.GroupRepresentation; @@ -72,6 +73,7 @@ public class SSSDTest extends AbstractKeycloakTest { adminClient.realm(REALM_NAME).userFederation().create(userFederation); } + @Ignore @Test public void testProviderFactories() { List providerFactories = adminClient.realm(REALM_NAME).userFederation().getProviderFactories(); diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml index 4f4f42ddd6..f6838abab1 100755 --- a/testsuite/integration-arquillian/tests/pom.xml +++ b/testsuite/integration-arquillian/tests/pom.xml @@ -182,6 +182,8 @@ ${firefox_binary} ${project.version} + ${migration.project.version} + ${migration.product.version} diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index 873d99ddfe..8a2dc4b29f 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -232,6 +232,10 @@ org.infinispan infinispan-core + + org.infinispan + infinispan-cachestore-remote + org.seleniumhq.selenium selenium-java diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java index f5cbccb3f6..91075ae4ee 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java @@ -238,7 +238,7 @@ public class LDAPGroupMapperSyncTest { GroupModel model1 = realm.createGroup("model1"); realm.moveGroup(model1, null); GroupModel model2 = realm.createGroup("model2"); - kcGroup1.addChild(model2); + realm.moveGroup(model2, kcGroup1); // Sync groups again from LDAP. Nothing deleted syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterInvalidationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterInvalidationTest.java new file mode 100644 index 0000000000..f71d0da25a --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterInvalidationTest.java @@ -0,0 +1,420 @@ +/* + * 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.testsuite.model; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.infinispan.Cache; +import org.infinispan.notifications.Listener; +import org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved; +import org.infinispan.notifications.cachelistener.event.CacheEntryRemovedEvent; +import org.jboss.logging.Logger; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Test; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientTemplateModel; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserConsentModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.KeycloakServer; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.testsuite.util.cli.TestCacheUtils; + +/** + * Requires execution with cluster (or external JDG) enabled and real database, which will be shared for both cluster nodes. Everything set by system properties: + * + * 1) Use those system properties to run against shared MySQL: + * + * -Dkeycloak.connectionsJpa.url=jdbc:mysql://localhost/keycloak -Dkeycloak.connectionsJpa.driver=com.mysql.jdbc.Driver -Dkeycloak.connectionsJpa.user=keycloak + * -Dkeycloak.connectionsJpa.password=keycloak + * + * + * 2) Then either choose from: + * + * 2.a) Run test with 2 keycloak nodes in cluster. Add this system property for that: -Dkeycloak.connectionsInfinispan.clustered=true + * + * 2.b) Run test with 2 keycloak nodes without cluster, but instead with external JDG. Both keycloak servers will send invalidation events to the JDG server and receive the events from this JDG server. + * They don't communicate with each other. So JDG is man-in-the-middle. + * + * This assumes that you have JDG 7.0 server running on localhost with HotRod endpoint on port 11222 (which is default port anyway). + * + * You also need to have this cache configured in JDG_HOME/standalone/configuration/standalone.xml to infinispan subsystem : + * + * + * + * Finally, add this system property when running the test: -Dkeycloak.connectionsInfinispan.remoteStoreEnabled=true + * + * @author Marek Posolda + */ +@Ignore +public class ClusterInvalidationTest { + + protected static final Logger logger = Logger.getLogger(ClusterInvalidationTest.class); + + private static final String REALM_NAME = "test"; + + private static final int SLEEP_TIME_MS = Integer.parseInt(System.getProperty("sleep.time", "500")); + + private static TestListener listener1realms; + private static TestListener listener1users; + private static TestListener listener2realms; + private static TestListener listener2users; + + @ClassRule + public static KeycloakRule server1 = new KeycloakRule(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + InfinispanConnectionProvider infinispan = manager.getSession().getProvider(InfinispanConnectionProvider.class); + + Cache cache = infinispan.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME); + listener1realms = new TestListener("server1 - realms", cache); + cache.addListener(listener1realms); + + cache = infinispan.getCache(InfinispanConnectionProvider.USER_CACHE_NAME); + listener1users = new TestListener("server1 - users", cache); + cache.addListener(listener1users); + } + + }); + + @ClassRule + public static KeycloakRule server2 = new KeycloakRule(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + InfinispanConnectionProvider infinispan = manager.getSession().getProvider(InfinispanConnectionProvider.class); + + Cache cache = infinispan.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME); + listener2realms = new TestListener("server2 - realms", cache); + cache.addListener(listener2realms); + + cache = infinispan.getCache(InfinispanConnectionProvider.USER_CACHE_NAME); + listener2users = new TestListener("server2 - users", cache); + cache.addListener(listener2users); + } + + }) { + + @Override + protected void configureServer(KeycloakServer server) { + server.getConfig().setPort(8082); + } + + @Override + protected void importRealm() { + } + + @Override + protected void removeTestRealms() { + } + + }; + + private static void clearListeners() { + listener1realms.getInvalidationsAndClear(); + listener1users.getInvalidationsAndClear(); + listener2realms.getInvalidationsAndClear(); + listener2users.getInvalidationsAndClear(); + } + + + @Test + public void testClusterInvalidation() throws Exception { + cacheEverything(); + + clearListeners(); + + KeycloakSession session1 = server1.startSession(); + + + logger.info("UPDATE REALM"); + + RealmModel realm = session1.realms().getRealmByName(REALM_NAME); + realm.setDisplayName("foo"); + session1 = commit(server1, session1, true); + + assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 3, realm.getId()); + assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 3, realm.getId()); + + + // CREATES + + logger.info("CREATE ROLE"); + realm = session1.realms().getRealmByName(REALM_NAME); + realm.addRole("foo-role"); + session1 = commit(server1, session1, true); + + assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 1, "test.roles"); + assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 1, "test.roles"); + + + logger.info("CREATE CLIENT"); + realm = session1.realms().getRealmByName(REALM_NAME); + realm.addClient("foo-client"); + session1 = commit(server1, session1, true); + + assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 1, "test.realm.clients"); + assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 1, "test.realm.clients"); + + logger.info("CREATE GROUP"); + realm = session1.realms().getRealmByName(REALM_NAME); + GroupModel group = realm.createGroup("foo-group"); + session1 = commit(server1, session1, true); + + assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 1, "test.top.groups"); + assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 1, "test.top.groups"); + + logger.info("CREATE CLIENT TEMPLATE"); + realm = session1.realms().getRealmByName(REALM_NAME); + realm.addClientTemplate("foo-template"); + session1 = commit(server1, session1, true); + + assertInvalidations(listener1realms.getInvalidationsAndClear(), 2, 3, realm.getId()); + assertInvalidations(listener2realms.getInvalidationsAndClear(), 0, 2); // realm not cached on server2 due to previous invalidation + + + // UPDATES + + logger.info("UPDATE ROLE"); + realm = session1.realms().getRealmByName(REALM_NAME); + ClientModel testApp = realm.getClientByClientId("test-app"); + RoleModel role = session1.realms().getClientRole(realm, testApp, "customer-user"); + role.setDescription("Foo"); + session1 = commit(server1, session1, true); + + assertInvalidations(listener1realms.getInvalidationsAndClear(), 2, 3, role.getId()); + assertInvalidations(listener2realms.getInvalidationsAndClear(), 2, 3, role.getId()); + + logger.info("UPDATE GROUP"); + realm = session1.realms().getRealmByName(REALM_NAME); + group = KeycloakModelUtils.findGroupByPath(realm, "/topGroup"); + group.grantRole(role); + session1 = commit(server1, session1, true); + + assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 1, group.getId()); + assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 1, group.getId()); + + logger.info("UPDATE CLIENT"); + realm = session1.realms().getRealmByName(REALM_NAME); + testApp = realm.getClientByClientId("test-app"); + testApp.setDescription("foo");; + session1 = commit(server1, session1, true); + + assertInvalidations(listener1realms.getInvalidationsAndClear(), 2, 3, testApp.getId()); + assertInvalidations(listener2realms.getInvalidationsAndClear(), 2, 3, testApp.getId()); + + // Cache client template on server2 + KeycloakSession session2 = server2.startSession(); + realm = session2.realms().getRealmByName(REALM_NAME); + realm.getClientTemplates().get(0); + + + logger.info("UPDATE CLIENT TEMPLATE"); + realm = session1.realms().getRealmByName(REALM_NAME); + ClientTemplateModel clientTemplate = realm.getClientTemplates().get(0); + clientTemplate.setDescription("bar"); + + session1 = commit(server1, session1, true); + + assertInvalidations(listener1realms.getInvalidationsAndClear(), 1, 1, clientTemplate.getId()); + assertInvalidations(listener2realms.getInvalidationsAndClear(), 1, 1, clientTemplate.getId()); + + // Nothing yet invalidated in user cache + assertInvalidations(listener1users.getInvalidationsAndClear(), 0, 0); + assertInvalidations(listener2users.getInvalidationsAndClear(), 0, 0); + + logger.info("UPDATE USER"); + realm = session1.realms().getRealmByName(REALM_NAME); + UserModel user = session1.users().getUserByEmail("keycloak-user@localhost", realm); + user.setSingleAttribute("foo", "Bar"); + session1 = commit(server1, session1, true); + + assertInvalidations(listener1users.getInvalidationsAndClear(), 1, 5, user.getId(), "test.email.keycloak-user@localhost"); + assertInvalidations(listener2users.getInvalidationsAndClear(), 1, 5, user.getId()); + + logger.info("UPDATE USER CONSENTS"); + realm = session1.realms().getRealmByName(REALM_NAME); + testApp = realm.getClientByClientId("test-app"); + user = session1.users().getUserByEmail("keycloak-user@localhost", realm); + session1.users().addConsent(realm, user.getId(), new UserConsentModel(testApp)); + session1 = commit(server1, session1, true); + + assertInvalidations(listener1users.getInvalidationsAndClear(), 1, 1, user.getId() + ".consents"); + assertInvalidations(listener2users.getInvalidationsAndClear(), 1, 1, user.getId() + ".consents"); + + + // REMOVALS + + logger.info("REMOVE USER"); + realm = session1.realms().getRealmByName(REALM_NAME); + user = session1.users().getUserByUsername("john-doh@localhost", realm); + session1.users().removeUser(realm, user); + + session1 = commit(server1, session1, true); + + assertInvalidations(listener1users.getInvalidationsAndClear(), 3, 5, user.getId(), user.getId() + ".consents", "test.username.john-doh@localhost"); + assertInvalidations(listener2users.getInvalidationsAndClear(), 2, 5, user.getId(), user.getId() + ".consents"); + + cacheEverything(); + + logger.info("REMOVE CLIENT TEMPLATE"); + realm = session1.realms().getRealmByName(REALM_NAME); + realm.removeClientTemplate(clientTemplate.getId()); + session1 = commit(server1, session1, true); + + assertInvalidations(listener1realms.getInvalidationsAndClear(), 2, 5, realm.getId(), clientTemplate.getId()); + assertInvalidations(listener2realms.getInvalidationsAndClear(), 2, 5, realm.getId(), clientTemplate.getId()); + + cacheEverything(); + + logger.info("REMOVE ROLE"); + realm = session1.realms().getRealmByName(REALM_NAME); + role = realm.getRole("user"); + realm.removeRole(role); + ClientModel thirdparty = session1.realms().getClientByClientId("third-party", realm); + session1 = commit(server1, session1, true); + + assertInvalidations(listener1realms.getInvalidationsAndClear(), 7, 10, role.getId(), realm.getId(), "test.roles", "test.user.roles", testApp.getId(), thirdparty.getId(), group.getId()); + assertInvalidations(listener2realms.getInvalidationsAndClear(), 7, 10, role.getId(), realm.getId(), "test.roles", "test.user.roles", testApp.getId(), thirdparty.getId(), group.getId()); + + // all users invalidated + assertInvalidations(listener1users.getInvalidationsAndClear(), 10, 100); + assertInvalidations(listener2users.getInvalidationsAndClear(), 10, 100); + + cacheEverything(); + + logger.info("REMOVE GROUP"); + realm = session1.realms().getRealmByName(REALM_NAME); + group = realm.getGroupById(group.getId()); + String subgroupId = group.getSubGroups().iterator().next().getId(); + realm.removeGroup(group); + session1 = commit(server1, session1, true); + + assertInvalidations(listener1realms.getInvalidationsAndClear(), 3, 5, group.getId(), subgroupId, "test.top.groups"); + assertInvalidations(listener2realms.getInvalidationsAndClear(), 3, 5, group.getId(), subgroupId, "test.top.groups"); + + // all users invalidated + assertInvalidations(listener1users.getInvalidationsAndClear(), 10, 100); + assertInvalidations(listener2users.getInvalidationsAndClear(), 10, 100); + + cacheEverything(); + + logger.info("REMOVE CLIENT"); + realm = session1.realms().getRealmByName(REALM_NAME); + testApp = realm.getClientByClientId("test-app"); + role = testApp.getRole("customer-user"); + realm.removeClient(testApp.getId()); + session1 = commit(server1, session1, true); + + assertInvalidations(listener1realms.getInvalidationsAndClear(), 8, 12, testApp.getId(), testApp.getId() + ".roles", role.getId(), testApp.getId() + ".customer-user.roles", "test.realm.clients", thirdparty.getId()); + assertInvalidations(listener2realms.getInvalidationsAndClear(), 8, 12, testApp.getId(), testApp.getId() + ".roles", role.getId(), testApp.getId() + ".customer-user.roles", "test.realm.clients", thirdparty.getId()); + + // all users invalidated + assertInvalidations(listener1users.getInvalidationsAndClear(), 10, 100); + assertInvalidations(listener2users.getInvalidationsAndClear(), 10, 100); + + cacheEverything(); + + logger.info("REMOVE REALM"); + realm = session1.realms().getRealmByName(REALM_NAME); + session1.realms().removeRealm(realm.getId()); + session1 = commit(server1, session1, true); + + assertInvalidations(listener1realms.getInvalidationsAndClear(), 50, 200, realm.getId(), thirdparty.getId()); + assertInvalidations(listener2realms.getInvalidationsAndClear(), 50, 200, realm.getId(), thirdparty.getId()); + + // all users invalidated + assertInvalidations(listener1users.getInvalidationsAndClear(), 10, 100); + assertInvalidations(listener2users.getInvalidationsAndClear(), 10, 100); + + + //Thread.sleep(10000000); + } + + private void assertInvalidations(Map invalidations, int low, int high, String... expectedNames) { + int size = invalidations.size(); + Assert.assertTrue("Size was " + size + ". Entries were: " + invalidations.keySet(), size >= low); + Assert.assertTrue("Size was " + size + ". Entries were: " + invalidations.keySet(), size <= high); + + for (String expected : expectedNames) { + Assert.assertTrue("Can't find " + expected + ". Entries were: " + invalidations.keySet(), invalidations.keySet().contains(expected)); + } + } + + private KeycloakSession commit(KeycloakRule rule, KeycloakSession session, boolean sleepAfterCommit) throws Exception { + session.getTransactionManager().commit(); + session.close(); + + if (sleepAfterCommit) { + Thread.sleep(SLEEP_TIME_MS); + } + + return rule.startSession(); + } + + private void cacheEverything() throws Exception { + KeycloakSession session1 = server1.startSession(); + TestCacheUtils.cacheRealmWithEverything(session1, REALM_NAME); + session1 = commit(server1, session1, false); + + KeycloakSession session2 = server2.startSession(); + TestCacheUtils.cacheRealmWithEverything(session2, REALM_NAME); + session2 = commit(server1, session2, false); + } + + + @Listener(observation = Listener.Observation.PRE) + public static class TestListener { + + private final String name; + private final Cache cache; // Just for debugging + + private Map invalidations = new ConcurrentHashMap<>(); + + public TestListener(String name, Cache cache) { + this.name = name; + this.cache = cache; + } + + @CacheEntryRemoved + public void cacheEntryRemoved(CacheEntryRemovedEvent event) { + logger.infof("%s: Invalidated %s: %s", name, event.getKey(), event.getValue()); + invalidations.put(event.getKey().toString(), event.getValue()); + } + + Map getInvalidationsAndClear() { + Map newMap = new HashMap<>(invalidations); + invalidations.clear(); + return newMap; + } + + } + + +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java index 80f663f846..60d92d9891 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java @@ -336,6 +336,7 @@ public class UserSessionPersisterProviderTest { resetSession(); + Assert.assertEquals(1, persister.getUserSessionsCount(true)); loadedSessions = loadPersistedSessionsPaginated(true, 10, 1, 1); UserSessionModel persistedSession = loadedSessions.get(0); UserSessionProviderTest.assertSession(persistedSession, session.users().getUserByUsername("user2", realm), "127.0.0.3", started, started, "test-app"); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java index f2fc3aab0d..824200d521 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java @@ -74,8 +74,12 @@ public class UserSessionProviderTest { UserModel user2 = session.users().getUserByUsername("user2", realm); UserManager um = new UserManager(session); - um.removeUser(realm, user1); - um.removeUser(realm, user2); + if (user1 != null) { + um.removeUser(realm, user1); + } + if (user2 != null) { + um.removeUser(realm, user2); + } kc.stopSession(session, true); } @@ -528,11 +532,12 @@ public class UserSessionProviderTest { resetSession(); - session.sessions().onUserRemoved(realm, session.users().getUserByUsername("user1", realm)); + UserModel user1 = session.users().getUserByUsername("user1", realm); + new UserManager(session).removeUser(realm, user1); resetSession(); - assertTrue(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user1", realm)).isEmpty()); + assertTrue(session.sessions().getUserSessions(realm, user1).isEmpty()); assertFalse(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user2", realm)).isEmpty()); assertNull(session.sessions().getUserLoginFailure(realm, "user1")); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/CacheCommands.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/CacheCommands.java new file mode 100644 index 0000000000..0c7eff0450 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/CacheCommands.java @@ -0,0 +1,115 @@ +/* + * 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.testsuite.util.cli; + +import java.util.Map; +import java.util.Set; + +import org.infinispan.Cache; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; + +/** + * @author Marek Posolda + */ +public class CacheCommands { + + public static class ListCachesCommand extends AbstractCommand { + + @Override + public String getName() { + return "listCaches"; + } + + @Override + protected void doRunCommand(KeycloakSession session) { + InfinispanConnectionProvider ispnProvider = session.getProvider(InfinispanConnectionProvider.class); + Set cacheNames = ispnProvider.getCache("realms").getCacheManager().getCacheNames(); + log.infof("Available caches: %s", cacheNames); + } + + } + + + public static class GetCacheCommand extends AbstractCommand { + + @Override + public String getName() { + return "getCache"; + } + + @Override + protected void doRunCommand(KeycloakSession session) { + String cacheName = getArg(0); + InfinispanConnectionProvider ispnProvider = session.getProvider(InfinispanConnectionProvider.class); + Cache cache = ispnProvider.getCache(cacheName); + if (cache == null) { + log.errorf("Cache '%s' doesn't exist", cacheName); + throw new HandledException(); + } + + printCache(cache); + } + + private void printCache(Cache cache) { + int size = cache.size(); + log.infof("Cache %s, size: %d", cache.getName(), size); + + if (size > 50) { + log.info("Skip printing cache recors due to big size"); + } else { + for (Map.Entry entry : cache.entrySet()) { + log.infof("%s=%s", entry.getKey(), entry.getValue()); + } + } + } + + @Override + public String printUsage() { + return super.printUsage() + " . cache-name is name of the infinispan cache provided by InfinispanConnectionProvider"; + } + + } + + + public static class CacheRealmObjectsCommand extends AbstractCommand { + + @Override + public String getName() { + return "cacheRealmObjects"; + } + + @Override + protected void doRunCommand(KeycloakSession session) { + String realmName = getArg(0); + RealmModel realm = session.realms().getRealmByName(realmName); + if (realm == null) { + log.errorf("Realm not found: %s", realmName); + throw new HandledException(); + } + + TestCacheUtils.cacheRealmWithEverything(session, realmName); + } + + @Override + public String printUsage() { + return super.printUsage() + " "; + } + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/RoleCommands.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/RoleCommands.java new file mode 100644 index 0000000000..2c71c72116 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/RoleCommands.java @@ -0,0 +1,127 @@ +/* + * 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.testsuite.util.cli; + +import java.util.HashSet; +import java.util.Set; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionTask; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleContainerModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.utils.KeycloakModelUtils; + +/** + * @author Marek Posolda + */ +public class RoleCommands { + + public static class CreateRoles extends AbstractCommand { + + private String rolePrefix; + private String roleContainer; + + @Override + public String getName() { + return "createRoles"; + } + + private class StateHolder { + int firstInThisBatch; + int countInThisBatch; + int remaining; + }; + + @Override + protected void doRunCommand(KeycloakSession session) { + rolePrefix = getArg(0); + roleContainer = getArg(1); + int first = getIntArg(2); + int count = getIntArg(3); + int batchCount = getIntArg(4); + + final StateHolder state = new StateHolder(); + state.firstInThisBatch = first; + state.remaining = count; + state.countInThisBatch = Math.min(batchCount, state.remaining); + while (state.remaining > 0) { + KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), new KeycloakSessionTask() { + + @Override + public void run(KeycloakSession session) { + createRolesInBatch(session, roleContainer, rolePrefix, state.firstInThisBatch, state.countInThisBatch); + } + }); + + // update state + state.firstInThisBatch = state.firstInThisBatch + state.countInThisBatch; + state.remaining = state.remaining - state.countInThisBatch; + state.countInThisBatch = Math.min(batchCount, state.remaining); + } + + log.infof("Command finished. All roles from %s to %s created", rolePrefix + first, rolePrefix + (first + count - 1)); + } + + private void createRolesInBatch(KeycloakSession session, String roleContainer, String rolePrefix, int first, int count) { + RoleContainerModel container = getRoleContainer(session, roleContainer); + + int last = first + count; + for (int counter = first; counter < last; counter++) { + String roleName = rolePrefix + counter; + RoleModel role = container.addRole(roleName); + } + log.infof("Roles from %s to %s created", rolePrefix + first, rolePrefix + (last - 1)); + } + + private RoleContainerModel getRoleContainer(KeycloakSession session, String roleContainer) { + String[] parts = roleContainer.split("/"); + String realmName = parts[0]; + + RealmModel realm = session.realms().getRealmByName(realmName); + if (realm == null) { + log.errorf("Unknown realm: %s", realmName); + throw new HandledException(); + } + + if (parts.length == 1) { + return realm; + } else { + String clientId = parts[1]; + ClientModel client = session.realms().getClientByClientId(clientId, realm); + if (client == null) { + log.errorf("Unknown client: %s", clientId); + throw new HandledException(); + } + + return client; + } + } + + @Override + public String printUsage() { + return super.printUsage() + " . " + + "\n'total-count' refers to total count of newly created roles. 'batch-size' refers to number of created roles in each transaction. 'starting-role-offset' refers to starting role offset." + + "\nFor example if 'starting-role-offset' is 15 and total-count is 10 and role-prefix is 'test', it will create roles test15, test16, test17, ... , test24" + + "\n'role-container' is either realm (then use just realmName like 'demo' or client (then use realm/clientId like 'demo/my-client' .\n" + + "Example usage: " + super.printUsage() + " test demo 0 500 100"; + } + + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestCacheUtils.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestCacheUtils.java new file mode 100644 index 0000000000..9792f9dff6 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestCacheUtils.java @@ -0,0 +1,88 @@ +/* + * 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.testsuite.util.cli; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientTemplateModel; +import org.keycloak.models.FederatedIdentityModel; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleContainerModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; + +/** + * @author Marek Posolda + */ +public class TestCacheUtils { + + public static void cacheRealmWithEverything(KeycloakSession session, String realmName) { + RealmModel realm = session.realms().getRealmByName(realmName); + + for (ClientModel client : realm.getClients()) { + realm.getClientById(client.getId()); + realm.getClientByClientId(client.getClientId()); + + cacheRoles(session, realm, client); + } + + cacheRoles(session, realm, realm); + + for (GroupModel group : realm.getTopLevelGroups()) { + cacheGroupRecursive(realm, group); + } + + for (ClientTemplateModel clientTemplate : realm.getClientTemplates()) { + realm.getClientTemplateById(clientTemplate.getId()); + } + + for (UserModel user : session.users().getUsers(realm)) { + session.users().getUserById(user.getId(), realm); + if (user.getEmail() != null) { + session.users().getUserByEmail(user.getEmail(), realm); + } + session.users().getUserByUsername(user.getUsername(), realm); + + session.users().getConsents(realm, user.getId()); + + for (FederatedIdentityModel fedIdentity : session.users().getFederatedIdentities(user, realm)) { + session.users().getUserByFederatedIdentity(fedIdentity, realm); + } + } + } + + private static void cacheRoles(KeycloakSession session, RealmModel realm, RoleContainerModel roleContainer) { + for (RoleModel role : roleContainer.getRoles()) { + realm.getRoleById(role.getId()); + roleContainer.getRole(role.getName()); + if (roleContainer instanceof RealmModel) { + session.realms().getRealmRole(realm, role.getName()); + } else { + session.realms().getClientRole(realm, (ClientModel) roleContainer, role.getName()); + } + } + } + + private static void cacheGroupRecursive(RealmModel realm, GroupModel group) { + realm.getGroupById(group.getId()); + for (GroupModel sub : group.getSubGroups()) { + cacheGroupRecursive(realm, sub); + } + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java index 8e9582bfd0..9b2c17aaca 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java @@ -57,7 +57,11 @@ public class TestsuiteCLI { UserCommands.Remove.class, UserCommands.Count.class, UserCommands.GetUser.class, - SyncDummyFederationProviderCommand.class + SyncDummyFederationProviderCommand.class, + RoleCommands.CreateRoles.class, + CacheCommands.ListCachesCommand.class, + CacheCommands.GetCacheCommand.class, + CacheCommands.CacheRealmObjectsCommand.class }; private final KeycloakSessionFactory sessionFactory; diff --git a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json index 06b4e5266f..3f4ddd1a1e 100755 --- a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json @@ -97,7 +97,10 @@ "default": { "clustered": "${keycloak.connectionsInfinispan.clustered:false}", "async": "${keycloak.connectionsInfinispan.async:false}", - "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}" + "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}", + "remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:false}", + "remoteStoreHost": "${keycloak.connectionsInfinispan.remoteStoreHost:localhost}", + "remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}" } }, diff --git a/testsuite/integration/src/test/resources/log4j.properties b/testsuite/integration/src/test/resources/log4j.properties index f0ff6ac6e3..2fa1d70fbc 100755 --- a/testsuite/integration/src/test/resources/log4j.properties +++ b/testsuite/integration/src/test/resources/log4j.properties @@ -46,7 +46,8 @@ log4j.logger.org.keycloak.connections.jpa.updater.liquibase=${keycloak.liquibase # log4j.logger.org.keycloak.models.sessions.infinispan.initializer=trace # Enable to view cache activity -# log4j.logger.org.keycloak.models.cache=trace +#log4j.logger.org.keycloak.cluster.infinispan=trace +#log4j.logger.org.keycloak.models.cache.infinispan=debug # Enable to view database updates # log4j.logger.org.keycloak.connections.mongo.updater.DefaultMongoUpdaterProvider=debug diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_lt.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_lt.properties index c40c438911..d4ffc54d8f 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_lt.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_lt.properties @@ -120,7 +120,7 @@ realm-tab-email=El. pa\u0161tas realm-tab-themes=Temos realm-tab-cache=Pod\u0117lis realm-tab-tokens=Raktai -realm-tab-client-initial-access=Pradiniai prieigos raktai +realm-tab-client-registration=Klient\u0173 registracija realm-tab-security-defenses=Saugos priemon\u0117s realm-tab-general=Bendra informacija add-realm=Prid\u0117ti srit\u012F @@ -207,6 +207,8 @@ include-authnstatement=\u012Etraukti AuthnStatement include-authnstatement.tooltip=Ar prisijungimo b\u016Bdas ir laikas \u0161ur\u0117t\u0173 b\u016Bti \u012Ftraukiami \u012F prisijungimo operacijos atsakym\u0105? sign-documents=Pasira\u0161yti dokumentus sign-documents.tooltip=Ar SAML dokumentai turi b\u016Bt\u012F pasira\u0161omi \u0161ios srities? +sign-documents-redirect-enable-key-info-ext=Optimizuoti REDIRECT pasira\u0161ymo rakto paie\u0161k\u0105 +sign-documents-redirect-enable-key-info-ext.tooltip=Ar privalo b\u016Bti itrauktas pasira\u0161ymo rakto ID \u012F SAML protokolo \u017Einut\u0117s element\u0105 kuomet pasira\u0161omi Keycloak REDIRECT SP s\u0105sajos dokumentai? Tokiu b\u016Bdu tikrinan\u010Dioji pus\u0117 optimizuoja tikrinimo proce\u0105 naudodama tik vien\u0105 rakt\u0105 vietoj to, kad bandyt\u0173 vis\u0173 rakt\u0173 kombinacijas. sign-assertions=Pasira\u0161yti sprendinius sign-assertions.tooltip=Ar SAML sprendiniai SAML dokumentuose turi b\u016Bti pasira\u0161omi? \u0160is nustatymas neb\u016Btinas, kuomet naudojamas viso dokumento pasira\u0161ymas. signature-algorithm=Para\u0161o algoritmas @@ -432,6 +434,10 @@ store-tokens=Saugoti raktus identity-provider.store-tokens.tooltip=Jei \u012Fgalinta, tuomet po naudotoj\u0173 prisijungimo, prieigos raktai bus i\u0161saugoti. stored-tokens-readable=Saugoti raktus skaitomame formate identity-provider.stored-tokens-readable.tooltip=Jei \u012Fgalinta, tuomet naudotojai gali per\u017Ei\u016Br\u0117ti i\u0161saugotus prieigos raktus. \u012Egalinama broker.read-token rol\u0117. +disableUserInfo=U\u017Edrausti naudotojo informacijos prieig\u0105 +identity-provider.disableUserInfo.tooltip=Ar u\u017Edrausti prieig\u0105 prie papildomos naudotojo profilio informacijos per User Info paslaug\u0105? Numatyta reik\u0161m\u0117 - naudoti \u0161i\u0105 OIDC paslaug\u0105. +userIp=Naudoti userIp parametr\u0105 +identity-provider.google-userIp.tooltip=Ar kvie\u010Diant Google naudotojo informacijos paslaug\u0105 naudoti 'userIp' u\u017Eklausos parametr\u0105? Nusta\u010Dius bus naudojamas naudotojo IP adresas. Nustatymas naudingas tuo atveju, jei Google ribot\u0173 u\u017Eklaus\u0173 kiek\u0161 i\u0161 vieno IP adreso. update-profile-on-first-login=Profilio duomen\u0173 atnaujinimas pirmojo prisijungimo metu on=On off=Off @@ -500,8 +506,8 @@ force-authentication=Priverstin\u0117 autentifikacija identity-provider.force-authentication.tooltip=Jei \u012Fgalinta, tuomet tapatyb\u0117s teik\u0117jas privalo autentifikuoti naudotoj\u0105 i\u0161 naujo nepasitikint ankstesniu prisijungimu. validate-signature=Para\u0161o tikrinimas saml.validate-signature.tooltip=\u012Ejungti/i\u0161jungti SAML atsakym\u0173 para\u0161o tikrinim\u0105. -validating-x509-certificate=X509 sertifikatas tikrinimui -validating-x509-certificate.tooltip=PEM formato sertifikatas, kuris turi b\u016Bti naudojamas para\u0161\u0173 tikrinimui. +validating-x509-certificate=X509 sertifikatai tikrinimui +validating-x509-certificate.tooltip=PEM formato sertifikatai, kurie turi b\u016Bti naudojami para\u0161\u0173 tikrinimui. Reik\u0161m\u0117s skiriamos kableliais (,). saml.import-from-url.tooltip=Importuoti metaduomenis i\u0161 nutolusio IDP SAML subjekto apra\u0161o. social.client-id.tooltip=Kliento identifikatorius u\u017Eregistruotas tapatyb\u0117s teik\u0117jo sistemoje. social.client-secret.tooltip=Kliento saugos kodas u\u017Eregistruotas tapatyb\u0117s teik\u0117jo sistemoje. @@ -532,6 +538,7 @@ remainingCount=Lik\u0119s kiekis created=Sukurta back=Atgal initial-access-tokens=Pradiniai prieigos raktai +initial-access-tokens.tooltip=Pradiniai prieigos raktai naudojami klient\u0173 registracijoms dinaminiu b\u016Bdu. U\u017Eklausos su \u0161iais raktais gali b\u016Bti siun\u010Diami i\u0161 bet kurio serverio. add-initial-access-tokens=Prid\u0117ti pradin\u012F prieigos rakt\u0105 initial-access-token=Pradinis prieigos raktas initial-access.copyPaste.tooltip=Nukopijuokite ir \u012Fklijuokite prieigos rakt\u0105 prie\u0161 i\u0161eidami i\u0161 \u0161io puslapio. V\u0117liau negal\u0117site kopijuoti \u0161i\u0173 prieigos rakt\u0173. @@ -540,15 +547,29 @@ initial-access-token.confirm.title=Kopijuoti pradinius prieigos raktus initial-access-token.confirm.text=Pra\u0161ome \u012Fsitikinti, kad nusikopijavote pradinius prieigos raktus nes v\u0117liau prie rakt\u0173 nebegal\u0117site prieiti no-initial-access-available=N\u0117ra galim\u0173 pradini\u0173 prieigos rak\u0161\u0173 -trusted-hosts-legend=Patikimi kliento registracijos serveriai -trusted-hosts-legend.tooltip=Serveri\u0173 vardai, kuriais pasitikima kliento registracijos metu. Klient\u0173 registravimo u\u017Eklausos i\u0161 \u0161i\u0173 serveri\u0173 gali b\u016Bti siun\u010Diamos be pradini\u0173 prieigos rakt\u0173. Klient\u0173 registracijos skai\u010Dius ribojamas pagal nurodyt\u0105 kiekvieno serverio limit\u0105. -no-client-trusted-hosts-available=N\u0117ra galim\u0173 patikim\u0173 serveri\u0173 -add-client-reg-trusted-host=Prid\u0117ti patikim\u0105 server\u012F -hostname=Serverio vardas -client-reg-hostname.tooltip=Pilnas serverio vardas arba IP adresas. Klient\u0173 registracijomis su \u0161iuo serverio vardu arba IP adresu bus pasitikima ir leid\u017Eiama nauj\u0173 klient\u0173 registracija. -client-reg-count.tooltip=Limitas, kiek registravimo u\u017Eklaus\u0173 galima atsi\u0173sti i\u0161 kiekvieno serverio. Limitas bus atkurtas tik po atk\u016Brimo. -client-reg-remainingCount.tooltip=I\u0161 \u0161io serverio lik\u0119s galim\u0173 registracijos u\u017Eklaus\u0173 skai\u010Dius. Limitas bus atkurtas tik po atk\u016Brimo. -reset-remaining-count=Atk\u016Brimo limit\u0105 +client-reg-policies=Klient\u0173 registravimo taisykl\u0117s +client-reg-policy.name.tooltip=Taisykl\u0117s rodomas pavadinimas +anonymous-policies=Anonimin\u0117s prieigos taisykl\u0117s +anonymous-policies.tooltip=\u0160ios taisykl\u0117s naudojamos tuomet, kai klient\u0173 registravimo paslauga i\u0161kvie\u010Diama neautentifikuota u\u017Eklausa. T.y. u\u017Eklausa neturi nei pradini\u0173 prieigos rakt\u0173 (Initial Access Token) nei prieigos rakt\u0173 (Bearer Token). +auth-policies=Autentifikuotos prieigos taisykl\u0117s +auth-policies.tooltip=\u0160ios taisykl\u0117s naudojamos tuomet, kai klient\u0173 registravimo paslauga i\u0161kvie\u010Diama autentifikuota u\u017Eklausa. T.y. u\u017Eklausa turi pradini\u0173 prieigos rakt\u0173 (Initial Access Token) arba prieigos rakt\u0173 (Bearer Token). +policy-name=Taisykl\u0117s pavadinimas +no-client-reg-policies-configured=N\u0117ra klient\u0173 registravimo taisykli\u0173 +trusted-hosts.label=Patikimi serveriai +trusted-hosts.tooltip=Serveri\u0173 s\u0105ra\u0161as, kuriems suteikiama teis\u0117 kviesti klient\u0173 registravimo paslaug\u0105 (Client Registration Service) ir/arba naudototi \u0161ias reik\u0161mes klient\u0173 URI parametre (Client URI). Galima naudoti serveri\u0173 vardus arba IP adresus. Jei kaip pirmas simbolis naudojamas i\u0161ple\u010Diantis simbolis (pvz '*.example.com') tuomet visas domenas 'example.com' bus patikimas. +host-sending-registration-request-must-match.label=Klient\u0173 registracijos paslaugos naudotojo serverio vardas turi sutapti +host-sending-registration-request-must-match.tooltip=Jei \u0161galinta, tuomet visos klient\u0173 registravimo u\u017Eklausos leid\u017Eiamos tik tuo atveju, jei jos buvo i\u0161si\u0173stos i\u0161 to pa\u010Dio patikimo serverio ar domeno. +client-uris-must-match.label=Klient\u0173 URI turi sutapti +client-uris-must-match.tooltip=Jei \u012Fgalinta, tuomet visos klient\u0173 nuorodos (nukreipimo nuorodos ir kitos) leid\u017Eiamos tik tuo atveju, jei jos sutampa su patikimu serverio vardu arba domenu. +allowed-protocol-mappers.label=Leid\u017Eiami protokolo atitikmen\u0173 parink\u0117jai +allowed-protocol-mappers.tooltip=Nurodykite visus leid\u017Eiamus protokolo atitikmen\u0173 parink\u0117jus. Jei bandoma registruoti klient\u0105, kuris turi protokolo atitikmen\u0173 parink\u0117j\u0105 ne\u0161traukt\u0105 \u0161 leid\u017Eiam\u0173 s\u0105ra\u0161\u0105, tuomet visa registracijos u\u017Eklausa bus atmesta. +consent-required-for-all-mappers.label=Privalomas vis\u0173 atitikmen\u0173 parink\u0117j\u0173 pritarimas +consent-required-for-all-mappers.label=Consent Required For Mappers +consent-required-for-all-mappers.tooltip=Jei \u012Fgalinta, tuomet visi naujai u\u017Eregistruotiems protokolo parink\u0117jams automati\u0161kai \u012Fgalinama consentRequired opcija. Tai rei\u0161kia, kad naudotojas privalo pateikti patvirtinim\u0105. PASTABA: Patvirtinimo ekranas rodomas tik tiems klientams, kuriems \u012Fjungtas consentRequired nustatymas. Da\u017Eniausiai geriausia nustatyti \u0161i\u0105 nuostat\u0105 kartu su consent-required taisykle. +allowed-client-templates.label=Leid\u017Eiami klient\u0173 \u0161ablonai +allowed-client-templates.tooltip=Leid\u017Eiam\u0173 kliento \u0161ablon\u0173 s\u0105ra\u0161as, kuriuos galima naudoti naujai registruojamiems klientams. Bandant registruoti klient\u0105 naudojant kliento \u0161ablon\u0105, kurio n\u0117ra s\u0105ra\u0161e bus atmestas. Pradin\u0117 reik\u0161m\u0117 - tu\u0161\u010Dias s\u0105ra\u0161as, t.y. neleid\u017Eiamas nei vienas kliento \u0161ablonas. +max-clients.label=Mksimalus srities klient\u0173 skai\u010Dius +max-clients.tooltip=Nauj\u0173 klient\u0173 registracija draud\u017Eiama, jei u\u017Eregistruot\u0173 klient\u0173 skai\u010Dius yra toks pats arba didesnis nei nustatytas limitas. client-templates=Klient\u0173 \u0161ablonai client-templates.tooltip=Klient\u0173 \u0161ablonai leid\u017Eia nurodyti bendr\u0105 vis\u0173 klient\u0173 konfig\u016Bracij\u0105 @@ -880,6 +901,8 @@ spi=SPI granted-roles=Suteiktos rol\u0117s granted-protocol-mappers=Suteiktos protokolo atitikmen\u0173 s\u0105sajos additional-grants=Papildomai suteikta +consent-created-date=Sukurta +consent-last-updated-date=Pask. kart\u0105 atnaujinta revoke=At\u0161aukti new-password=Naujas slapta\u017Eodis password-confirmation=Pakartotas slapta\u017Eodis @@ -1144,3 +1167,52 @@ authz-evaluation-policies.tooltip=Informacija apie vertinime dalyvavusias taisyk authz-evaluation-authorization-data=Atsakymas authz-evaluation-authorization-data.tooltip=Autorizavimo u\u017Eklausos apdorojimo rezultatas su autorizacijos duomenimis. Rezultatas parodo k\u0105 Keycloak gr\u0105\u017Eina klientui pra\u0161an\u010Diam leidimo. Per\u017Ei\u016Br\u0117kite 'authorization' teigin\u012F su leidimais, kurie buvo suteikti \u0161iai autorizacijos u\u017Eklausai. authz-show-authorization-data=Rodyti autorizacijos duomenis + +kid=KID +keys=Raktai +all=Visi +status=B\u016Bsena +keystore=Rakt\u0173 saugykla +keystores=Rakt\u0173 saugyklos +add-keystore=Prid\u0117ti rakt\u0173 saugykl\u0105 +add-keystore.placeholder=Prid\u0117ti rakt\u0173 saugykl\u0105... +view=\u017Di\u016Br\u0117ti +active=Aktyvus + +Sunday=Sekmadienis +Monday=Pirmadienis +Tuesday=Antradienis +Wednesday=Tre\u010Diadienis +Thursday=Ketvirtadienis +Friday=Penktadienis +Saturday=\u0160e\u0161tadienis + +user-storage-cache-policy=Pod\u0117lio nustatymai +userStorage.cachePolicy=Pod\u0117lio taisykl\u0117s +userStorage.cachePolicy.option.DEFAULT=DEFAULT +userStorage.cachePolicy.option.EVICT_WEEKLY=EVICT_WEEKLY +userStorage.cachePolicy.option.EVICT_DAILY=EVICT_DAILY +userStorage.cachePolicy.option.MAX_LIFESPAN=MAX_LIFESPAN +userStorage.cachePolicy.option.NO_CACHE=NO_CACHE +userStorage.cachePolicy.tooltip=Saugyklos teik\u0117jo pod\u0117lio nustatymai. 'DEFAULT' naudojami numatytieji globalaus naudotojo pod\u0117lio nustatymai. 'EVICT_DAILY' naudotoj\u0173 pod\u0117lis i\u0161valomas kiekvien\u0105 dien\u0105 numatytuoju laiku. 'EVICT_WEEKLY' naudotoj\u0173 pod\u0117lis i\u0161valomas kart\u0105 \u012F savait\u0119 numatyt\u0105 dien\u0105. 'MAX-LIFESPAN' maksimalus pod\u0117lio \u012Fra\u0161o galiojimo laikas milisekund\u0117mis. +userStorage.cachePolicy.evictionDay=I\u0161valymo diena +userStorage.cachePolicy.evictionDay.tooltip=Savait\u0117s diena, kuomet pod\u0117lio \u012Fra\u0161ai taps nebeaktual\u016Bs +userStorage.cachePolicy.evictionHour=I\u0161valymo valanda +userStorage.cachePolicy.evictionHour.tooltip=Valanda, kuomet pod\u0117lio \u012Fra\u0161ai taps nebeaktual\u016Bs. +userStorage.cachePolicy.evictionMinute=I\u0161valymo minut\u0117 +userStorage.cachePolicy.evictionMinute.tooltip=Minut\u0117, kuomet pod\u0117lio \u012Fra\u0161ai taps nebeaktual\u016Bs. +userStorage.cachePolicy.maxLifespan=Maksimalus galiojimo laikas +userStorage.cachePolicy.maxLifespan.tooltip=Maksimalus galiojimo laikas milisekund\u0117mis po kurio pod\u0117lio \u012Fra\u0161ai taps nebeaktual\u016Bs. +user-origin-link=Saugojimo kilm\u0117 + +disable=I\u0161jungti +disableable-credential-types=I\u0161jungiami tipai +credentials.disableable.tooltip=Galim\u0173 i\u0161jungti prisijungimo duomen\u0173 tip\u0173 s\u0105ra\u0161as +disable-credential-types=I\u0161jungti prisijungimo duomen\u0173 tipus +credentials.disable.tooltip=Paspauskite mygtuk\u0105 nor\u0117dami i\u0161jungti pa\u017Eym\u0117tus prisijungimo duomen\u0173 tipus +credential-types=Prisijungimo duomen\u0173 tipai +manage-user-password=Tvarkyti slapta\u017Eod\u017Eius +disable-credentials=I\u0161jungti prisijungimo duomenis +credential-reset-actions=Prisijungimo duomen\u0173 atk\u016Brimas +ldap-mappers=LDAP atitikmen\u0173 parink\u0117jai +create-ldap-mapper=Sukurti LDAP atitikmen\u0173 parink\u0117j\u0105 \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index 624e9a5733..564313c860 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -1352,7 +1352,11 @@ module.controller('ClientScopeMappingCtrl', function($scope, $http, realm, clien } - + $scope.hideRoleSelector = function() { + return ($scope.client.useTemplateScope && $scope.template && template.fullScopeAllowed) + || (!$scope.template && $scope.client.fullScopeAllowed); + } + $scope.changeFlag = function() { Client.update({ realm : realm.realm, diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js index c3b7e1fb89..36b432fd7c 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js @@ -337,7 +337,7 @@ module.controller('UserTabCtrl', function($scope, $location, Dialog, Notificatio module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser, User, Components, UserFederationInstances, UserImpersonation, RequiredActions, - $location, Dialog, Notifications) { + $location, $http, Dialog, Notifications) { $scope.realm = realm; $scope.create = !user.id; $scope.editUsername = $scope.create || $scope.realm.editUsernameAllowed; @@ -362,7 +362,16 @@ module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser }); }; if(user.federationLink) { - console.log("federationLink is not null"); + console.log("federationLink is not null. It is " + user.federationLink); + + // TODO: This is temporary and should be removed once we remove userFederation SPI. It can be replaced with Components.get below + var fedUrl = authUrl + '/admin/realms/' + realm.realm + '/user-federation/instances-with-fallback/' + user.federationLink; + $http.get(fedUrl).success(function(data, status, headers, config) { + $scope.federationLinkName = data.federationLinkName; + $scope.federationLink = data.federationLink; + }); + + /* if (user.federationLink.startsWith('f:')) { Components.get({realm: realm.realm, componentId: user.federationLink}, function (link) { $scope.federationLinkName = link.name; @@ -373,7 +382,7 @@ module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser $scope.federationLinkName = link.displayName; $scope.federationLink = "#/realms/" + realm.realm + "/user-federation/providers/" + link.providerName + "/" + link.id; }); - } + }*/ } else { console.log("federationLink is null"); @@ -627,14 +636,21 @@ module.controller('UserFederationCtrl', function($scope, $location, $route, real for (var i = 0; i < $scope.providers.length; i++) { $scope.providers[i].isUserFederationProvider = false; } - /* + UserFederationProviders.query({realm: realm.realm}, function(data) { for (var i = 0; i < data.length; i++) { data[i].isUserFederationProvider = true; + + var existingProvider = $scope.providers.find(function(provider){ return provider.id == data[i].id }); + if (existingProvider) { + angular.copy(data[i], existingProvider); + continue; + } + $scope.providers.push(data[i]); } }); - */ + $scope.addProvider = function(provider) { console.log('Add provider: ' + provider.id); @@ -1712,6 +1728,7 @@ module.controller('LDAPUserStorageCtrl', function($scope, $location, Notificatio } $scope.changed = false; + $scope.lastVendor = instance.config['vendor'][0]; } initUserStorageSettings(); @@ -1724,7 +1741,7 @@ module.controller('LDAPUserStorageCtrl', function($scope, $location, Notificatio } if (!angular.equals($scope.instance.config['vendor'][0], $scope.lastVendor)) { - console.log("LDAP vendor changed"); + console.log("LDAP vendor changed. Previous=" + $scope.lastVendor + " New=" + $scope.instance.config['vendor'][0]); $scope.lastVendor = $scope.instance.config['vendor'][0]; if ($scope.lastVendor === "ad") { @@ -1771,8 +1788,8 @@ module.controller('LDAPUserStorageCtrl', function($scope, $location, Notificatio $scope.save = function() { $scope.changed = false; - if (!parseInt($scope.instance.config['batchSizeForSync'[0]])) { - $scope.instance.config['batchSizeForSync'][0] = DEFAULT_BATCH_SIZE; + if (!parseInt($scope.instance.config['batchSizeForSync'][0])) { + $scope.instance.config['batchSizeForSync'] = [ DEFAULT_BATCH_SIZE ]; } else { $scope.instance.config['batchSizeForSync'][0] = parseInt($scope.instance.config.batchSizeForSync).toString(); } diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-list.html index 84600c097c..2574f2d1b7 100644 --- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-list.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-list.html @@ -23,14 +23,14 @@
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappings.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappings.html index 47ab627fbd..fc10e6bf66 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappings.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappings.html @@ -41,7 +41,7 @@ -
+
@@ -132,6 +132,6 @@
-
+ \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-ldap.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-ldap.html index 089a65f442..a4f17a8a5c 100644 --- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-ldap.html +++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-ldap.html @@ -1,6 +1,6 @@

- {{instance.displayName|capitalize}} + {{instance.name|capitalize}}

{{:: 'add-user-federation-provider' | translate}}

diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml index 840fa2ca24..3c8f429e1d 100755 --- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml +++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml @@ -92,10 +92,10 @@ - - + + - +