keycloak-memoizeit
Changes
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java 8(+6 -2)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java 77(+49 -28)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java 11(+10 -1)
authz/client/pom.xml 12(+11 -1)
authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java 2(+1 -1)
authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProviderFactory.java 2(+1 -1)
authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProviderFactory.java 2(+1 -1)
core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java 12(+12 -0)
dependencies/server-all/pom.xml 4(+4 -0)
distribution/adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-core/main/module.xml 1(+1 -0)
distribution/adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-authz-client/main/module.xml 42(+42 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/eclipse/sisu/main/module.xml 1(+1 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml 3(+3 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml 2(+1 -1)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-sssd-federation/main/module.xml 1(+1 -0)
examples/providers/rest/README.md 4(+2 -2)
federation/sssd/pom.xml 5(+1 -4)
misc/CrossDataCenter.md 116(+116 -0)
model/infinispan/pom.xml 4(+4 -0)
model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java 93(+93 -0)
model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java 92(+22 -70)
model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java 105(+64 -41)
model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java 204(+204 -0)
model/infinispan/src/main/java/org/keycloak/cluster/infinispan/KeycloakHotRodMarshallerFactory.java 33(+33 -0)
model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java 54(+44 -10)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java 9(+1 -8)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/ClientTemplateQuery.java 11(+0 -11)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/GroupListQuery.java 4(+1 -3)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/RoleListQuery.java 3(+2 -1)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientAddedEvent.java 55(+55 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientRemovedEvent.java 74(+74 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientTemplateEvent.java 52(+52 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientUpdatedEvent.java 55(+55 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupAddedEvent.java 54(+54 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupMovedEvent.java 64(+64 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupRemovedEvent.java 59(+59 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/GroupUpdatedEvent.java 52(+52 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/InvalidationEvent.java 43(+43 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmCacheInvalidationEvent.java 31(+31 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmRemovedEvent.java 53(+53 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RealmUpdatedEvent.java 53(+53 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleAddedEvent.java 53(+53 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleRemovedEvent.java 55(+55 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/RoleUpdatedEvent.java 55(+55 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheInvalidationEvent.java 31(+31 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserCacheRealmInvalidationEvent.java 51(+51 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserConsentsUpdatedEvent.java 51(+51 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkRemovedEvent.java 72(+72 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFederationLinkUpdatedEvent.java 50(+50 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserFullInvalidationEvent.java 78(+78 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/UserUpdatedEvent.java 57(+57 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java 19(+14 -5)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java 22(+17 -5)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java 137(+40 -97)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java 198(+127 -71)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/ClientQueryPredicate.java 48(+0 -48)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/ClientTemplateQueryPredicate.java 40(+0 -40)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/GroupQueryPredicate.java 40(+0 -40)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/RealmQueryPredicate.java 40(+0 -40)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/RoleQueryPredicate.java 40(+0 -40)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java 68(+61 -7)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java 117(+66 -51)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java 4(+2 -2)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java 8(+7 -1)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java 13(+10 -3)
model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java 231(+231 -0)
model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProviderFactory.java 5(+0 -5)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java 31(+24 -7)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProviderFactory.java 5(+0 -5)
pom.xml 13(+6 -7)
README.md 2(+1 -1)
server-spi-private/src/main/java/org/keycloak/credential/hash/PasswordHashProviderFactory.java 0(+0 -0)
server-spi-private/src/main/java/org/keycloak/credential/hash/Pbkdf2PasswordHashProvider.java 0(+0 -0)
server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java 0(+0 -0)
server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProviderFactory.java 22(+22 -0)
services/src/main/java/org/keycloak/authentication/authenticators/browser/ConditionalOtpFormAuthenticator.java 74(+60 -14)
services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java 4(+0 -4)
services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java 105(+81 -24)
services/src/main/java/org/keycloak/protocol/oidc/mappers/UserClientRoleMappingMapper.java 54(+43 -11)
services/src/main/java/org/keycloak/protocol/oidc/mappers/UserRealmRoleMappingMapper.java 17(+4 -13)
services/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java 8(+8 -0)
services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java 17(+10 -7)
services/src/main/java/org/keycloak/services/resources/admin/UserFederationProvidersResource.java 30(+30 -0)
testsuite/integration/pom.xml 4(+4 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java 2(+1 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterInvalidationTest.java 420(+420 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java 1(+1 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java 13(+9 -4)
testsuite/integration-arquillian/pom.xml 28(+28 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingExportImportResource.java 9(+9 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java 4(+4 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/migration/MigrationTestExecutionDecider.java 8(+6 -2)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingExportImportResource.java 6(+6 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java 31(+7 -24)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java 4(+3 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomAuthFlowOTPTest.java 104(+69 -35)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/FlowTest.java 27(+27 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/SAMLClientRegistrationTest.java 16(+10 -6)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java 6(+1 -5)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/LegacyImportTest.java 112(+76 -36)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LoginStatusIframeEndpointTest.java 28(+28 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java 153(+146 -7)
testsuite/integration-arquillian/tests/base/src/test/resources/clientreg-test/saml-entity-descriptor.xml 16(+14 -2)
testsuite/integration-arquillian/tests/base/src/test/resources/exportimport-test/kc11-exported-realm.json 286(+2 -284)
testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json 5(+4 -1)
testsuite/integration-arquillian/tests/other/adapters/jboss/eap/src/test/java/org/keycloak/testsuite/adapter/EAPOIDCKerberosLdapAdapterTest.java 13(+0 -13)
testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/EAP6OIDCKerberosLdapAdapterTest.java 13(+0 -13)
testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EAP6DefaultAuthzConfigAdapterTest.java 28(+28 -0)
testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EAP6PhotozExampleAdapterTest.java 29(+29 -0)
testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/example/authorization/EAP6ServletAuthzAdapterTest.java 30(+30 -0)
testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/WildflyOIDCKerberosLdapAdapterTest.java 13(+0 -13)
testsuite/integration-arquillian/tests/other/sssd/src/test/java/org/keycloak/testsuite/sssd/SSSDTest.java 2(+2 -0)
Details
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 121adf1..472afb7 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 9377b0b..0186e18 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("Could not find a configuration for path [%s]", path);
- response.sendError(403, "Could not find a configuration for path [" + path + "].");
+ LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
- return createEmptyAuthorizationContext(false);
- }
+ if (pathConfig == null) {
+ if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
+ return createAuthorizationContext(accessToken);
+ }
- PathConfig actualPathConfig = resolvePathConfig(pathConfig, request);
- Set<String> requiredScopes = getRequiredScopes(actualPathConfig, request);
+ LOGGER.debugf("Could not find a configuration for path [%s]", path);
+ response.sendError(403, "Could not find a configuration for path [" + path + "].");
- if (isAuthorized(actualPathConfig, requiredScopes, accessToken, httpFacade)) {
- try {
- return createAuthorizationContext(accessToken);
- } catch (Exception e) {
- throw new RuntimeException("Error processing path [" + actualPathConfig.getPath() + "].", e);
- }
- }
+ return createEmptyAuthorizationContext(false);
+ }
+
+ if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
+ return createEmptyAuthorizationContext(true);
+ }
+
+ PathConfig actualPathConfig = resolvePathConfig(pathConfig, request);
+ Set<String> requiredScopes = getRequiredScopes(actualPathConfig, request);
- if (!challenge(actualPathConfig, requiredScopes, httpFacade)) {
- LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
- response.sendError(403, "Authorization failed.");
+ if (isAuthorized(actualPathConfig, requiredScopes, accessToken, httpFacade)) {
+ try {
+ return createAuthorizationContext(accessToken);
+ } catch (Exception e) {
+ throw new RuntimeException("Error processing path [" + actualPathConfig.getPath() + "].", e);
+ }
+ }
+
+ if (!challenge(actualPathConfig, requiredScopes, httpFacade)) {
+ LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
+ response.sendError(403, "Authorization failed.");
+ }
+ }
}
return createEmptyAuthorizationContext(false);
@@ -125,14 +137,17 @@ public abstract class AbstractPolicyEnforcer {
}
List<Permission> permissions = authorization.getPermissions();
+ boolean hasPermission = false;
for (Permission permission : permissions) {
if (permission.getResourceSetId() != null) {
if (isResourcePermission(actualPathConfig, permission)) {
+ hasPermission = true;
+
if (actualPathConfig.isInstance() && !matchResourcePermission(actualPathConfig, permission)) {
continue;
-
}
+
if (hasResourceScopePermission(requiredScopes, permission, actualPathConfig)) {
LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, permissions);
if (request.getMethod().equalsIgnoreCase("DELETE") && actualPathConfig.isInstance()) {
@@ -143,11 +158,16 @@ public abstract class AbstractPolicyEnforcer {
}
} else {
if (hasResourceScopePermission(requiredScopes, permission, actualPathConfig)) {
+ hasPermission = true;
return true;
}
}
}
+ if (!hasPermission && EnforcementMode.PERMISSIVE.equals(actualPathConfig.getEnforcementMode())) {
+ return true;
+ }
+
LOGGER.debugf("Authorization FAILED for path [%s]. No enough permissions [%s].", actualPathConfig, permissions);
return false;
@@ -218,6 +238,7 @@ public abstract class AbstractPolicyEnforcer {
config.setScopes(originalConfig.getScopes());
config.setMethods(originalConfig.getMethods());
config.setParentConfig(originalConfig);
+ config.setEnforcementMode(originalConfig.getEnforcementMode());
this.paths.add(config);
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 ff694bf..37b8f3d 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<PathConfig> configurePaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
- if (enforcerConfig.getPaths().isEmpty()) {
+ boolean loadPathsFromServer = true;
+
+ for (PathConfig pathConfig : enforcerConfig.getPaths()) {
+ if (!PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
+ loadPathsFromServer = false;
+ break;
+ }
+ }
+
+ if (loadPathsFromServer) {
LOGGER.info("No path provided in configuration.");
return configureAllPathsForResourceServer(protectedResource);
} else {
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 e516e34..f9a1e77 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<String> getFriendlyAttributes(String friendlyName) {
- List<String> list = friendlyAttributes.get(name);
+ List<String> list = friendlyAttributes.get(friendlyName);
if (list != null) {
return Collections.unmodifiableList(list);
} else {
authz/client/pom.xml 12(+11 -1)
diff --git a/authz/client/pom.xml b/authz/client/pom.xml
index bf3f6b8..d053a99 100644
--- a/authz/client/pom.xml
+++ b/authz/client/pom.xml
@@ -18,6 +18,8 @@
<description>KeyCloak AuthZ: Client API</description>
<properties>
+ <maven.compiler.source>1.7</maven.compiler.source>
+ <maven.compiler.target>1.7</maven.compiler.target>
<keycloak.osgi.export>
org.keycloak.authorization.client.*
</keycloak.osgi.export>
@@ -63,6 +65,14 @@
<build>
<plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>${maven.compiler.source}</source>
+ <target>${maven.compiler.target}</target>
+ </configuration>
+ </plugin>
<!-- Adding OSGI metadata to the JAR without changing the packaging type. -->
<plugin>
<artifactId>maven-jar-plugin</artifactId>
@@ -98,4 +108,4 @@
</plugins>
</build>
-</project>
\ No newline at end of file
+</project>
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 be83987..d132620 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<R> {
private HttpMethodResponse<R> response;
public HttpMethod(Configuration configuration, RequestBuilder builder) {
- this(configuration, builder, new HashMap<>(), new HashMap<>());
+ this(configuration, builder, new HashMap<String, String>(), new HashMap<String, String>());
}
public HttpMethod(Configuration configuration, RequestBuilder builder, HashMap<String, String> params, HashMap<String, String> headers) {
@@ -155,4 +155,4 @@ public class HttpMethod<R> {
}
};
}
-}
\ 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 4155240..fceca19 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<R> {
});
}
- public HttpMethodResponse<R> json(Class<R> responseType) {
+ public HttpMethodResponse<R> json(final Class<R> responseType) {
return new HttpMethodResponse<R>(this.method) {
@Override
public R execute() {
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 301bb7b..67de87a 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 9461474..fdeeac0 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 0e1e208..b3305ae 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 05bb97d..a14594b 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<Permission> getPermissions() {
- return this.authzToken.getAuthorization().getPermissions();
+ if (this.authzToken == null) {
+ return Collections.emptyList();
+ }
+
+ Authorization authorization = this.authzToken.getAuthorization();
+
+ if (authorization == null) {
+ return Collections.emptyList();
+ }
+
+ return Collections.unmodifiableList(authorization.getPermissions());
}
public boolean isGranted() {
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 0c3faf8..db874c0 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<String> scopes = Collections.emptyList();
private String id;
+ @JsonProperty("enforcement-mode")
+ private EnforcementMode enforcementMode = EnforcementMode.ENFORCING;
+
@JsonIgnore
private PathConfig parentConfig;
@@ -173,6 +176,14 @@ public class PolicyEnforcerConfig {
return id;
}
+ public EnforcementMode getEnforcementMode() {
+ return enforcementMode;
+ }
+
+ public void setEnforcementMode(EnforcementMode enforcementMode) {
+ this.enforcementMode = enforcementMode;
+ }
+
@Override
public String toString() {
return "PathConfig{" +
@@ -181,6 +192,7 @@ public class PolicyEnforcerConfig {
", path='" + path + '\'' +
", scopes=" + scopes +
", id='" + id + '\'' +
+ ", enforcerMode='" + enforcementMode + '\'' +
'}';
}
dependencies/server-all/pom.xml 4(+4 -0)
diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml
index 526a0cf..3fdf489 100755
--- a/dependencies/server-all/pom.xml
+++ b/dependencies/server-all/pom.xml
@@ -256,6 +256,10 @@
</exclusions>
</dependency>
<dependency>
+ <groupId>org.sonatype.sisu.inject</groupId>
+ <artifactId>guice-servlet</artifactId>
+ </dependency>
+ <dependency>
<groupId>org.sonatype.plexus</groupId>
<artifactId>plexus-cipher</artifactId>
</dependency>
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 15c4b6a..7419fa2 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 @@
+
<!--
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
@@ -39,6 +40,9 @@
<include>org/keycloak/keycloak-as7-subsystem/**</include>
<include>org/keycloak/keycloak-adapter-subsystem/**</include>
<include>org/keycloak/keycloak-servlet-oauth-client/**</include>
+
+ <!-- Authorization -->
+ <include>org/keycloak/keycloak-authz-client/**</include>
</includes>
<excludes>
<exclude>**/*.war</exclude>
diff --git a/distribution/adapters/as7-eap6-adapter/as7-modules/build.xml b/distribution/adapters/as7-eap6-adapter/as7-modules/build.xml
index 829a4d6..d22273f 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 @@
<maven-resource group="org.keycloak" artifact="keycloak-servlet-oauth-client"/>
</module-def>
+ <!-- Authorization -->
+ <module-def name="org.keycloak.keycloak-authz-client">
+ <maven-resource group="org.keycloak" artifact="keycloak-authz-client"/>
+ </module-def>
</target>
<target name="clean-target">
diff --git a/distribution/adapters/as7-eap6-adapter/as7-modules/pom.xml b/distribution/adapters/as7-eap6-adapter/as7-modules/pom.xml
index 732e7c7..e484d8d 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 @@
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-json-provider</artifactId>
</dependency>
+ <!-- Authorization -->
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-client</artifactId>
+ </dependency>
</dependencies>
<build>
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 984cb50..21ea5ed 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 @@
<module name="org.keycloak.keycloak-common"/>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-adapter-spi"/>
+ <module name="org.keycloak.keycloak-authz-client"/>
</dependencies>
</module>
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 0000000..3cd1abd
--- /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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+
+
+<!--
+ ~ JBoss, Home of Professional Open Source.
+ ~ Copyright 2016 Red Hat, Inc., and individual contributors
+ ~ as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<module xmlns="urn:jboss:module:1.1" name="org.keycloak.keycloak-authz-client">
+ <resources>
+ <!-- Insert resources here -->
+ </resources>
+ <dependencies>
+ <module name="org.bouncycastle" />
+ <module name="javax.api"/>
+ <module name="javax.activation.api"/>
+ <module name="sun.jdk" optional="true" />
+ <module name="javax.ws.rs.api"/>
+ <module name="org.keycloak.keycloak-core"/>
+ <module name="org.keycloak.keycloak-common"/>
+ <module name="org.apache.httpcomponents"/>
+ <module name="com.fasterxml.jackson.core.jackson-core"/>
+ <module name="com.fasterxml.jackson.core.jackson-annotations"/>
+ <module name="com.fasterxml.jackson.core.jackson-databind"/>
+ <module name="com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider"/>
+ </dependencies>
+
+</module>
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 c69ea6b..1fafc63 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 @@
<include>org/keycloak/keycloak-as7-subsystem/**</include>
<include>org/keycloak/keycloak-adapter-subsystem/**</include>
<include>org/keycloak/keycloak-servlet-oauth-client/**</include>
+
+ <!-- Authorization -->
+ <include>org/keycloak/keycloak-authz-client/**</include>
</includes>
<excludes>
<exclude>**/*.war</exclude>
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 3861d46..55b92dc 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 @@
<resources>
<artifact name="${org.eclipse.sisu:org.eclipse.sisu.inject}"/>
<artifact name="${org.eclipse.sisu:org.eclipse.sisu.plexus}"/>
+ <artifact name="${org.sonatype.sisu.inject:guice-servlet}"/>
</resources>
<dependencies>
<module name="javax.api"/>
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 a80a008..e7fdb8a 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 @@
<module name="org.keycloak.keycloak-server-spi"/>
<module name="org.keycloak.keycloak-server-spi-private"/>
<module name="org.infinispan"/>
+ <module name="org.infinispan.commons"/>
+ <module name="org.infinispan.cachestore.remote"/>
+ <module name="org.infinispan.client.hotrod"/>
<module name="org.jboss.logging"/>
<module name="javax.api"/>
</dependencies>
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 9db480c..cd71511 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 @@
<module name="org.keycloak.keycloak-js-adapter" services="import"/>
<module name="org.keycloak.keycloak-kerberos-federation" services="import"/>
<module name="org.keycloak.keycloak-ldap-federation" services="import"/>
- <module name="org.keycloak.keycloak-sssd-federation" services="import"/>
+ <module name="org.keycloak.keycloak-sssd-federation" optional="true" services="import"/>
<module name="org.keycloak.keycloak-server-spi" services="import"/>
<module name="org.keycloak.keycloak-server-spi-private" services="import"/>
<module name="org.keycloak.keycloak-model-jpa" services="import"/>
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 58939eb..6d56d6e 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 @@
<resources>
<artifact name="${org.keycloak:keycloak-sssd-federation}"/>
+ <resource-root path="/usr/share/java/jna.jar"/>
</resources>
<dependencies>
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 17fd5f0..a3b85f1 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")
examples/providers/rest/README.md 4(+2 -2)
diff --git a/examples/providers/rest/README.md b/examples/providers/rest/README.md
index d78e9bc..9d26627 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
<provider>module:org.keycloak.examples.hello-rest-example</provider>
</providers>
-Then start (or restart) the server. Once started open http://localhost:8080/realms/master/hello and you should see the message _Hello master_.
-You can also invoke the endpoint for other realms by replacing `master` with the realm name in the above url.
\ 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 0000000..b2ee966
--- /dev/null
+++ b/examples/saml/servlet-filter/src/main/webapp/META-INF/jboss-deployment-structure.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<jboss-deployment-structure>
+<deployment>
+ <dependencies>
+ <module name="org.apache.httpcomponents" />
+ </dependencies>
+</deployment>
+</jboss-deployment-structure>
\ No newline at end of file
federation/sssd/pom.xml 5(+1 -4)
diff --git a/federation/sssd/pom.xml b/federation/sssd/pom.xml
index 29113f7..a9029c4 100644
--- a/federation/sssd/pom.xml
+++ b/federation/sssd/pom.xml
@@ -49,6 +49,7 @@
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
+ <scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
@@ -70,10 +71,6 @@
<artifactId>jboss-logging</artifactId>
<scope>provided</scope>
</dependency>
- <dependency>
- <groupId>com.github.jnr</groupId>
- <artifactId>jnr-unixsocket</artifactId>
- </dependency>
</dependencies>
</project>
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 0000000..4088d46
--- /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 <a href="mailto:bruno@abstractj.org">Bruno Oliveira</a>.
+ */
+public class LibraryLoader {
+
+ private static final String[] PATHS = {"/usr/lib/", "/usr/lib64/", "/usr/local/lib/", "/opt/local/lib/"};
+ private static final String LIBRARY_NAME = "libunix_dbus_java";
+ private static final String VERSION = "0.0.8";
+ private static boolean loadSucceeded;
+
+ public static LibraryLoader load() {
+ for (String path : PATHS) {
+ try {
+ System.load(String.format("%s/%s.so.%s", path, LIBRARY_NAME, VERSION));
+ loadSucceeded = true;
+ break;
+ } catch (UnsatisfiedLinkError e) {
+ loadSucceeded = false;
+ }
+ }
+
+ return new LibraryLoader();
+ }
+
+ public boolean succeed() {
+ return loadSucceeded;
+ }
+}
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 0000000..24fd20c
--- /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 7537f01..8851637 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 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 UnixSocketChannel channel;
+ 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 0000000..0baba47
--- /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/cx/ath/matthew/unix/USInputStream.java b/federation/sssd/src/main/java/cx/ath/matthew/unix/USInputStream.java
index b11609f..eb143fe 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 <a href="https://github.com/jruby/jruby/blob/master/core/src/main/java/org/jruby/ext/socket/RubyUNIXSocket.java">RubyUNIXSocket.java</a>
- */
- private int receive(byte[] dataBytes, int off, int len) {
- int recvStatus = -1;
- try {
- InputStream inputStream = Channels.newInputStream(channel);
- recvStatus = inputStream.read(dataBytes, off, len);
-
- } catch (IOException e) {
- e.printStackTrace();
- }
-
- return recvStatus;
- }
}
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 1855b26..d8c85a7 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 <a href="https://github.com/jruby/jruby/blob/master/core/src/main/java/org/jruby/ext/socket/RubyUNIXSocket.java">RubyUNIXSocket.java</a>
- */
- private void send(int sock, ByteBuffer[] outIov) {
-
- final POSIX posix = POSIXFactory.getNativePOSIX();
- MsgHdr outMessage = posix.allocateMsgHdr();
-
- outMessage.setIov(outIov);
-
- CmsgHdr outControl = outMessage.allocateControl(4);
- outControl.setLevel(SocketLevel.SOL_SOCKET.intValue());
- outControl.setType(0x01);
-
- ByteBuffer fdBuf = ByteBuffer.allocateDirect(4);
- fdBuf.order(ByteOrder.nativeOrder());
- fdBuf.putInt(0, channel.getFD());
- outControl.setData(fdBuf);
-
- posix.sendmsg(sock, outMessage, 0);
-
- }
-
- private void send(int sock, byte[] dataBytes, int off, int len) {
- ByteBuffer[] outIov = new ByteBuffer[1];
- outIov[0] = ByteBuffer.allocateDirect(dataBytes.length);
- outIov[0].put(dataBytes, off, len);
- outIov[0].flip();
-
- send(sock, outIov);
- }
-
- protected void send(int sock, byte[] dataBytes) {
- ByteBuffer[] outIov = new ByteBuffer[1];
- outIov[0] = ByteBuffer.allocateDirect(dataBytes.length);
- outIov[0].put(dataBytes);
- outIov[0].flip();
-
- send(sock, outIov);
- }
}
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 2a53426..45e8cb7 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 0c997bd..1745bcf 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 {
misc/CrossDataCenter.md 116(+116 -0)
diff --git a/misc/CrossDataCenter.md b/misc/CrossDataCenter.md
new file mode 100644
index 0000000..4146eaa
--- /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" :
+
+```
+<local-cache name="work" start="EAGER" batching="false" />
+```
+
+- Start server:
+```
+cd JDG_HOME/bin
+./standalone.sh -Djboss.socket.binding.port-offset=100
+```
+
+Keycloak servers setup
+----------------------
+You need to setup 2 Keycloak nodes in this way.
+
+For now, it's recommended to test Keycloak overlay on EAP7 because of infinispan bug, which is fixed in EAP 7.0 (infinispan 8.1.2), but not
+yet on Wildfly 10 (infinispan 8.1.0). See below for details.
+
+1) Configure shared database in KEYCLOAK_HOME/standalone/configuration/standalone.xml . For example MySQL
+
+2) Add `module` attribute to the infinispan keycloak container:
+
+```
+<cache-container name="keycloak" jndi-name="infinispan/Keycloak" module="org.keycloak.keycloak-model-infinispan">
+```
+
+3) Configure `work` cache to use remoteStore. You should use this:
+
+```
+<local-cache name="work">
+ <remote-store passivation="false" fetch-state="false" purge="false" preload="false" shared="true" cache="work" remote-servers="remote-cache">
+ <property name="rawValues">true</property>
+ <property name="marshaller">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>
+ </remote-store>
+</local-cache>
+```
+
+4) Configure connection to the external JDG server. Because we used port offset 100 for JDG (see above), the HotRod endpoint is running on 11322 .
+So add the config like this to the bottom of standalone.xml under `socket-binding-group` element:
+
+```
+<outbound-socket-binding name="remote-cache">
+ <remote-destination host="localhost" port="11322"/>
+</outbound-socket-binding>
+```
+
+5) Optional: Configure logging in standalone.xml to see what invalidation events were send:
+````
+<logger category="org.keycloak.cluster.infinispan">
+ <level name="TRACE"/>
+</logger>
+<logger category="org.keycloak.models.cache.infinispan">
+ <level name="DEBUG"/>
+</logger>
+````
+
+6) Setup Keycloak node2 . Just copy Keycloak to another location on your laptop and repeat steps 1-5 above for second server too.
+
+7) Run server 1 with parameters like (assuming you have virtual hosts "node1" and "node2" defined in your `/etc/hosts` ):
+```
+./standalone.sh -Djboss.node.name=node1 -b node1 -bmanagement node1
+```
+
+and server2 with:
+```
+./standalone.sh -Djboss.node.name=node2 -b node2 -bmanagement node2
+```
+
+8) Note something like this in both `KEYCLOAK_HOME/standalone/log/server.log` on both nodes. Note that cluster Startup Time will be same time on both nodes:
+```
+2016-11-16 22:12:52,080 DEBUG [org.keycloak.cluster.infinispan.InfinispanClusterProviderFactory] (ServerService Thread Pool -- 62) My address: node1-1953169551
+2016-11-16 22:12:52,081 DEBUG [org.keycloak.cluster.infinispan.CrossDCAwareCacheFactory] (ServerService Thread Pool -- 62) RemoteStore is available. Cross-DC scenario will be used
+2016-11-16 22:12:52,119 DEBUG [org.keycloak.cluster.infinispan.InfinispanClusterProviderFactory] (ServerService Thread Pool -- 62) Loaded cluster startup time: Wed Nov 16 22:09:48 CET 2016
+2016-11-16 22:12:52,128 DEBUG [org.keycloak.cluster.infinispan.InfinispanNotificationsManager] (ServerService Thread Pool -- 62) Added listener for HotRod remoteStore cache: work
+```
+
+9) Login to node1. Then change any realm on node2. You will see in the node2 server.log that RealmUpdatedEvent was sent and on node1 that this event was received.
+
+This is done even if node1 and node2 are NOT in cluster as it's the external JDG used for communication between 2 keycloak servers and sending/receiving cache invalidation events. But note that userSession
+doesn't yet work (eg. if you login to node1, you won't see the userSession on node2).
+
+
+WARNING: Previous steps works on Keycloak server overlay deployed on EAP 7.0 . With deploy on Wildfly 10.0.0.Final, you will see exception
+at startup caused by the bug https://issues.jboss.org/browse/ISPN-6203 .
+
+There is a workaround to add this line into KEYCLOAK_HOME/modules/system/layers/base/org/wildfly/clustering/service/main/module.xml :
+
+```
+<module name="org.infinispan.client.hotrod"/>
+```
model/infinispan/pom.xml 4(+4 -0)
diff --git a/model/infinispan/pom.xml b/model/infinispan/pom.xml
index f10fa60..fba921c 100755
--- a/model/infinispan/pom.xml
+++ b/model/infinispan/pom.xml
@@ -49,6 +49,10 @@
<artifactId>infinispan-core</artifactId>
</dependency>
<dependency>
+ <groupId>org.infinispan</groupId>
+ <artifactId>infinispan-cachestore-remote</artifactId>
+ </dependency>
+ <dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
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 0000000..17795ca
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+abstract class CrossDCAwareCacheFactory {
+
+ protected static final Logger logger = Logger.getLogger(CrossDCAwareCacheFactory.class);
+
+
+ abstract BasicCache<String, Serializable> getCache();
+
+
+ static CrossDCAwareCacheFactory getFactory(Cache<String, Serializable> workCache, Set<RemoteStore> remoteStores) {
+ if (remoteStores.isEmpty()) {
+ logger.debugf("No configured remoteStore available. Cross-DC scenario is not used");
+ return new InfinispanCacheWrapperFactory(workCache);
+ } else {
+ logger.debugf("RemoteStore is available. Cross-DC scenario will be used");
+
+ if (remoteStores.size() > 1) {
+ logger.warnf("More remoteStores configured for work cache. Will use just the first one");
+ }
+
+ // For cross-DC scenario, we need to return underlying remoteCache for atomic operations to work properly
+ RemoteStore remoteStore = remoteStores.iterator().next();
+ RemoteCache remoteCache = remoteStore.getRemoteCache();
+ return new RemoteCacheWrapperFactory(remoteCache);
+ }
+ }
+
+
+ // We don't have external JDG configured. No cross-DC.
+ private static class InfinispanCacheWrapperFactory extends CrossDCAwareCacheFactory {
+
+ private final Cache<String, Serializable> workCache;
+
+ InfinispanCacheWrapperFactory(Cache<String, Serializable> workCache) {
+ this.workCache = workCache;
+ }
+
+ @Override
+ BasicCache<String, Serializable> getCache() {
+ return workCache;
+ }
+
+ }
+
+
+ // We have external JDG configured. Cross-DC should be enabled
+ private static class RemoteCacheWrapperFactory extends CrossDCAwareCacheFactory {
+
+ private final RemoteCache<String, Serializable> remoteCache;
+
+ RemoteCacheWrapperFactory(RemoteCache<String, Serializable> remoteCache) {
+ this.remoteCache = remoteCache;
+ }
+
+ @Override
+ BasicCache<String, Serializable> getCache() {
+ // Flags are per-invocation!
+ return remoteCache.withFlags(Flag.FORCE_RETURN_VALUE);
+ }
+
+ }
+}
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 8b77c25..5a4bdb7 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<String, Serializable> cache;
+ private final int clusterStartupTime;
+ private final String myAddress;
+ private final CrossDCAwareCacheFactory crossDCAwareCacheFactory;
+ private final InfinispanNotificationsManager notificationsManager; // Just to extract notifications related stuff to separate class
- public InfinispanClusterProvider(InfinispanClusterProviderFactory factory, KeycloakSession session, Cache<String, Serializable> cache) {
- this.factory = factory;
- this.session = session;
- this.cache = cache;
+ public InfinispanClusterProvider(int clusterStartupTime, String myAddress, CrossDCAwareCacheFactory crossDCAwareCacheFactory, InfinispanNotificationsManager notificationsManager) {
+ this.myAddress = myAddress;
+ this.clusterStartupTime = clusterStartupTime;
+ this.crossDCAwareCacheFactory = crossDCAwareCacheFactory;
+ this.notificationsManager = notificationsManager;
}
@Override
public int getClusterStartupTime() {
- Integer existingClusterStartTime = (Integer) cache.get(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY);
- if (existingClusterStartTime != null) {
- return existingClusterStartTime;
- } else {
- // clusterStartTime not yet initialized. Let's try to put our startupTime
- int serverStartTime = (int) (session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
-
- existingClusterStartTime = (Integer) cache.putIfAbsent(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY, serverStartTime);
- if (existingClusterStartTime == null) {
- logger.debugf("Initialized cluster startup time to %s", Time.toDate(serverStartTime).toString());
- return serverStartTime;
- } else {
- return existingClusterStartTime;
- }
- }
+ return clusterStartupTime;
}
@@ -104,56 +87,33 @@ public class InfinispanClusterProvider implements ClusterProvider {
@Override
public void registerListener(String taskKey, ClusterListener task) {
- factory.registerListener(taskKey, task);
+ this.notificationsManager.registerListener(taskKey, task);
}
@Override
- public void notify(String taskKey, ClusterEvent event) {
- // Put the value to the cache to notify listeners on all the nodes
- cache.put(taskKey, event);
+ public void notify(String taskKey, ClusterEvent event, boolean ignoreSender) {
+ this.notificationsManager.notify(taskKey, event, ignoreSender);
}
- private String getCurrentNode(Cache<String, Serializable> cache) {
- Transport transport = cache.getCacheManager().getTransport();
- return transport==null ? "local" : transport.getAddress().toString();
- }
-
-
- private LockEntry createLockEntry(Cache<String, Serializable> cache) {
+ private LockEntry createLockEntry() {
LockEntry lock = new LockEntry();
- lock.setNode(getCurrentNode(cache));
+ lock.setNode(myAddress);
lock.setTimestamp(Time.currentTime());
return lock;
}
private boolean tryLock(String cacheKey, int taskTimeoutInSeconds) {
- LockEntry myLock = createLockEntry(cache);
+ LockEntry myLock = createLockEntry();
- LockEntry existingLock = (LockEntry) cache.putIfAbsent(cacheKey, myLock);
+ LockEntry existingLock = (LockEntry) crossDCAwareCacheFactory.getCache().putIfAbsent(cacheKey, myLock, taskTimeoutInSeconds, TimeUnit.SECONDS);
if (existingLock != null) {
- // Task likely already in progress. Check if timestamp is not outdated
- int thatTime = existingLock.getTimestamp();
- int currentTime = Time.currentTime();
- if (thatTime + taskTimeoutInSeconds < currentTime) {
- if (logger.isTraceEnabled()) {
- logger.tracef("Task %s outdated when in progress by node %s. Will try to replace task with our node %s", cacheKey, existingLock.getNode(), myLock.getNode());
- }
- boolean replaced = cache.replace(cacheKey, existingLock, myLock);
- if (!replaced) {
- if (logger.isTraceEnabled()) {
- logger.tracef("Failed to replace the task %s. Other thread replaced in the meantime. Ignoring task.", cacheKey);
- }
- }
- return replaced;
- } else {
- if (logger.isTraceEnabled()) {
- logger.tracef("Task %s in progress already by node %s. Ignoring task.", cacheKey, existingLock.getNode());
- }
- return false;
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Task %s in progress already by node %s. Ignoring task.", cacheKey, existingLock.getNode());
}
+ return false;
} else {
if (logger.isTraceEnabled()) {
logger.tracef("Successfully acquired lock for task %s. Our node is %s", cacheKey, myLock.getNode());
@@ -168,20 +128,12 @@ public class InfinispanClusterProvider implements ClusterProvider {
int retry = 3;
while (true) {
try {
- cache.getAdvancedCache()
- .withFlags(Flag.IGNORE_RETURN_VALUES, Flag.FORCE_SYNCHRONOUS)
- .remove(cacheKey);
+ crossDCAwareCacheFactory.getCache().remove(cacheKey);
if (logger.isTraceEnabled()) {
logger.tracef("Task %s removed from the cache", cacheKey);
}
return;
} catch (RuntimeException e) {
- ComponentStatus status = cache.getStatus();
- if (status.isStopping() || status.isTerminated()) {
- logger.warnf("Failed to remove task %s from the cache. Cache is already terminating", cacheKey);
- logger.debug(e.getMessage(), e);
- return;
- }
retry--;
if (retry == 0) {
throw e;
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 75aef45..a96621d 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanClusterProviderFactory implements ClusterProviderFactory {
@@ -57,28 +56,82 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory
protected static final Logger logger = Logger.getLogger(InfinispanClusterProviderFactory.class);
+ // Infinispan cache
private volatile Cache<String, Serializable> workCache;
- private Map<String, ClusterListener> listeners = new HashMap<>();
+ // Ensure that atomic operations (like putIfAbsent) must work correctly in any of: non-clustered, clustered or cross-Data-Center (cross-DC) setups
+ private CrossDCAwareCacheFactory crossDCAwareCacheFactory;
+
+ private String myAddress;
+
+ private int clusterStartupTime;
+
+ // Just to extract notifications related stuff to separate class
+ private InfinispanNotificationsManager notificationsManager;
@Override
public ClusterProvider create(KeycloakSession session) {
lazyInit(session);
- return new InfinispanClusterProvider(this, session, workCache);
+ return new InfinispanClusterProvider(clusterStartupTime, myAddress, crossDCAwareCacheFactory, notificationsManager);
}
private void lazyInit(KeycloakSession session) {
if (workCache == null) {
synchronized (this) {
if (workCache == null) {
- workCache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
+ InfinispanConnectionProvider ispnConnections = session.getProvider(InfinispanConnectionProvider.class);
+ workCache = ispnConnections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
+
workCache.getCacheManager().addListener(new ViewChangeListener());
- workCache.addListener(new CacheEntryListener());
+ initMyAddress();
+
+ Set<RemoteStore> remoteStores = getRemoteStores();
+ crossDCAwareCacheFactory = CrossDCAwareCacheFactory.getFactory(workCache, remoteStores);
+
+ clusterStartupTime = initClusterStartupTime(session);
+
+ notificationsManager = InfinispanNotificationsManager.create(workCache, myAddress, remoteStores);
}
}
}
}
+
+ // See if we have RemoteStore (external JDG) configured for cross-Data-Center scenario
+ private Set<RemoteStore> getRemoteStores() {
+ return workCache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class);
+ }
+
+
+ protected void initMyAddress() {
+ Transport transport = workCache.getCacheManager().getTransport();
+ this.myAddress = transport == null ? HostUtils.getHostName() + "-" + workCache.hashCode() : transport.getAddress().toString();
+ logger.debugf("My address: %s", this.myAddress);
+ }
+
+
+ protected int initClusterStartupTime(KeycloakSession session) {
+ Integer existingClusterStartTime = (Integer) crossDCAwareCacheFactory.getCache().get(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY);
+ if (existingClusterStartTime != null) {
+ logger.debugf("Loaded cluster startup time: %s", Time.toDate(existingClusterStartTime).toString());
+ return existingClusterStartTime;
+ } else {
+ // clusterStartTime not yet initialized. Let's try to put our startupTime
+ int serverStartTime = (int) (session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
+
+ existingClusterStartTime = (Integer) crossDCAwareCacheFactory.getCache().putIfAbsent(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY, serverStartTime);
+ if (existingClusterStartTime == null) {
+ logger.debugf("Initialized cluster startup time to %s", Time.toDate(serverStartTime).toString());
+ return serverStartTime;
+ } else {
+ logger.debugf("Loaded cluster startup time: %s", Time.toDate(existingClusterStartTime).toString());
+ return existingClusterStartTime;
+ }
+ }
+ }
+
+
+
@Override
public void init(Config.Scope config) {
}
@@ -167,34 +220,4 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory
}
- <T> void registerListener(String taskKey, ClusterListener task) {
- listeners.put(taskKey, task);
- }
-
- @Listener
- public class CacheEntryListener {
-
- @CacheEntryCreated
- public void cacheEntryCreated(CacheEntryCreatedEvent<String, Object> event) {
- if (!event.isPre()) {
- trigger(event.getKey(), event.getValue());
- }
- }
-
- @CacheEntryModified
- public void cacheEntryModified(CacheEntryModifiedEvent<String, Object> event) {
- if (!event.isPre()) {
- trigger(event.getKey(), event.getValue());
- }
- }
-
- private void trigger(String key, Object value) {
- ClusterListener task = listeners.get(key);
- if (task != null) {
- ClusterEvent event = (ClusterEvent) value;
- task.run(event);
- }
- }
- }
-
}
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 0000000..57cc003
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class InfinispanNotificationsManager {
+
+ protected static final Logger logger = Logger.getLogger(InfinispanNotificationsManager.class);
+
+ private final MultivaluedHashMap<String, ClusterListener> listeners = new MultivaluedHashMap<>();
+
+ private final Cache<String, Serializable> workCache;
+
+ private final String myAddress;
+
+
+ protected InfinispanNotificationsManager(Cache<String, Serializable> workCache, String myAddress) {
+ this.workCache = workCache;
+ this.myAddress = myAddress;
+ }
+
+
+ // Create and init manager including all listeners etc
+ public static InfinispanNotificationsManager create(Cache<String, Serializable> workCache, String myAddress, Set<RemoteStore> remoteStores) {
+ InfinispanNotificationsManager manager = new InfinispanNotificationsManager(workCache, myAddress);
+
+ // We need CacheEntryListener just if we don't have remoteStore. With remoteStore will be all cluster nodes notified anyway from HotRod listener
+ if (remoteStores.isEmpty()) {
+ workCache.addListener(manager.new CacheEntryListener());
+
+ logger.debugf("Added listener for infinispan cache: %s", workCache.getName());
+ } else {
+ for (RemoteStore remoteStore : remoteStores) {
+ RemoteCache<Object, Object> remoteCache = remoteStore.getRemoteCache();
+ remoteCache.addClientListener(manager.new HotRodListener(remoteCache));
+
+ logger.debugf("Added listener for HotRod remoteStore cache: %s", remoteCache.getName());
+ }
+ }
+
+ return manager;
+ }
+
+
+ void registerListener(String taskKey, ClusterListener task) {
+ listeners.add(taskKey, task);
+ }
+
+
+ void notify(String taskKey, ClusterEvent event, boolean ignoreSender) {
+ WrapperClusterEvent wrappedEvent = new WrapperClusterEvent();
+ wrappedEvent.setDelegateEvent(event);
+ wrappedEvent.setIgnoreSender(ignoreSender);
+ wrappedEvent.setSender(myAddress);
+
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Sending event %s: %s", taskKey, event);
+ }
+
+ // Put the value to the cache to notify listeners on all the nodes
+ workCache.getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES)
+ .put(taskKey, wrappedEvent, 120, TimeUnit.SECONDS);
+ }
+
+
+ @Listener(observation = Listener.Observation.POST)
+ public class CacheEntryListener {
+
+ @CacheEntryCreated
+ public void cacheEntryCreated(CacheEntryCreatedEvent<String, Serializable> event) {
+ eventReceived(event.getKey(), event.getValue());
+ }
+
+ @CacheEntryModified
+ public void cacheEntryModified(CacheEntryModifiedEvent<String, Serializable> event) {
+ eventReceived(event.getKey(), event.getValue());
+ }
+ }
+
+
+ @ClientListener
+ public class HotRodListener {
+
+ private final RemoteCache<Object, Object> remoteCache;
+
+ public HotRodListener(RemoteCache<Object, Object> remoteCache) {
+ this.remoteCache = remoteCache;
+ }
+
+
+ @ClientCacheEntryCreated
+ public void created(ClientCacheEntryCreatedEvent event) {
+ String key = event.getKey().toString();
+ hotrodEventReceived(key);
+ }
+
+
+ @ClientCacheEntryModified
+ public void updated(ClientCacheEntryModifiedEvent event) {
+ String key = event.getKey().toString();
+ hotrodEventReceived(key);
+ }
+
+ private void hotrodEventReceived(String key) {
+ // TODO: Look at CacheEventConverter stuff to possibly include value in the event and avoid additional remoteCache request
+ Object value = remoteCache.get(key);
+
+ Serializable rawValue;
+ if (value instanceof MarshalledEntry) {
+ Object rw = ((MarshalledEntry)value).getValue();
+ rawValue = (Serializable) rw;
+ } else {
+ rawValue = (Serializable) value;
+ }
+
+
+ eventReceived(key, rawValue);
+ }
+
+ }
+
+ private void eventReceived(String key, Serializable obj) {
+ if (!(obj instanceof WrapperClusterEvent)) {
+ return;
+ }
+
+ WrapperClusterEvent event = (WrapperClusterEvent) obj;
+
+ if (event.isIgnoreSender()) {
+ if (this.myAddress.equals(event.getSender())) {
+ return;
+ }
+ }
+
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Received event %s: %s", key, event);
+ }
+
+ ClusterEvent wrappedEvent = event.getDelegateEvent();
+
+ List<ClusterListener> myListeners = listeners.get(key);
+ if (myListeners != null) {
+ for (ClusterListener listener : myListeners) {
+ listener.eventReceived(wrappedEvent);
+ }
+ }
+
+ myListeners = listeners.get(ClusterProvider.ALL);
+ if (myListeners != null) {
+ for (ClusterListener listener : myListeners) {
+ listener.eventReceived(wrappedEvent);
+ }
+ }
+ }
+}
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 0000000..4a73bf3
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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 0000000..b03dd70
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class WrapperClusterEvent implements ClusterEvent {
+
+ private String sender; // will be null in non-clustered environment
+ private boolean ignoreSender;
+ private ClusterEvent delegateEvent;
+
+ public String getSender() {
+ return sender;
+ }
+
+ public void setSender(String sender) {
+ this.sender = sender;
+ }
+
+ public boolean isIgnoreSender() {
+ return ignoreSender;
+ }
+
+ public void setIgnoreSender(boolean ignoreSender) {
+ this.ignoreSender = ignoreSender;
+ }
+
+ public ClusterEvent getDelegateEvent() {
+ return delegateEvent;
+ }
+
+ public void setDelegateEvent(ClusterEvent delegateEvent) {
+ this.delegateEvent = delegateEvent;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("WrapperClusterEvent [ sender=%s, delegateEvent=%s ]", sender, delegateEvent.toString());
+ }
+}
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 8ad75fd..7781e3a 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 ad1ba26..c254ea7 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<String, Long> revisions;
protected final Cache<String, Revisioned> cache;
protected final UpdateCounter counter = new UpdateCounter();
@@ -63,9 +62,10 @@ public abstract class CacheManager {
public CacheManager(Cache<String, Revisioned> cache, Cache<String, Long> revisions) {
this.cache = cache;
this.revisions = revisions;
- this.cache.addListener(this);
}
+ protected abstract Logger getLogger();
+
public Cache<String, Revisioned> getCache() {
return cache;
}
@@ -79,10 +79,7 @@ public abstract class CacheManager {
if (revision == null) {
revision = counter.current();
}
- // if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event
- // so, we do this to force this.
- String invalidationKey = "invalidation.key" + id;
- cache.putForExternalRead(invalidationKey, new AbstractRevisioned(-1L, invalidationKey));
+
return revision;
}
@@ -101,12 +98,16 @@ public abstract class CacheManager {
}
Long rev = revisions.get(id);
if (rev == null) {
- RealmCacheManager.logger.tracev("get() missing rev");
+ if (getLogger().isTraceEnabled()) {
+ getLogger().tracev("get() missing rev {0}", id);
+ }
return null;
}
long oRev = o.getRevision() == null ? -1L : o.getRevision().longValue();
if (rev > oRev) {
- RealmCacheManager.logger.tracev("get() rev: {0} o.rev: {1}", rev.longValue(), oRev);
+ if (getLogger().isTraceEnabled()) {
+ getLogger().tracev("get() rev: {0} o.rev: {1}", rev.longValue(), oRev);
+ }
return null;
}
return o != null && type.isInstance(o) ? type.cast(o) : null;
@@ -114,9 +115,11 @@ public abstract class CacheManager {
public Object invalidateObject(String id) {
Revisioned removed = (Revisioned)cache.remove(id);
- // if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event
- // so, we do this to force the event.
- cache.remove("invalidation.key" + id);
+
+ if (getLogger().isTraceEnabled()) {
+ getLogger().tracef("Removed key='%s', value='%s' from cache", id, removed);
+ }
+
bumpVersion(id);
return removed;
}
@@ -137,37 +140,35 @@ public abstract class CacheManager {
//revisions.getAdvancedCache().lock(id);
Long rev = revisions.get(id);
if (rev == null) {
- if (id.endsWith("realm.clients")) RealmCacheManager.logger.trace("addRevisioned rev == null realm.clients");
rev = counter.current();
revisions.put(id, rev);
}
revisions.startBatch();
if (!revisions.getAdvancedCache().lock(id)) {
- RealmCacheManager.logger.trace("Could not obtain version lock");
+ if (getLogger().isTraceEnabled()) {
+ getLogger().tracev("Could not obtain version lock: {0}", id);
+ }
return;
}
rev = revisions.get(id);
if (rev == null) {
- if (id.endsWith("realm.clients")) RealmCacheManager.logger.trace("addRevisioned rev2 == null realm.clients");
return;
}
if (rev > startupRevision) { // revision is ahead transaction start. Other transaction updated in the meantime. Don't cache
- if (RealmCacheManager.logger.isTraceEnabled()) {
- RealmCacheManager.logger.tracev("Skipped cache. Current revision {0}, Transaction start revision {1}", object.getRevision(), startupRevision);
+ if (getLogger().isTraceEnabled()) {
+ getLogger().tracev("Skipped cache. Current revision {0}, Transaction start revision {1}", object.getRevision(), startupRevision);
}
return;
}
if (rev.equals(object.getRevision())) {
- if (id.endsWith("realm.clients")) RealmCacheManager.logger.tracev("adding Object.revision {0} rev {1}", object.getRevision(), rev);
cache.putForExternalRead(id, object);
return;
}
if (rev > object.getRevision()) { // revision is ahead, don't cache
- if (id.endsWith("realm.clients")) RealmCacheManager.logger.trace("addRevisioned revision is ahead realm.clients");
+ if (getLogger().isTraceEnabled()) getLogger().tracev("Skipped cache. Object revision {0}, Cache revision {1}", object.getRevision(), rev);
return;
}
// revisions cache has a lower value than the object.revision, so update revision and add it to cache
- if (id.endsWith("realm.clients")) RealmCacheManager.logger.tracev("adding Object.revision {0} rev {1}", object.getRevision(), rev);
revisions.put(id, object.getRevision());
if (lifespan < 0) cache.putForExternalRead(id, object);
else cache.putForExternalRead(id, object, lifespan, TimeUnit.MILLISECONDS);
@@ -196,63 +197,36 @@ public abstract class CacheManager {
.filter(predicate).iterator();
}
- @CacheEntryInvalidated
- public void cacheInvalidated(CacheEntryInvalidatedEvent<String, Object> event) {
- if (event.isPre()) {
- String key = event.getKey();
- if (key.startsWith("invalidation.key")) {
- // if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event
- // so, we do this to force this.
- String bump = key.substring("invalidation.key".length());
- RealmCacheManager.logger.tracev("bumping invalidation key {0}", bump);
- bumpVersion(bump);
- return;
- }
- } else {
- //if (!event.isPre()) {
- String key = event.getKey();
- if (key.startsWith("invalidation.key")) {
- // if you do cache.remove() on node 1 and the entry doesn't exist on node 2, node 2 never receives a invalidation event
- // so, we do this to force this.
- String bump = key.substring("invalidation.key".length());
- bumpVersion(bump);
- RealmCacheManager.logger.tracev("bumping invalidation key {0}", bump);
- return;
- }
- bumpVersion(key);
- Object object = event.getValue();
- if (object != null) {
- bumpVersion(key);
- Predicate<Map.Entry<String, Revisioned>> predicate = getInvalidationPredicate(object);
- if (predicate != null) runEvictions(predicate);
- RealmCacheManager.logger.tracev("invalidating: {0}" + object.getClass().getName());
- }
+ public void sendInvalidationEvents(KeycloakSession session, Collection<InvalidationEvent> invalidationEvents) {
+ ClusterProvider clusterProvider = session.getProvider(ClusterProvider.class);
+
+ // Maybe add InvalidationEvent, which will be collection of all invalidationEvents? That will reduce cluster traffic even more.
+ for (InvalidationEvent event : invalidationEvents) {
+ clusterProvider.notify(generateEventId(event), event, true);
}
}
- @CacheEntriesEvicted
- public void cacheEvicted(CacheEntriesEvictedEvent<String, Object> event) {
- if (!event.isPre())
- for (Map.Entry<String, Object> entry : event.getEntries().entrySet()) {
- Object object = entry.getValue();
- bumpVersion(entry.getKey());
- if (object == null) continue;
- RealmCacheManager.logger.tracev("evicting: {0}" + object.getClass().getName());
- Predicate<Map.Entry<String, Revisioned>> predicate = getInvalidationPredicate(object);
- if (predicate != null) runEvictions(predicate);
- }
+ protected String generateEventId(InvalidationEvent event) {
+ return new StringBuilder(event.getId())
+ .append("_")
+ .append(event.hashCode())
+ .toString();
}
- public void runEvictions(Predicate<Map.Entry<String, Revisioned>> current) {
- Set<String> evictions = new HashSet<>();
- addInvalidations(current, evictions);
- RealmCacheManager.logger.tracev("running evictions size: {0}", evictions.size());
- for (String key : evictions) {
- cache.evict(key);
- bumpVersion(key);
+
+ protected void invalidationEventReceived(InvalidationEvent event) {
+ Set<String> invalidations = new HashSet<>();
+
+ addInvalidationsFromEvent(event, invalidations);
+
+ getLogger().debugf("Invalidating %d cache items after received event %s", invalidations.size(), event);
+
+ for (String invalidation : invalidations) {
+ invalidateObject(invalidation);
}
}
- protected abstract Predicate<Map.Entry<String, Revisioned>> getInvalidationPredicate(Object object);
+ protected abstract void addInvalidationsFromEvent(InvalidationEvent event, Set<String> invalidations);
+
}
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 980957a..11c1c62 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/entities/CachedRealm.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
index 4647f74..5dd4bac 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<String> defaultGroups = new LinkedList<String>();
- protected Set<String> groups = new HashSet<String>();
protected List<String> clientTemplates= new LinkedList<>();
protected boolean internationalizationEnabled;
protected Set<String> supportedLocales;
@@ -237,9 +236,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
executionsById.put(execution.getId(), execution);
}
}
- for (GroupModel group : model.getGroups()) {
- groups.add(group.getId());
- }
+
for (AuthenticatorConfigModel authenticator : model.getAuthenticatorConfigs()) {
authenticatorConfigs.put(authenticator.getId(), authenticator);
}
@@ -541,10 +538,6 @@ public class CachedRealm extends AbstractExtendableRevisioned {
return clientAuthenticationFlow;
}
- public Set<String> getGroups() {
- return groups;
- }
-
public List<String> getDefaultGroups() {
return defaultGroups;
}
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 21a73ad..e924c05 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 0000000..1b022ca
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientAddedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String clientUuid;
+ private String clientId;
+ private String realmId;
+
+ public static ClientAddedEvent create(String clientUuid, String clientId, String realmId) {
+ ClientAddedEvent event = new ClientAddedEvent();
+ event.clientUuid = clientUuid;
+ event.clientId = clientId;
+ event.realmId = realmId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return clientUuid;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("ClientAddedEvent [ realmId=%s, clientUuid=%s, clientId=%s ]", realmId, clientUuid, clientId);
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ realmCache.clientAdded(realmId, clientUuid, clientId, invalidations);
+ }
+}
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 0000000..2e620db
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientRemovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String clientUuid;
+ private String clientId;
+ private String realmId;
+ // roleId -> roleName
+ private Map<String, String> clientRoles;
+
+ public static ClientRemovedEvent create(ClientModel client) {
+ ClientRemovedEvent event = new ClientRemovedEvent();
+
+ event.realmId = client.getRealm().getId();
+ event.clientUuid = client.getId();
+ event.clientId = client.getClientId();
+ event.clientRoles = new HashMap<>();
+ for (RoleModel clientRole : client.getRoles()) {
+ event.clientRoles.put(clientRole.getId(), clientRole.getName());
+ }
+
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return clientUuid;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("ClientRemovedEvent [ realmId=%s, clientUuid=%s, clientId=%s, clientRoleIds=%s ]", realmId, clientUuid, clientId, clientRoles);
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ realmCache.clientRemoval(realmId, clientUuid, clientId, invalidations);
+
+ // Separate iteration for all client roles to invalidate records dependent on them
+ for (Map.Entry<String, String> clientRole : clientRoles.entrySet()) {
+ String roleId = clientRole.getKey();
+ String roleName = clientRole.getValue();
+ realmCache.roleRemoval(roleId, roleName, clientUuid, invalidations);
+ }
+ }
+}
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 0000000..7bb13a9
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientTemplateEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String clientTemplateId;
+
+ public static ClientTemplateEvent create(String clientTemplateId) {
+ ClientTemplateEvent event = new ClientTemplateEvent();
+ event.clientTemplateId = clientTemplateId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return clientTemplateId;
+ }
+
+
+ @Override
+ public String toString() {
+ return "ClientTemplateEvent [ " + clientTemplateId + " ]";
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ // Nothing. ID was already invalidated
+ }
+}
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 0000000..cc6c263
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientUpdatedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String clientUuid;
+ private String clientId;
+ private String realmId;
+
+ public static ClientUpdatedEvent create(String clientUuid, String clientId, String realmId) {
+ ClientUpdatedEvent event = new ClientUpdatedEvent();
+ event.clientUuid = clientUuid;
+ event.clientId = clientId;
+ event.realmId = realmId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return clientUuid;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("ClientUpdatedEvent [ realmId=%s, clientUuid=%s, clientId=%s ]", realmId, clientUuid, clientId);
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ realmCache.clientUpdated(realmId, clientUuid, clientId, invalidations);
+ }
+}
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 0000000..77dcf69
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class GroupAddedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String groupId;
+ private String realmId;
+
+ public static GroupAddedEvent create(String groupId, String realmId) {
+ GroupAddedEvent event = new GroupAddedEvent();
+ event.realmId = realmId;
+ event.groupId = groupId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return groupId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("GroupAddedEvent [ realmId=%s, groupId=%s ]", realmId, groupId);
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ realmCache.groupQueriesInvalidations(realmId, invalidations);
+ }
+}
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 0000000..2f5566a
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class GroupMovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String groupId;
+ private String newParentId; // null if moving to top-level
+ private String oldParentId; // null if moving from top-level
+ private String realmId;
+
+ public static GroupMovedEvent create(GroupModel group, GroupModel toParent, String realmId) {
+ GroupMovedEvent event = new GroupMovedEvent();
+ event.realmId = realmId;
+ event.groupId = group.getId();
+ event.oldParentId = group.getParentId();
+ event.newParentId = toParent==null ? null : toParent.getId();
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return groupId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("GroupMovedEvent [ realmId=%s, groupId=%s, newParentId=%s, oldParentId=%s ]", realmId, groupId, newParentId, oldParentId);
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ realmCache.groupQueriesInvalidations(realmId, invalidations);
+ if (newParentId != null) {
+ invalidations.add(newParentId);
+ }
+ if (oldParentId != null) {
+ invalidations.add(oldParentId);
+ }
+ }
+}
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 0000000..37689fa
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class GroupRemovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String groupId;
+ private String parentId;
+ private String realmId;
+
+ public static GroupRemovedEvent create(GroupModel group, String realmId) {
+ GroupRemovedEvent event = new GroupRemovedEvent();
+ event.realmId = realmId;
+ event.groupId = group.getId();
+ event.parentId = group.getParentId();
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return groupId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("GroupRemovedEvent [ realmId=%s, groupId=%s, parentId=%s ]", realmId, groupId, parentId);
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ realmCache.groupQueriesInvalidations(realmId, invalidations);
+ if (parentId != null) {
+ invalidations.add(parentId);
+ }
+ }
+}
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 0000000..c59021b
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class GroupUpdatedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String groupId;
+
+ public static GroupUpdatedEvent create(String groupId) {
+ GroupUpdatedEvent event = new GroupUpdatedEvent();
+ event.groupId = groupId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return groupId;
+ }
+
+
+ @Override
+ public String toString() {
+ return "GroupUpdatedEvent [ " + groupId + " ]";
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ // Nothing. ID already invalidated
+ }
+}
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 0000000..ea59ff5
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class InvalidationEvent implements ClusterEvent {
+
+ public abstract String getId();
+
+ @Override
+ public int hashCode() {
+ return getClass().hashCode() * 13 + getId().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) return false;
+ if (!obj.getClass().equals(this.getClass())) return false;
+
+ InvalidationEvent that = (InvalidationEvent) obj;
+ if (!that.getId().equals(getId())) return false;
+ return true;
+ }
+}
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 0000000..2876e08
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface RealmCacheInvalidationEvent {
+
+ void addInvalidations(RealmCacheManager realmCache, Set<String> 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 0000000..3558757
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RealmRemovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String realmId;
+ private String realmName;
+
+ public static RealmRemovedEvent create(String realmId, String realmName) {
+ RealmRemovedEvent event = new RealmRemovedEvent();
+ event.realmId = realmId;
+ event.realmName = realmName;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return realmId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("RealmRemovedEvent [ realmId=%s, realmName=%s ]", realmId, realmName);
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ realmCache.realmRemoval(realmId, realmName, invalidations);
+ }
+}
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 0000000..624fc6d
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RealmUpdatedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String realmId;
+ private String realmName;
+
+ public static RealmUpdatedEvent create(String realmId, String realmName) {
+ RealmUpdatedEvent event = new RealmUpdatedEvent();
+ event.realmId = realmId;
+ event.realmName = realmName;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return realmId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("RealmUpdatedEvent [ realmId=%s, realmName=%s ]", realmId, realmName);
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ realmCache.realmUpdated(realmId, realmName, invalidations);
+ }
+}
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 0000000..cb393e5
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RoleAddedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String roleId;
+ private String containerId;
+
+ public static RoleAddedEvent create(String roleId, String containerId) {
+ RoleAddedEvent event = new RoleAddedEvent();
+ event.roleId = roleId;
+ event.containerId = containerId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return roleId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("RoleAddedEvent [ roleId=%s, containerId=%s ]", roleId, containerId);
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ realmCache.roleAdded(containerId, invalidations);
+ }
+}
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 0000000..6137b1b
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RoleRemovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String roleId;
+ private String roleName;
+ private String containerId;
+
+ public static RoleRemovedEvent create(String roleId, String roleName, String containerId) {
+ RoleRemovedEvent event = new RoleRemovedEvent();
+ event.roleId = roleId;
+ event.roleName = roleName;
+ event.containerId = containerId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return roleId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("RoleRemovedEvent [ roleId=%s, containerId=%s ]", roleId, containerId);
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ realmCache.roleRemoval(roleId, roleName, containerId, invalidations);
+ }
+}
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 0000000..4b2ae5b
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RoleUpdatedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
+
+ private String roleId;
+ private String roleName;
+ private String containerId;
+
+ public static RoleUpdatedEvent create(String roleId, String roleName, String containerId) {
+ RoleUpdatedEvent event = new RoleUpdatedEvent();
+ event.roleId = roleId;
+ event.roleName = roleName;
+ event.containerId = containerId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return roleId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("RoleUpdatedEvent [ roleId=%s, roleName=%s, containerId=%s ]", roleId, roleName, containerId);
+ }
+
+ @Override
+ public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
+ realmCache.roleUpdated(containerId, roleName, invalidations);
+ }
+}
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 0000000..964e97a
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface UserCacheInvalidationEvent {
+
+ void addInvalidations(UserCacheManager userCache, Set<String> 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 0000000..3996181
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserCacheRealmInvalidationEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
+
+ private String realmId;
+
+ public static UserCacheRealmInvalidationEvent create(String realmId) {
+ UserCacheRealmInvalidationEvent event = new UserCacheRealmInvalidationEvent();
+ event.realmId = realmId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return realmId; // Just a placeholder
+ }
+
+ @Override
+ public String toString() {
+ return String.format("UserCacheRealmInvalidationEvent [ realmId=%s ]", realmId);
+ }
+
+ @Override
+ public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
+ userCache.invalidateRealmUsers(realmId, invalidations);
+ }
+}
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 0000000..021e841
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserConsentsUpdatedEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
+
+ private String userId;
+
+ public static UserConsentsUpdatedEvent create(String userId) {
+ UserConsentsUpdatedEvent event = new UserConsentsUpdatedEvent();
+ event.userId = userId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return userId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("UserConsentsUpdatedEvent [ userId=%s ]", userId);
+ }
+
+ @Override
+ public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
+ userCache.consentInvalidation(userId, invalidations);
+ }
+}
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 0000000..15704df
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserFederationLinkRemovedEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
+
+ private String userId;
+ private String realmId;
+ private String identityProviderId;
+ private String socialUserId;
+
+ public static UserFederationLinkRemovedEvent create(String userId, String realmId, FederatedIdentityModel socialLink) {
+ UserFederationLinkRemovedEvent event = new UserFederationLinkRemovedEvent();
+ event.userId = userId;
+ event.realmId = realmId;
+ if (socialLink != null) {
+ event.identityProviderId = socialLink.getIdentityProvider();
+ event.socialUserId = socialLink.getUserId();
+ }
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return userId;
+ }
+
+ public String getRealmId() {
+ return realmId;
+ }
+
+ public String getIdentityProviderId() {
+ return identityProviderId;
+ }
+
+ public String getSocialUserId() {
+ return socialUserId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("UserFederationLinkRemovedEvent [ userId=%s, identityProviderId=%s, socialUserId=%s ]", userId, identityProviderId, socialUserId);
+ }
+
+ @Override
+ public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
+ userCache.federatedIdentityLinkRemovedInvalidation(userId, realmId, identityProviderId, socialUserId, invalidations);
+ }
+}
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 0000000..8bbfb41
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserFederationLinkUpdatedEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
+
+ private String userId;
+
+ public static UserFederationLinkUpdatedEvent create(String userId) {
+ UserFederationLinkUpdatedEvent event = new UserFederationLinkUpdatedEvent();
+ event.userId = userId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return userId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("UserFederationLinkUpdatedEvent [ userId=%s ]", userId);
+ }
+
+ @Override
+ public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
+ userCache.federatedIdentityLinkUpdatedInvalidation(userId, invalidations);
+ }
+}
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 0000000..d637ac2
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserFullInvalidationEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
+
+ private String userId;
+ private String username;
+ private String email;
+ private String realmId;
+ private boolean identityFederationEnabled;
+ private Map<String, String> federatedIdentities;
+
+ public static UserFullInvalidationEvent create(String userId, String username, String email, String realmId, boolean identityFederationEnabled, Collection<FederatedIdentityModel> federatedIdentities) {
+ UserFullInvalidationEvent event = new UserFullInvalidationEvent();
+ event.userId = userId;
+ event.username = username;
+ event.email = email;
+ event.realmId = realmId;
+
+ event.identityFederationEnabled = identityFederationEnabled;
+ if (identityFederationEnabled) {
+ event.federatedIdentities = new HashMap<>();
+ for (FederatedIdentityModel socialLink : federatedIdentities) {
+ event.federatedIdentities.put(socialLink.getIdentityProvider(), socialLink.getUserId());
+ }
+ }
+
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return userId;
+ }
+
+ public Map<String, String> getFederatedIdentities() {
+ return federatedIdentities;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("UserFullInvalidationEvent [ userId=%s, username=%s, email=%s ]", userId, username, email);
+ }
+
+ @Override
+ public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
+ userCache.fullUserInvalidation(userId, username, email, realmId, identityFederationEnabled, federatedIdentities, invalidations);
+ }
+}
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 0000000..429b4af
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserUpdatedEvent extends InvalidationEvent implements UserCacheInvalidationEvent {
+
+ private String userId;
+ private String username;
+ private String email;
+ private String realmId;
+
+ public static UserUpdatedEvent create(String userId, String username, String email, String realmId) {
+ UserUpdatedEvent event = new UserUpdatedEvent();
+ event.userId = userId;
+ event.username = username;
+ event.email = email;
+ event.realmId = realmId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return userId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("UserUpdatedEvent [ userId=%s, username=%s, email=%s ]", userId, username, email);
+ }
+
+ @Override
+ public void addInvalidations(UserCacheManager userCache, Set<String> invalidations) {
+ userCache.userUpdatedInvalidations(userId, username, email, realmId, invalidations);
+ }
+}
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 4bbe4c7..c2ad8ce 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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -54,14 +54,23 @@ public class InfinispanCacheRealmProviderFactory implements CacheRealmProviderFa
Cache<String, Revisioned> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
Cache<String, Long> revisions = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_NAME);
realmCache = new RealmCacheManager(cache, revisions);
+
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
- cluster.registerListener(REALM_CLEAR_CACHE_EVENTS, new ClusterListener() {
- @Override
- public void run(ClusterEvent event) {
- realmCache.clear();
+ cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> {
+
+ if (event instanceof InvalidationEvent) {
+ InvalidationEvent invalidationEvent = (InvalidationEvent) event;
+ realmCache.invalidationEventReceived(invalidationEvent);
}
});
+ cluster.registerListener(REALM_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> {
+
+ realmCache.clear();
+
+ });
+
+ log.debug("Registered cluster listeners");
}
}
}
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 14d420b..e8c2ba1 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -55,13 +55,25 @@ public class InfinispanUserCacheProviderFactory implements UserCacheProviderFact
Cache<String, Revisioned> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_CACHE_NAME);
Cache<String, Long> revisions = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME);
userCache = new UserCacheManager(cache, revisions);
+
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
- cluster.registerListener(USER_CLEAR_CACHE_EVENTS, new ClusterListener() {
- @Override
- public void run(ClusterEvent event) {
- userCache.clear();
+
+ cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> {
+
+ if (event instanceof InvalidationEvent) {
+ InvalidationEvent invalidationEvent = (InvalidationEvent) event;
+ userCache.invalidationEventReceived(invalidationEvent);
}
+
});
+
+ cluster.registerListener(USER_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> {
+
+ userCache.clear();
+
+ });
+
+ log.debug("Registered cluster listeners");
}
}
}
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 069b34a..1748e3c 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");
}
@@ -732,13 +727,6 @@ public class RealmAdapter implements CachedRealmModel {
}
@Override
- public boolean removeRoleById(String id) {
- cacheSession.registerRoleInvalidation(id);
- getDelegateForUpdate();
- return updated.removeRoleById(id);
- }
-
- @Override
public boolean isEventsEnabled() {
if (isUpdated()) return updated.isEventsEnabled();
return cached.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
@@ -1258,12 +1240,6 @@ public class RealmAdapter implements CachedRealmModel {
}
@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 55e3b38..b01dbab 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-@Listener
public class RealmCacheManager extends CacheManager {
- protected static final Logger logger = Logger.getLogger(RealmCacheManager.class);
-
- public RealmCacheManager(Cache<String, Revisioned> cache, Cache<String, Long> revisions) {
- super(cache, revisions);
- }
-
-
- public void realmInvalidation(String id, Set<String> invalidations) {
- Predicate<Map.Entry<String, Revisioned>> predicate = getRealmInvalidationPredicate(id);
- addInvalidations(predicate, invalidations);
- }
-
- public Predicate<Map.Entry<String, Revisioned>> getRealmInvalidationPredicate(String id) {
- return RealmQueryPredicate.create().realm(id);
- }
-
- public void clientInvalidation(String id, Set<String> invalidations) {
- addInvalidations(getClientInvalidationPredicate(id), invalidations);
- }
-
- public Predicate<Map.Entry<String, Revisioned>> getClientInvalidationPredicate(String id) {
- return ClientQueryPredicate.create().client(id);
- }
-
- public void roleInvalidation(String id, Set<String> invalidations) {
- addInvalidations(getRoleInvalidationPredicate(id), invalidations);
+ private static final Logger logger = Logger.getLogger(RealmCacheManager.class);
+ @Override
+ protected Logger getLogger() {
+ return logger;
}
- public Predicate<Map.Entry<String, Revisioned>> getRoleInvalidationPredicate(String id) {
- return HasRolePredicate.create().role(id);
+ public RealmCacheManager(Cache<String, Revisioned> cache, Cache<String, Long> revisions) {
+ super(cache, revisions);
}
- public void groupInvalidation(String id, Set<String> invalidations) {
- addInvalidations(getGroupInvalidationPredicate(id), invalidations);
+ public void realmUpdated(String id, String name, Set<String> invalidations) {
+ invalidations.add(id);
+ invalidations.add(RealmCacheSession.getRealmByNameCacheKey(name));
}
- public Predicate<Map.Entry<String, Revisioned>> getGroupInvalidationPredicate(String id) {
- return GroupQueryPredicate.create().group(id);
- }
-
- public void clientTemplateInvalidation(String id, Set<String> invalidations) {
- addInvalidations(getClientTemplateInvalidationPredicate(id), invalidations);
+ public void realmRemoval(String id, String name, Set<String> invalidations) {
+ realmUpdated(id, name, invalidations);
+ addInvalidations(InRealmPredicate.create().realm(id), invalidations);
}
- public Predicate<Map.Entry<String, Revisioned>> getClientTemplateInvalidationPredicate(String id) {
- return ClientTemplateQueryPredicate.create().template(id);
+ public void roleAdded(String roleContainerId, Set<String> invalidations) {
+ invalidations.add(RealmCacheSession.getRolesCacheKey(roleContainerId));
}
- public void realmRemoval(String id, Set<String> invalidations) {
- Predicate<Map.Entry<String, Revisioned>> predicate = getRealmRemovalPredicate(id);
- addInvalidations(predicate, invalidations);
+ public void roleUpdated(String roleContainerId, String roleName, Set<String> invalidations) {
+ invalidations.add(RealmCacheSession.getRoleByNameCacheKey(roleContainerId, roleName));
}
- public Predicate<Map.Entry<String, Revisioned>> getRealmRemovalPredicate(String id) {
- Predicate<Map.Entry<String, Revisioned>> predicate = null;
- predicate = RealmQueryPredicate.create().realm(id)
- .or(InRealmPredicate.create().realm(id));
- return predicate;
- }
+ public void roleRemoval(String id, String roleName, String roleContainerId, Set<String> invalidations) {
+ invalidations.add(RealmCacheSession.getRolesCacheKey(roleContainerId));
+ invalidations.add(RealmCacheSession.getRoleByNameCacheKey(roleContainerId, roleName));
- public void clientAdded(String realmId, String id, Set<String> invalidations) {
- addInvalidations(getClientAddedPredicate(realmId), invalidations);
+ addInvalidations(HasRolePredicate.create().role(id), invalidations);
}
- public Predicate<Map.Entry<String, Revisioned>> getClientAddedPredicate(String realmId) {
- return ClientQueryPredicate.create().inRealm(realmId);
+ public void groupQueriesInvalidations(String realmId, Set<String> invalidations) {
+ invalidations.add(RealmCacheSession.getGroupsQueryCacheKey(realmId));
+ invalidations.add(RealmCacheSession.getTopGroupsQueryCacheKey(realmId)); // Just easier to always invalidate top-level too. It's not big performance penalty
}
- public void clientRemoval(String realmId, String id, Set<String> invalidations) {
- Predicate<Map.Entry<String, Revisioned>> predicate = null;
- predicate = getClientRemovalPredicate(realmId, id);
- addInvalidations(predicate, invalidations);
+ public void clientAdded(String realmId, String clientUUID, String clientId, Set<String> invalidations) {
+ invalidations.add(RealmCacheSession.getRealmClientsQueryCacheKey(realmId));
}
- public Predicate<Map.Entry<String, Revisioned>> getClientRemovalPredicate(String realmId, String id) {
- Predicate<Map.Entry<String, Revisioned>> predicate;
- predicate = ClientQueryPredicate.create().inRealm(realmId)
- .or(ClientQueryPredicate.create().client(id))
- .or(InClientPredicate.create().client(id));
- return predicate;
+ public void clientUpdated(String realmId, String clientUuid, String clientId, Set<String> invalidations) {
+ invalidations.add(RealmCacheSession.getClientByClientIdCacheKey(clientId, realmId));
}
- public void roleRemoval(String id, Set<String> invalidations) {
- addInvalidations(getRoleRemovalPredicate(id), invalidations);
+ // Client roles invalidated separately
+ public void clientRemoval(String realmId, String clientUUID, String clientId, Set<String> invalidations) {
+ invalidations.add(RealmCacheSession.getRealmClientsQueryCacheKey(realmId));
+ invalidations.add(RealmCacheSession.getClientByClientIdCacheKey(clientId, realmId));
+ addInvalidations(InClientPredicate.create().client(clientUUID), invalidations);
}
- public Predicate<Map.Entry<String, Revisioned>> getRoleRemovalPredicate(String id) {
- return getRoleInvalidationPredicate(id);
- }
@Override
- protected Predicate<Map.Entry<String, Revisioned>> getInvalidationPredicate(Object object) {
- if (object instanceof CachedRealm) {
- CachedRealm cached = (CachedRealm)object;
- return getRealmRemovalPredicate(cached.getId());
- } else if (object instanceof CachedClient) {
- CachedClient cached = (CachedClient)object;
- Predicate<Map.Entry<String, Revisioned>> predicate = getClientRemovalPredicate(cached.getRealm(), cached.getId());
- return predicate;
- } else if (object instanceof CachedRole) {
- CachedRole cached = (CachedRole)object;
- return getRoleRemovalPredicate(cached.getId());
- } else if (object instanceof CachedGroup) {
- CachedGroup cached = (CachedGroup)object;
- return getGroupInvalidationPredicate(cached.getId());
- } else if (object instanceof CachedClientTemplate) {
- CachedClientTemplate cached = (CachedClientTemplate)object;
- return getClientTemplateInvalidationPredicate(cached.getId());
+ protected void addInvalidationsFromEvent(InvalidationEvent event, Set<String> invalidations) {
+ if (event instanceof RealmCacheInvalidationEvent) {
+ invalidations.add(event.getId());
+
+ ((RealmCacheInvalidationEvent) event).addInvalidations(this, invalidations);
}
- return null;
}
+
}
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 9321f47..d61a611 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<String, GroupAdapter> managedGroups = new HashMap<>();
protected Set<String> listInvalidations = new HashSet<>();
protected Set<String> invalidations = new HashSet<>();
+ protected Set<InvalidationEvent> invalidationEvents = new HashSet<>(); // Events to be sent across cluster
protected boolean clearAll;
protected final long startupRevision;
@@ -150,7 +166,7 @@ public class RealmCacheSession implements CacheRealmProvider {
public void clear() {
cache.clear();
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
- cluster.notify(InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS, new ClearCacheEvent());
+ cluster.notify(InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true);
}
@Override
@@ -167,21 +183,19 @@ public class RealmCacheSession implements CacheRealmProvider {
}
@Override
- public void registerRealmInvalidation(String id) {
- invalidateRealm(id);
- cache.realmInvalidation(id, invalidations);
- }
-
- private void invalidateRealm(String id) {
- invalidations.add(id);
+ public void registerRealmInvalidation(String id, String name) {
+ cache.realmUpdated(id, name, invalidations);
RealmAdapter adapter = managedRealms.get(id);
if (adapter != null) adapter.invalidateFlag();
+
+ invalidationEvents.add(RealmUpdatedEvent.create(id, name));
}
@Override
- public void registerClientInvalidation(String id) {
+ public void registerClientInvalidation(String id, String clientId, String realmId) {
invalidateClient(id);
- cache.clientInvalidation(id, invalidations);
+ invalidationEvents.add(ClientUpdatedEvent.create(id, clientId, realmId));
+ cache.clientUpdated(realmId, id, clientId, invalidations);
}
private void invalidateClient(String id) {
@@ -193,7 +207,9 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public void registerClientTemplateInvalidation(String id) {
invalidateClientTemplate(id);
- cache.clientTemplateInvalidation(id, invalidations);
+ // Note: Adding/Removing client template is supposed to invalidate CachedRealm as well, so the list of clientTemplates is invalidated.
+ // But separate RealmUpdatedEvent will be sent for it. So ClientTemplateEvent don't need to take care of it.
+ invalidationEvents.add(ClientTemplateEvent.create(id));
}
private void invalidateClientTemplate(String id) {
@@ -203,14 +219,15 @@ public class RealmCacheSession implements CacheRealmProvider {
}
@Override
- public void registerRoleInvalidation(String id) {
+ public void registerRoleInvalidation(String id, String roleName, String roleContainerId) {
invalidateRole(id);
- roleInvalidations(id);
+ cache.roleUpdated(roleContainerId, roleName, invalidations);
+ invalidationEvents.add(RoleUpdatedEvent.create(id, roleName, roleContainerId));
}
- private void roleInvalidations(String roleId) {
+ private void roleRemovalInvalidations(String roleId, String roleName, String roleContainerId) {
Set<String> newInvalidations = new HashSet<>();
- cache.roleInvalidation(roleId, newInvalidations);
+ cache.roleRemoval(roleId, roleName, roleContainerId, newInvalidations);
invalidations.addAll(newInvalidations);
// need to make sure that scope and group mapping clients and groups are invalidated
for (String id : newInvalidations) {
@@ -229,6 +246,11 @@ public class RealmCacheSession implements CacheRealmProvider {
clientTemplate.invalidate();
continue;
}
+ RoleAdapter role = managedRoles.get(id);
+ if (role != null) {
+ role.invalidate();
+ continue;
+ }
}
@@ -243,10 +265,26 @@ public class RealmCacheSession implements CacheRealmProvider {
if (adapter != null) adapter.invalidate();
}
+ private void addedRole(String roleId, String roleContainerId) {
+ // this is needed so that a new role that hasn't been committed isn't cached in a query
+ listInvalidations.add(roleContainerId);
+
+ invalidateRole(roleId);
+ cache.roleAdded(roleContainerId, invalidations);
+ invalidationEvents.add(RoleAddedEvent.create(roleId, roleContainerId));
+ }
+
@Override
public void registerGroupInvalidation(String id) {
+ invalidateGroup(id, null, false);
+ addGroupEventIfAbsent(GroupUpdatedEvent.create(id));
+ }
+
+ private void invalidateGroup(String id, String realmId, boolean invalidateQueries) {
invalidateGroup(id);
- cache.groupInvalidation(id, invalidations);
+ if (invalidateQueries) {
+ cache.groupQueriesInvalidations(realmId, invalidations);
+ }
}
private void invalidateGroup(String id) {
@@ -259,6 +297,8 @@ public class RealmCacheSession implements CacheRealmProvider {
for (String id : invalidations) {
cache.invalidateObject(id);
}
+
+ cache.sendInvalidationEvents(session, invalidationEvents);
}
private KeycloakTransaction getPrepareTransaction() {
@@ -358,14 +398,14 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public RealmModel createRealm(String name) {
RealmModel realm = getDelegate().createRealm(name);
- registerRealmInvalidation(realm.getId());
+ registerRealmInvalidation(realm.getId(), realm.getName());
return realm;
}
@Override
public RealmModel createRealm(String id, String name) {
RealmModel realm = getDelegate().createRealm(id, name);
- registerRealmInvalidation(realm.getId());
+ registerRealmInvalidation(realm.getId(), realm.getName());
return realm;
}
@@ -434,7 +474,7 @@ public class RealmCacheSession implements CacheRealmProvider {
}
}
- public String getRealmByNameCacheKey(String name) {
+ static String getRealmByNameCacheKey(String name) {
return "realm.query.by.name." + name;
}
@@ -457,20 +497,12 @@ public class RealmCacheSession implements CacheRealmProvider {
RealmModel realm = getRealm(id);
if (realm == null) return false;
- invalidations.add(getRealmClientsQueryCacheKey(id));
- invalidations.add(getRealmByNameCacheKey(realm.getName()));
cache.invalidateObject(id);
- cache.realmRemoval(id, invalidations);
+ invalidationEvents.add(RealmRemovedEvent.create(id, realm.getName()));
+ cache.realmRemoval(id, realm.getName(), invalidations);
return getDelegate().removeRealm(id);
}
- protected void invalidateClient(RealmModel realm, ClientModel client) {
- invalidateClient(client.getId());
- invalidations.add(getRealmClientsQueryCacheKey(realm.getId()));
- invalidations.add(getClientByClientIdCacheKey(client.getClientId(), realm));
- listInvalidations.add(realm.getId());
- }
-
@Override
public ClientModel addClient(RealmModel realm, String clientId) {
@@ -486,30 +518,32 @@ public class RealmCacheSession implements CacheRealmProvider {
private ClientModel addedClient(RealmModel realm, ClientModel client) {
logger.trace("added Client.....");
- // need to invalidate realm client query cache every time as it may not be loaded on this node, but loaded on another
- invalidateClient(realm, client);
- cache.clientAdded(realm.getId(), client.getId(), invalidations);
- // this is needed so that a new client that hasn't been committed isn't cached in a query
+
+ invalidateClient(client.getId());
+ // this is needed so that a client that hasn't been committed isn't cached in a query
listInvalidations.add(realm.getId());
+
+ invalidationEvents.add(ClientAddedEvent.create(client.getId(), client.getClientId(), realm.getId()));
+ cache.clientAdded(realm.getId(), client.getId(), client.getClientId(), invalidations);
return client;
}
- private String getRealmClientsQueryCacheKey(String realm) {
+ static String getRealmClientsQueryCacheKey(String realm) {
return realm + REALM_CLIENTS_QUERY_SUFFIX;
}
- private String getGroupsQueryCacheKey(String realm) {
+ static String getGroupsQueryCacheKey(String realm) {
return realm + ".groups";
}
- private String getTopGroupsQueryCacheKey(String realm) {
+ static String getTopGroupsQueryCacheKey(String realm) {
return realm + ".top.groups";
}
- private String getRolesCacheKey(String container) {
+ static String getRolesCacheKey(String container) {
return container + ROLES_QUERY_SUFFIX;
}
- private String getRoleByNameCacheKey(String container, String name) {
+ static String getRoleByNameCacheKey(String container, String name) {
return container + "." + name + ROLES_QUERY_SUFFIX;
}
@@ -541,6 +575,7 @@ public class RealmCacheSession implements CacheRealmProvider {
for (String id : query.getClients()) {
ClientModel client = session.realms().getClientById(id, realm);
if (client == null) {
+ // TODO: Handle with cluster invalidations too
invalidations.add(cacheKey);
return getDelegate().getClients(realm);
}
@@ -554,12 +589,16 @@ public class RealmCacheSession implements CacheRealmProvider {
public boolean removeClient(String id, RealmModel realm) {
ClientModel client = getClientById(id, realm);
if (client == null) return false;
- // need to invalidate realm client query cache every time client list is changed
- invalidateClient(realm, client);
- cache.clientRemoval(realm.getId(), id, invalidations);
+
+ invalidateClient(client.getId());
+ // this is needed so that a client that hasn't been committed isn't cached in a query
+ listInvalidations.add(realm.getId());
+
+ invalidationEvents.add(ClientRemovedEvent.create(client));
+ cache.clientRemoval(realm.getId(), id, client.getClientId(), invalidations);
+
for (RoleModel role : client.getRoles()) {
- String roleId = role.getId();
- roleInvalidations(roleId);
+ roleRemovalInvalidations(role.getId(), role.getName(), client.getId());
}
return getDelegate().removeClient(id, realm);
}
@@ -577,11 +616,8 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public RoleModel addRealmRole(RealmModel realm, String id, String name) {
- invalidations.add(getRolesCacheKey(realm.getId()));
- // this is needed so that a new role that hasn't been committed isn't cached in a query
- listInvalidations.add(realm.getId());
RoleModel role = getDelegate().addRealmRole(realm, name);
- invalidations.add(role.getId());
+ addedRole(role.getId(), realm.getId());
return role;
}
@@ -664,11 +700,8 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public RoleModel addClientRole(RealmModel realm, ClientModel client, String id, String name) {
- invalidations.add(getRolesCacheKey(client.getId()));
- // this is needed so that a new role that hasn't been committed isn't cached in a query
- listInvalidations.add(client.getId());
RoleModel role = getDelegate().addClientRole(realm, client, id, name);
- invalidateRole(role.getId());
+ addedRole(role.getId(), client.getId());
return role;
}
@@ -734,10 +767,12 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public boolean removeRole(RealmModel realm, RoleModel role) {
- invalidations.add(getRolesCacheKey(role.getContainer().getId()));
- invalidations.add(getRoleByNameCacheKey(role.getContainer().getId(), role.getName()));
listInvalidations.add(role.getContainer().getId());
- registerRoleInvalidation(role.getId());
+
+ invalidateRole(role.getId());
+ invalidationEvents.add(RoleRemovedEvent.create(role.getId(), role.getName(), role.getContainer().getId()));
+ roleRemovalInvalidations(role.getId(), role.getName(), role.getContainer().getId());
+
return getDelegate().removeRole(realm, role);
}
@@ -797,8 +832,11 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public void moveGroup(RealmModel realm, GroupModel group, GroupModel toParent) {
- registerGroupInvalidation(group.getId());
- if (toParent != null) registerGroupInvalidation(toParent.getId());
+ invalidateGroup(group.getId(), realm.getId(), true);
+ if (toParent != null) invalidateGroup(group.getId(), realm.getId(), false); // Queries already invalidated
+ listInvalidations.add(realm.getId());
+
+ invalidationEvents.add(GroupMovedEvent.create(group, toParent, realm.getId()));
getDelegate().moveGroup(realm, group, toParent);
}
@@ -876,14 +914,15 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public boolean removeGroup(RealmModel realm, GroupModel group) {
- registerGroupInvalidation(group.getId());
+ invalidateGroup(group.getId(), realm.getId(), true);
listInvalidations.add(realm.getId());
- invalidations.add(getGroupsQueryCacheKey(realm.getId()));
- if (group.getParentId() == null) {
- invalidations.add(getTopGroupsQueryCacheKey(realm.getId()));
- } else {
- registerGroupInvalidation(group.getParentId());
+ cache.groupQueriesInvalidations(realm.getId(), invalidations);
+ if (group.getParentId() != null) {
+ invalidateGroup(group.getParentId(), realm.getId(), false); // Queries already invalidated
}
+
+ invalidationEvents.add(GroupRemovedEvent.create(group, realm.getId()));
+
return getDelegate().removeGroup(realm, group);
}
@@ -893,11 +932,11 @@ public class RealmCacheSession implements CacheRealmProvider {
return groupAdded(realm, group);
}
- public GroupModel groupAdded(RealmModel realm, GroupModel group) {
+ private GroupModel groupAdded(RealmModel realm, GroupModel group) {
listInvalidations.add(realm.getId());
- invalidations.add(getGroupsQueryCacheKey(realm.getId()));
- invalidations.add(getTopGroupsQueryCacheKey(realm.getId()));
+ cache.groupQueriesInvalidations(realm.getId(), invalidations);
invalidations.add(group.getId());
+ invalidationEvents.add(GroupAddedEvent.create(group.getId(), realm.getId()));
return group;
}
@@ -909,15 +948,32 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public void addTopLevelGroup(RealmModel realm, GroupModel subGroup) {
- invalidations.add(getTopGroupsQueryCacheKey(realm.getId()));
- invalidations.add(subGroup.getId());
+ invalidateGroup(subGroup.getId(), realm.getId(), true);
if (subGroup.getParentId() != null) {
- registerGroupInvalidation(subGroup.getParentId());
+ invalidateGroup(subGroup.getParentId(), realm.getId(), false); // Queries already invalidated
}
+
+ addGroupEventIfAbsent(GroupMovedEvent.create(subGroup, null, realm.getId()));
+
getDelegate().addTopLevelGroup(realm, subGroup);
}
+ private void addGroupEventIfAbsent(InvalidationEvent eventToAdd) {
+ String groupId = eventToAdd.getId();
+
+ // Check if we have existing event with bigger priority
+ boolean eventAlreadyExists = invalidationEvents.stream().filter((InvalidationEvent event) -> {
+
+ return (event.getId().equals(groupId)) && (event instanceof GroupAddedEvent || event instanceof GroupMovedEvent || event instanceof GroupRemovedEvent);
+
+ }).findFirst().isPresent();
+
+ if (!eventAlreadyExists) {
+ invalidationEvents.add(eventToAdd);
+ }
+ }
+
@Override
public ClientModel getClientById(String id, RealmModel realm) {
CachedClient cached = cache.get(id, CachedClient.class);
@@ -948,7 +1004,7 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public ClientModel getClientByClientId(String clientId, RealmModel realm) {
- String cacheKey = getClientByClientIdCacheKey(clientId, realm);
+ String cacheKey = getClientByClientIdCacheKey(clientId, realm.getId());
ClientListQuery query = cache.get(cacheKey, ClientListQuery.class);
String id = null;
@@ -976,8 +1032,8 @@ public class RealmCacheSession implements CacheRealmProvider {
return getClientById(id, realm);
}
- public String getClientByClientIdCacheKey(String clientId, RealmModel realm) {
- return realm.getId() + ".client.query.by.clientId." + clientId;
+ static String getClientByClientIdCacheKey(String clientId, String realmId) {
+ return realmId + ".client.query.by.clientId." + clientId;
}
@Override
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 a43aeb8..b6862f5 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 ee8dc8b..e949314 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-@Listener
public class UserCacheManager extends CacheManager {
- protected static final Logger logger = Logger.getLogger(UserCacheManager.class);
+ private static final Logger logger = Logger.getLogger(UserCacheManager.class);
protected volatile boolean enabled = true;
+
public UserCacheManager(Cache<String, Revisioned> cache, Cache<String, Long> revisions) {
super(cache, revisions);
}
@Override
+ protected Logger getLogger() {
+ return logger;
+ }
+
+ @Override
public void clear() {
cache.clear();
revisions.clear();
}
+
+ public void userUpdatedInvalidations(String userId, String username, String email, String realmId, Set<String> invalidations) {
+ invalidations.add(userId);
+ if (email != null) invalidations.add(UserCacheSession.getUserByEmailCacheKey(realmId, email));
+ invalidations.add(UserCacheSession.getUserByUsernameCacheKey(realmId, username));
+ }
+
+ // Fully invalidate user including consents and federatedIdentity links.
+ public void fullUserInvalidation(String userId, String username, String email, String realmId, boolean identityFederationEnabled, Map<String, String> federatedIdentities, Set<String> invalidations) {
+ userUpdatedInvalidations(userId, username, email, realmId, invalidations);
+
+ if (identityFederationEnabled) {
+ // Invalidate all keys for lookup this user by any identityProvider link
+ for (Map.Entry<String, String> socialLink : federatedIdentities.entrySet()) {
+ String fedIdentityCacheKey = UserCacheSession.getUserByFederatedIdentityCacheKey(realmId, socialLink.getKey(), socialLink.getValue());
+ invalidations.add(fedIdentityCacheKey);
+ }
+
+ // Invalidate federationLinks of user
+ invalidations.add(UserCacheSession.getFederatedIdentityLinksCacheKey(userId));
+ }
+
+ // Consents
+ invalidations.add(UserCacheSession.getConsentCacheKey(userId));
+ }
+
+ public void federatedIdentityLinkUpdatedInvalidation(String userId, Set<String> invalidations) {
+ invalidations.add(UserCacheSession.getFederatedIdentityLinksCacheKey(userId));
+ }
+
+ public void federatedIdentityLinkRemovedInvalidation(String userId, String realmId, String identityProviderId, String socialUserId, Set<String> invalidations) {
+ invalidations.add(UserCacheSession.getFederatedIdentityLinksCacheKey(userId));
+ if (identityProviderId != null) {
+ invalidations.add(UserCacheSession.getUserByFederatedIdentityCacheKey(realmId, identityProviderId, socialUserId));
+ }
+ }
+
+ public void consentInvalidation(String userId, Set<String> invalidations) {
+ invalidations.add(UserCacheSession.getConsentCacheKey(userId));
+ }
+
+
@Override
- protected Predicate<Map.Entry<String, Revisioned>> getInvalidationPredicate(Object object) {
- return null;
+ protected void addInvalidationsFromEvent(InvalidationEvent event, Set<String> invalidations) {
+ if (event instanceof UserCacheInvalidationEvent) {
+ ((UserCacheInvalidationEvent) event).addInvalidations(this, invalidations);
+ }
}
public void invalidateRealmUsers(String realm, Set<String> invalidations) {
- addInvalidations(InRealmPredicate.create().realm(realm), invalidations);
+ InRealmPredicate inRealmPredicate = getInRealmPredicate(realm);
+ addInvalidations(inRealmPredicate, invalidations);
+ }
+
+ private InRealmPredicate getInRealmPredicate(String realmId) {
+ return InRealmPredicate.create().realm(realmId);
}
}
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 5531de1..cb8c0a8 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<String> invalidations = new HashSet<>();
protected Set<String> realmInvalidations = new HashSet<>();
+ protected Set<InvalidationEvent> invalidationEvents = new HashSet<>(); // Events to be sent across cluster
protected Map<String, UserModel> managedUsers = new HashMap<>();
public UserCacheSession(UserCacheManager cache, KeycloakSession session) {
@@ -85,7 +93,7 @@ public class UserCacheSession implements UserCache {
public void clear() {
cache.clear();
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
- cluster.notify(InfinispanUserCacheProviderFactory.USER_CLEAR_CACHE_EVENTS, new ClearCacheEvent());
+ cluster.notify(InfinispanUserCacheProviderFactory.USER_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true);
}
public UserProvider getDelegate() {
@@ -97,10 +105,8 @@ public class UserCacheSession implements UserCache {
}
public void registerUserInvalidation(RealmModel realm,CachedUser user) {
- invalidations.add(user.getId());
- if (user.getEmail() != null) invalidations.add(getUserByEmailCacheKey(realm.getId(), user.getEmail()));
- invalidations.add(getUserByUsernameCacheKey(realm.getId(), user.getUsername()));
- if (realm.isIdentityFederationEnabled()) invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
+ cache.userUpdatedInvalidations(user.getId(), user.getUsername(), user.getEmail(), user.getRealm(), invalidations);
+ invalidationEvents.add(UserUpdatedEvent.create(user.getId(), user.getUsername(), user.getEmail(), user.getRealm()));
}
@Override
@@ -108,16 +114,14 @@ public class UserCacheSession implements UserCache {
if (user instanceof CachedUserModel) {
((CachedUserModel)user).invalidate();
} else {
- invalidations.add(user.getId());
- if (user.getEmail() != null) invalidations.add(getUserByEmailCacheKey(realm.getId(), user.getEmail()));
- invalidations.add(getUserByUsernameCacheKey(realm.getId(), user.getUsername()));
- if (realm.isIdentityFederationEnabled()) invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
+ cache.userUpdatedInvalidations(user.getId(), user.getUsername(), user.getEmail(), realm.getId(), invalidations);
+ invalidationEvents.add(UserUpdatedEvent.create(user.getId(), user.getUsername(), user.getEmail(), realm.getId()));
}
}
@Override
public void evict(RealmModel realm) {
- realmInvalidations.add(realm.getId());
+ addRealmInvalidation(realm.getId());
}
protected void runInvalidations() {
@@ -127,6 +131,8 @@ public class UserCacheSession implements UserCache {
for (String invalidation : invalidations) {
cache.invalidateObject(invalidation);
}
+
+ cache.sendInvalidationEvents(session, invalidationEvents);
}
private KeycloakTransaction getTransaction() {
@@ -201,19 +207,23 @@ public class UserCacheSession implements UserCache {
return adapter;
}
- public String getUserByUsernameCacheKey(String realmId, String username) {
+ static String getUserByUsernameCacheKey(String realmId, String username) {
return realmId + ".username." + username;
}
- public String getUserByEmailCacheKey(String realmId, String email) {
+ static String getUserByEmailCacheKey(String realmId, String email) {
return realmId + ".email." + email;
}
- public String getUserByFederatedIdentityCacheKey(String realmId, FederatedIdentityModel socialLink) {
- return realmId + ".idp." + socialLink.getIdentityProvider() + "." + socialLink.getUserId();
+ private static String getUserByFederatedIdentityCacheKey(String realmId, FederatedIdentityModel socialLink) {
+ return getUserByFederatedIdentityCacheKey(realmId, socialLink.getIdentityProvider(), socialLink.getUserId());
+ }
+
+ static String getUserByFederatedIdentityCacheKey(String realmId, String identityProvider, String socialUserId) {
+ return realmId + ".idp." + identityProvider + "." + socialUserId;
}
- public String getFederatedIdentityLinksCacheKey(String userId) {
+ static String getFederatedIdentityLinksCacheKey(String userId) {
return userId + ".idplinks";
}
@@ -655,27 +665,32 @@ public class UserCacheSession implements UserCache {
@Override
public void updateConsent(RealmModel realm, String userId, UserConsentModel consent) {
- invalidations.add(getConsentCacheKey(userId));
+ invalidateConsent(userId);
getDelegate().updateConsent(realm, userId, consent);
}
@Override
public boolean revokeConsentForClient(RealmModel realm, String userId, String clientInternalId) {
- invalidations.add(getConsentCacheKey(userId));
+ invalidateConsent(userId);
return getDelegate().revokeConsentForClient(realm, userId, clientInternalId);
}
- public String getConsentCacheKey(String userId) {
+ static String getConsentCacheKey(String userId) {
return userId + ".consents";
}
@Override
public void addConsent(RealmModel realm, String userId, UserConsentModel consent) {
- invalidations.add(getConsentCacheKey(userId));
+ invalidateConsent(userId);
getDelegate().addConsent(realm, userId, consent);
}
+ private void invalidateConsent(String userId) {
+ cache.consentInvalidation(userId, invalidations);
+ invalidationEvents.add(UserConsentsUpdatedEvent.create(userId));
+ }
+
@Override
public UserConsentModel getConsentByClient(RealmModel realm, String userId, String clientId) {
logger.tracev("getConsentByClient: {0}", userId);
@@ -754,7 +769,7 @@ public class UserCacheSession implements UserCache {
public UserModel addUser(RealmModel realm, String id, String username, boolean addDefaultRoles, boolean addDefaultRequiredActions) {
UserModel user = getDelegate().addUser(realm, id, username, addDefaultRoles, addDefaultRoles);
// just in case the transaction is rolled back you need to invalidate the user and all cache queries for that user
- invalidateUser(realm, user);
+ fullyInvalidateUser(realm, user);
managedUsers.put(user.getId(), user);
return user;
}
@@ -763,94 +778,89 @@ public class UserCacheSession implements UserCache {
public UserModel addUser(RealmModel realm, String username) {
UserModel user = getDelegate().addUser(realm, username);
// just in case the transaction is rolled back you need to invalidate the user and all cache queries for that user
- invalidateUser(realm, user);
+ fullyInvalidateUser(realm, user);
managedUsers.put(user.getId(), user);
return user;
}
- protected void invalidateUser(RealmModel realm, UserModel user) {
- // just in case the transaction is rolled back you need to invalidate the user and all cache queries for that user
+ // just in case the transaction is rolled back you need to invalidate the user and all cache queries for that user
+ protected void fullyInvalidateUser(RealmModel realm, UserModel user) {
+ Set<FederatedIdentityModel> federatedIdentities = realm.isIdentityFederationEnabled() ? getFederatedIdentities(user, realm) : null;
- if (realm.isIdentityFederationEnabled()) {
- // Invalidate all keys for lookup this user by any identityProvider link
- Set<FederatedIdentityModel> federatedIdentities = getFederatedIdentities(user, realm);
- for (FederatedIdentityModel socialLink : federatedIdentities) {
- String fedIdentityCacheKey = getUserByFederatedIdentityCacheKey(realm.getId(), socialLink);
- invalidations.add(fedIdentityCacheKey);
- }
+ UserFullInvalidationEvent event = UserFullInvalidationEvent.create(user.getId(), user.getUsername(), user.getEmail(), realm.getId(), realm.isIdentityFederationEnabled(), federatedIdentities);
- // Invalidate federationLinks of user
- invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
- }
-
- invalidations.add(user.getId());
- if (user.getEmail() != null) invalidations.add(getUserByEmailCacheKey(realm.getId(), user.getEmail()));
- invalidations.add(getUserByUsernameCacheKey(realm.getId(), user.getUsername()));
+ cache.fullUserInvalidation(user.getId(), user.getUsername(), user.getEmail(), realm.getId(), realm.isIdentityFederationEnabled(), event.getFederatedIdentities(), invalidations);
+ invalidationEvents.add(event);
}
@Override
public boolean removeUser(RealmModel realm, UserModel user) {
- invalidateUser(realm, user);
+ fullyInvalidateUser(realm, user);
return getDelegate().removeUser(realm, user);
}
@Override
public void addFederatedIdentity(RealmModel realm, UserModel user, FederatedIdentityModel socialLink) {
- invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
+ invalidateFederationLink(user.getId());
getDelegate().addFederatedIdentity(realm, user, socialLink);
}
@Override
public void updateFederatedIdentity(RealmModel realm, UserModel federatedUser, FederatedIdentityModel federatedIdentityModel) {
- invalidations.add(getFederatedIdentityLinksCacheKey(federatedUser.getId()));
+ invalidateFederationLink(federatedUser.getId());
getDelegate().updateFederatedIdentity(realm, federatedUser, federatedIdentityModel);
}
+ private void invalidateFederationLink(String userId) {
+ cache.federatedIdentityLinkUpdatedInvalidation(userId, invalidations);
+ invalidationEvents.add(UserFederationLinkUpdatedEvent.create(userId));
+ }
+
@Override
public boolean removeFederatedIdentity(RealmModel realm, UserModel user, String socialProvider) {
// Needs to invalidate both directions
FederatedIdentityModel socialLink = getFederatedIdentity(user, socialProvider, realm);
- invalidations.add(getFederatedIdentityLinksCacheKey(user.getId()));
- if (socialLink != null) {
- invalidations.add(getUserByFederatedIdentityCacheKey(realm.getId(), socialLink));
- }
+
+ UserFederationLinkRemovedEvent event = UserFederationLinkRemovedEvent.create(user.getId(), realm.getId(), socialLink);
+ cache.federatedIdentityLinkRemovedInvalidation(user.getId(), realm.getId(), event.getIdentityProviderId(), event.getSocialUserId(), invalidations);
+ invalidationEvents.add(event);
return getDelegate().removeFederatedIdentity(realm, user, socialProvider);
}
@Override
public void grantToAllUsers(RealmModel realm, RoleModel role) {
- realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
+ addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().grantToAllUsers(realm, role);
}
@Override
public void preRemove(RealmModel realm) {
- realmInvalidations.add(realm.getId());
+ addRealmInvalidation(realm.getId());
getDelegate().preRemove(realm);
}
@Override
public void preRemove(RealmModel realm, RoleModel role) {
- realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
+ addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().preRemove(realm, role);
}
@Override
public void preRemove(RealmModel realm, GroupModel group) {
- realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
+ addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().preRemove(realm, group);
}
@Override
public void preRemove(RealmModel realm, UserFederationProviderModel link) {
- realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
+ addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().preRemove(realm, link);
}
@Override
public void preRemove(RealmModel realm, ClientModel client) {
- realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
+ addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().preRemove(realm, client);
}
@@ -862,9 +872,14 @@ public class UserCacheSession implements UserCache {
@Override
public void preRemove(RealmModel realm, ComponentModel component) {
if (!component.getProviderType().equals(UserStorageProvider.class.getName())) return;
- realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
+ addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
getDelegate().preRemove(realm, component);
}
+ private void addRealmInvalidation(String realmId) {
+ realmInvalidations.add(realmId);
+ invalidationEvents.add(UserCacheRealmInvalidationEvent.create(realmId));
+ }
+
}
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 44419cd..c21f787 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 343f2f0..663a4b2 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<String, SessionEntity> cache = connections.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
Cache<String, SessionEntity> offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
@@ -73,6 +74,11 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
public void onEvent(ProviderEvent event) {
if (event instanceof PostMigrationEvent) {
loadPersistentSessions(factory, maxErrors, sessionsPerSegment);
+ } else if (event instanceof UserModel.UserRemovedEvent) {
+ UserModel.UserRemovedEvent userRemovedEvent = (UserModel.UserRemovedEvent) event;
+
+ InfinispanUserSessionProvider provider = (InfinispanUserSessionProvider) userRemovedEvent.getKeycloakSession().getProvider(UserSessionProvider.class, getId());
+ provider.onUserRemoved(userRemovedEvent.getRealm(), userRemovedEvent.getUser());
}
}
});
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 1485da8..c332eea 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 0000000..e7c1337
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@Ignore
+public class ConcurrencyJDGRemoteCacheTest {
+
+ private static Map<String, EntryInfo> state = new HashMap<>();
+
+ public static void main(String[] args) throws Exception {
+ // Init map somehow
+ for (int i=0 ; i<100 ; i++) {
+ String key = "key-" + i;
+ state.put(key, new EntryInfo());
+ }
+
+ // Create caches, listeners and finally worker threads
+ Worker worker1 = createWorker(1);
+ Worker worker2 = createWorker(2);
+
+ // Start and join workers
+ worker1.start();
+ worker2.start();
+
+ worker1.join();
+ worker2.join();
+
+ // Output
+ for (Map.Entry<String, EntryInfo> entry : state.entrySet()) {
+ System.out.println(entry.getKey() + ":::" + entry.getValue());
+ worker1.cache.remove(entry.getKey());
+ }
+
+ // Finish JVM
+ worker1.cache.getCacheManager().stop();
+ worker2.cache.getCacheManager().stop();
+ }
+
+ private static Worker createWorker(int threadId) {
+ EmbeddedCacheManager manager = createManager(threadId);
+ Cache<String, Integer> cache = manager.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
+
+ System.out.println("Retrieved cache: " + threadId);
+
+ RemoteStore remoteStore = cache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class).iterator().next();
+ HotRodListener listener = new HotRodListener();
+ remoteStore.getRemoteCache().addClientListener(listener);
+
+ return new Worker(cache, threadId);
+ }
+
+ private static EmbeddedCacheManager createManager(int threadId) {
+ System.setProperty("java.net.preferIPv4Stack", "true");
+ System.setProperty("jgroups.tcp.port", "53715");
+ GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
+
+ boolean clustered = false;
+ boolean async = false;
+ boolean allowDuplicateJMXDomains = true;
+
+ if (clustered) {
+ gcb = gcb.clusteredDefault();
+ gcb.transport().clusterName("test-clustering");
+ }
+
+ gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
+
+ EmbeddedCacheManager cacheManager = new DefaultCacheManager(gcb.build());
+
+ Configuration invalidationCacheConfiguration = getCacheBackedByRemoteStore(threadId);
+
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.WORK_CACHE_NAME, invalidationCacheConfiguration);
+ return cacheManager;
+
+ }
+
+ private static Configuration getCacheBackedByRemoteStore(int threadId) {
+ ConfigurationBuilder cacheConfigBuilder = new ConfigurationBuilder();
+
+ // int port = threadId==1 ? 11222 : 11322;
+ int port = 11222;
+
+ return cacheConfigBuilder.persistence().addStore(RemoteStoreConfigurationBuilder.class)
+ .fetchPersistentState(false)
+ .ignoreModifications(false)
+ .purgeOnStartup(false)
+ .preload(false)
+ .shared(true)
+ .remoteCacheName(InfinispanConnectionProvider.WORK_CACHE_NAME)
+ .rawValues(true)
+ .forceReturnValues(false)
+ .addServer()
+ .host("localhost")
+ .port(port)
+ .connectionPool()
+ .maxActive(20)
+ .exhaustedAction(ExhaustedAction.CREATE_NEW)
+ .async()
+ . enabled(false).build();
+ }
+
+
+ @ClientListener
+ public static class HotRodListener {
+
+ //private AtomicInteger listenerCount = new AtomicInteger(0);
+
+ @ClientCacheEntryCreated
+ public void created(ClientCacheEntryCreatedEvent event) {
+ String cacheKey = (String) event.getKey();
+ state.get(cacheKey).successfulListenerWrites.incrementAndGet();
+ }
+
+ @ClientCacheEntryModified
+ public void updated(ClientCacheEntryModifiedEvent event) {
+ String cacheKey = (String) event.getKey();
+ state.get(cacheKey).successfulListenerWrites.incrementAndGet();
+ }
+
+ }
+
+
+ private static class Worker extends Thread {
+
+ private final Cache<String, Integer> cache;
+
+ private final int myThreadId;
+
+ private Worker(Cache<String, Integer> cache, int myThreadId) {
+ this.cache = cache;
+ this.myThreadId = myThreadId;
+ }
+
+ @Override
+ public void run() {
+ for (Map.Entry<String, EntryInfo> entry : state.entrySet()) {
+ String cacheKey = entry.getKey();
+ EntryInfo wrapper = state.get(cacheKey);
+
+ int val = getClusterStartupTime(this.cache, cacheKey, wrapper);
+ if (myThreadId == 1) {
+ wrapper.th1.set(val);
+ } else {
+ wrapper.th2.set(val);
+ }
+
+ }
+
+ System.out.println("Worker finished: " + myThreadId);
+ }
+
+ }
+
+ public static int getClusterStartupTime(Cache<String, Integer> cache, String cacheKey, EntryInfo wrapper) {
+ int startupTime = new Random().nextInt(1024);
+
+ // Concurrency doesn't work correctly with this
+ //Integer existingClusterStartTime = (Integer) cache.putIfAbsent(cacheKey, startupTime);
+
+ // Concurrency works fine with this
+ RemoteCache remoteCache = cache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class).iterator().next().getRemoteCache();
+ Integer existingClusterStartTime = (Integer) remoteCache.withFlags(Flag.FORCE_RETURN_VALUE).putIfAbsent(cacheKey, startupTime);
+
+ if (existingClusterStartTime == null) {
+ wrapper.successfulInitializations.incrementAndGet();
+ return startupTime;
+ } else {
+ return existingClusterStartTime;
+ }
+ }
+
+ private static class EntryInfo {
+ AtomicInteger successfulInitializations = new AtomicInteger(0);
+ AtomicInteger successfulListenerWrites = new AtomicInteger(0);
+ AtomicInteger th1 = new AtomicInteger();
+ AtomicInteger th2 = new AtomicInteger();
+
+ @Override
+ public String toString() {
+ return String.format("Inits: %d, listeners: %d, th1: %d, th2: %d", successfulInitializations.get(), successfulListenerWrites.get(), th1.get(), th2.get());
+ }
+ }
+
+
+
+}
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 f5d2666..55e4108 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<String> clients = query.getResultList();
for (String client : clients) {
- session.realms().removeClient(client, adapter);
+ // No need to go through cache. Clients were already invalidated
+ removeClient(client, adapter);
}
for (ClientTemplateEntity a : new LinkedList<>(realm.getClientTemplates())) {
@@ -154,7 +155,8 @@ public class JpaRealmProvider implements RealmProvider {
}
for (RoleModel role : adapter.getRoles()) {
- session.realms().removeRole(adapter, role);
+ // No need to go through cache. Roles were already invalidated
+ removeRole(adapter, role);
}
@@ -486,7 +488,8 @@ public class JpaRealmProvider implements RealmProvider {
session.users().preRemove(realm, client);
for (RoleModel role : client.getRoles()) {
- client.removeRole(role);
+ // No need to go through cache. Roles were already invalidated
+ removeRole(realm, role);
}
ClientEntity clientEntity = ((ClientAdapter)client).getEntity();
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 b0ea73a..9633f84 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 97aa4bd..f5b9d7d 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
@@ -953,13 +953,6 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
}
@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) {
passwordPolicy = PasswordPolicy.parse(session, realm.getPasswordPolicy());
@@ -1933,12 +1926,6 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
}
@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 254412b..35265af 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
@@ -45,11 +45,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 7b46c0c..6ef597e 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<MongoUserEntity> mongoUsers = getMongoStore().loadEntities(MongoUserEntity.class, query, invocationContext);
+ UserManager userManager = new UserManager(session);
+ for (MongoUserEntity userEntity : mongoUsers) {
+ // Doing this way to ensure UserRemovedEvent triggered with proper callbacks.
+ UserAdapter user = new UserAdapter(session, realm, userEntity, invocationContext);
+ userManager.removeUser(realm, user, this);
+ }
}
@Override
@@ -661,16 +676,18 @@ public class MongoUserProvider implements UserProvider, UserCredentialStore {
}
public MongoUserEntity getMongoUserEntity(UserModel user) {
- UserAdapter adapter = null;
- if (user instanceof CachedUserModel) {
- adapter = (UserAdapter)((CachedUserModel)user).getDelegateForUpdate();
- } else if (user instanceof UserAdapter ){
- adapter = (UserAdapter)user;
+ if (user instanceof UserAdapter) {
+ UserAdapter adapter = (UserAdapter)user;
+ return adapter.getMongoEntity();
+ } else if (user instanceof CachedUserModel) {
+ UserModel delegate = ((CachedUserModel)user).getDelegateForUpdate();
+ return getMongoUserEntity(delegate);
+ } else if (user instanceof UserModelDelegate){
+ UserModel delegate = ((UserModelDelegate) user).getDelegate();
+ return getMongoUserEntity(delegate);
} else {
return getMongoStore().loadEntity(MongoUserEntity.class, user.getId(), invocationContext);
-
}
- return adapter.getMongoEntity();
}
@Override
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 b4028a2..083a0e6 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
@@ -43,11 +43,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 a79c478..119c7df 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;
@@ -516,13 +511,6 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
}
@Override
- public boolean removeRoleById(String id) {
- RoleModel role = getRoleById(id);
- if (role == null) return false;
- return removeRole(role);
- }
-
- @Override
public Set<RoleModel> getRoles() {
DBObject query = new QueryBuilder()
.and("realmId").is(getId())
@@ -555,12 +543,6 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
}
@Override
- public void addTopLevelGroup(GroupModel subGroup) {
- session.realms().addTopLevelGroup(this, subGroup);
-
- }
-
- @Override
public void moveGroup(GroupModel group, GroupModel toParent) {
session.realms().moveGroup(this, group, toParent);
}
@@ -2006,28 +1988,39 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
@Override
public void removeComponent(ComponentModel component) {
Iterator<ComponentEntity> it = realm.getComponentEntities().iterator();
+ ComponentEntity found = null;
while(it.hasNext()) {
- if (it.next().getId().equals(component.getId())) {
- session.users().preRemove(this, component);
- removeComponents(component.getId());
- it.remove();
+ ComponentEntity next = it.next();
+ if (next.getId().equals(component.getId())) {
+ found = next;
break;
}
}
- updateRealm();
+ if (found != null) {
+ session.users().preRemove(this, component);
+ removeComponents(component.getId());
+ realm.getComponentEntities().remove(found);
+ updateRealm();
+ }
}
@Override
public void removeComponents(String parentId) {
Iterator<ComponentEntity> it = realm.getComponentEntities().iterator();
+ Set<ComponentEntity> toRemove = new HashSet<>();
while(it.hasNext()) {
ComponentEntity next = it.next();
if (next.getParentId().equals(parentId)) {
- session.users().preRemove(this, entityToModel(next));
- it.remove();
+ toRemove.add(next);
}
}
+
+ for (ComponentEntity toRem : toRemove) {
+ session.users().preRemove(this, entityToModel(toRem));
+ realm.getComponentEntities().remove(toRem);
+ }
+
updateRealm();
}
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 9d5ad7c..e5440cc 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<MongoUserEntity> implement
@Override
public boolean isMemberOf(GroupModel group) {
+ if (user.getGroupIds() == null) return false;
if (user.getGroupIds().contains(group.getId())) return true;
Set<GroupModel> groups = getGroups();
return RoleUtils.isMember(groups, group);
pom.xml 13(+6 -7)
diff --git a/pom.xml b/pom.xml
index 7918c6d..afd4212 100755
--- a/pom.xml
+++ b/pom.xml
@@ -80,7 +80,7 @@
<!-- Authorization Drools Policy Provider -->
<version.org.drools>6.4.0.Final</version.org.drools>
- <version.jboss-integration-platform>6.0.6.Final</version.jboss-integration-platform>
+ <version.jboss-integration-platform>6.0.10.Final</version.jboss-integration-platform>
<!-- Others -->
<apacheds.version>2.0.0-M21</apacheds.version>
@@ -98,7 +98,6 @@
<servlet.api.30.version>1.0.2.Final</servlet.api.30.version>
<twitter4j.version>4.0.4</twitter4j.version>
<jna.version>4.1.0</jna.version>
- <jnr.version>0.14</jnr.version>
<!-- Test -->
<greenmail.version>1.3.1b</greenmail.version>
@@ -635,6 +634,11 @@
<version>${infinispan.version}</version>
</dependency>
<dependency>
+ <groupId>org.infinispan</groupId>
+ <artifactId>infinispan-cachestore-remote</artifactId>
+ <version>${infinispan.version}</version>
+ </dependency>
+ <dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<version>${liquibase.version}</version>
@@ -703,11 +707,6 @@
<version>${jna.version}</version>
</dependency>
<dependency>
- <groupId>com.github.jnr</groupId>
- <artifactId>jnr-unixsocket</artifactId>
- <version>${jnr.version}</version>
- </dependency>
- <dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-ldap-federation</artifactId>
<version>${project.version}</version>
README.md 2(+1 -1)
diff --git a/README.md b/README.md
index 983d44c..a4454a0 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/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 245cff9..f81ea03 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 <b>Note: </b> 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<String, EncryptionAlgorithm> algorithms = new HashMap<String, EncryptionAlgorithm>(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;
- }
-
/**
* <p>
* 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();
+ 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)) {
- wrappingElementName = wrappingElementQName.getLocalPart();
+ 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. <b>NOTE:</> The document root element will be replaced by
+ * Encrypt the root document element inside a Document. <b>NOTE:</b> 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();
+ 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)) {
- wrappingElementName = wrappingElementQName.getLocalPart();
+ 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/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 cf3b782..1479495 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/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
index e9df047..09720a7 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<GroupModel> getGroups();
List<GroupModel> 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 85f1fd3..46c3198 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<RoleModel> 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<RoleModel> 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<RoleModel> 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 81b2b51..d606dfc 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@@ -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);
- }
+ if (userProvider.removeUser(realm, user)) {
+ session.getKeycloakSessionFactory().publish(new UserModel.UserRemovedEvent() {
- UserSessionPersisterProvider sessionsPersister = session.getProvider(UserSessionPersisterProvider.class);
- if (sessionsPersister != null) {
- sessionsPersister.onUserRemoved(realm, user);
- }
+ @Override
+ public RealmModel getRealm() {
+ return realm;
+ }
- if (userProvider.removeUser(realm, user)) {
+ @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 233c8a8..15cc296 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 585558c..4102de1 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/server-spi-private/src/main/java/org/keycloak/cluster/ClusterListener.java b/server-spi-private/src/main/java/org/keycloak/cluster/ClusterListener.java
index 41c65c0..2c07377 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 6c22056..abed174 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-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java b/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java
index 7bc1299..61ae1be 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-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProviderFactory.java
index 2c0b98c..350f4f9 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface UserSessionPersisterProviderFactory extends ProviderFactory<UserSessionPersisterProvider> {
+
+ @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/services/src/main/java/org/keycloak/authentication/authenticators/browser/ConditionalOtpFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ConditionalOtpFormAuthenticator.java
index ef17476..1696a1d 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<String, String> 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<String, String> config) {
+ private OtpDecision voteForUserOtpControlAttribute(UserModel user, Map<String, String> config) {
if (!config.containsKey(OTP_CONTROL_USER_ATTRIBUTE)) {
return ABSTAIN;
@@ -173,7 +191,7 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
return ABSTAIN;
}
- List<String> values = context.getUser().getAttribute(attributeName);
+ List<String> values = user.getAttribute(attributeName);
if (values.isEmpty()) {
return ABSTAIN;
@@ -191,14 +209,12 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
}
}
- private OtpDecision voteForHttpHeaderMatchesPattern(AuthenticationFlowContext context, Map<String, String> config) {
+ private OtpDecision voteForHttpHeaderMatchesPattern(MultivaluedMap<String, String> requestHeaders, Map<String, String> config) {
if (!config.containsKey(FORCE_OTP_FOR_HTTP_HEADER) && !config.containsKey(SKIP_OTP_FOR_HTTP_HEADER)) {
return ABSTAIN;
}
- MultivaluedMap<String, String> 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<String, String> config) {
+ private OtpDecision voteForUserRole(RealmModel realm, UserModel user, Map<String, String> 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<String, String> 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 9df33fc..9126689 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 5d2d054..605047f 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<String> 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 de4d054..4de3720 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".
- * <p>
- * Optionally prefixes each role name with the given {@code prefix}.
- * </p>
- *
- * @param roleModels
- * @param prefix the prefix to apply, may be {@literal null}
+ * Returns a stream with roles that come from:
+ * <ul>
+ * <li>Direct assignment of the role to the user</li>
+ * <li>Direct assignment of the role to any group of the user or any of its parent group</li>
+ * <li>Composite roles are expanded recursively, the composite role itself is also contained in the returned stream</li>
+ * </ul>
+ * @param user User to enumerate the roles for
+ * @return
+ */
+ public static Stream<RoleModel> 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));
+ }
+
+ /**
+ * Returns stream of the given group and its parents (recursively).
+ * @param group
* @return
*/
- protected Set<String> flattenRoleModelToRoleNames(Set<RoleModel> roleModels, String prefix) {
+ private static Stream<GroupModel> groupAndItsParentsStream(GroupModel group) {
+ Stream.Builder<GroupModel> sb = Stream.builder();
+ while (group != null) {
+ sb.add(group);
+ group = group.getParent();
+ }
+ return sb.build();
+ }
- Set<String> roleNames = new LinkedHashSet<>();
+ /**
+ * Recursively expands composite roles into their composite.
+ * @param role
+ * @return Stream of containing all of the composite roles and their components.
+ */
+ private static Stream<RoleModel> expandCompositeRolesStream(RoleModel role) {
+ Stream.Builder<RoleModel> sb = Stream.builder();
- Deque<RoleModel> stack = new ArrayDeque<>(roleModels);
- while (!stack.isEmpty()) {
+ Deque<RoleModel> 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();
+ return sb.build();
+ }
- if (prefix != null && !prefix.trim().isEmpty()) {
- roleName = prefix.trim() + roleName;
- }
+ /**
+ * 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<RoleModel> 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<RoleModel> clientUserRoles = getAllUserRolesStream(user).filter(restriction);
- roleNames.add(roleName);
+ boolean dontLimitScope = userSession.getClientSessions().stream().anyMatch(cs -> cs.getClient().isFullScopeAllowed());
+ if (! dontLimitScope) {
+ Set<RoleModel> clientRoles = userSession.getClientSessions().stream()
+ .flatMap(cs -> cs.getClient().getScopeMappings().stream())
+ .collect(Collectors.toSet());
+
+ clientUserRoles = clientUserRoles.filter(clientRoles::contains);
}
- return roleNames;
+ Set<String> 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 99b2610..8b64aef 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<String, Object> 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 01d47e1..5a88c2a 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<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<ProviderConfigProperty>();
+ private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>();
static {
@@ -60,6 +63,7 @@ public class UserClientRoleMappingMapper extends AbstractUserRoleMappingMapper {
OIDCAttributeMapperHelper.addAttributeConfig(CONFIG_PROPERTIES, UserClientRoleMappingMapper.class);
}
+ @Override
public List<ProviderConfigProperty> 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) {
+ String clientId = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID);
+ String rolePrefix = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_ROLE_PREFIX);
+
+ setClaim(token, mappingModel, userSession, getClientRoleFilter(clientId, userSession), rolePrefix);
+ }
- UserModel user = userSession.getUser();
+ private static Predicate<RoleModel> getClientRoleFilter(String clientId, UserSessionModel userSession) {
+ if (clientId == null) {
+ return RoleModel::isClientRole;
+ }
- String clientId = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID);
- if (clientId != null) {
+ RealmModel clientRealm = userSession.getRealm();
+ ClientModel client = clientRealm.getClientByClientId(clientId.trim());
- ClientModel clientModel = userSession.getRealm().getClientByClientId(clientId.trim());
- Set<RoleModel> clientRoleMappings = user.getClientRoleMappings(clientModel);
+ if (client == null) {
+ return RoleModel::isClientRole;
+ }
- String rolePrefix = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_ROLE_PREFIX);
- Set<String> clientRoleNames = flattenRoleModelToRoleNames(clientRoleMappings, rolePrefix);
+ ClientTemplateModel template = client.getClientTemplate();
+ boolean useTemplateScope = template != null && client.useTemplateScope();
+ boolean fullScopeAllowed = (useTemplateScope && template.isFullScopeAllowed()) || client.isFullScopeAllowed();
- OIDCAttributeMapperHelper.mapClaim(token, mappingModel, clientRoleNames);
+ Set<RoleModel> clientRoleMappings = client.getRoles();
+ if (fullScopeAllowed) {
+ return clientRoleMappings::contains;
+ }
+
+ Set<RoleModel> scopeMappings = new HashSet<>();
+
+ if (useTemplateScope) {
+ Set<RoleModel> templateScopeMappings = template.getScopeMappings();
+ if (templateScopeMappings != null) {
+ scopeMappings.addAll(templateScopeMappings);
+ }
}
- }
+ Set<RoleModel> 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 ef98182..f978b08 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<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<ProviderConfigProperty>();
+ private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>();
static {
@@ -54,6 +49,7 @@ public class UserRealmRoleMappingMapper extends AbstractUserRoleMappingMapper {
OIDCAttributeMapperHelper.addAttributeConfig(CONFIG_PROPERTIES, UserRealmRoleMappingMapper.class);
}
+ @Override
public List<ProviderConfigProperty> 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<String> 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 f606bfc..83f90f0 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<String> resolveValidWebOrigins(UriInfo uriInfo, ClientModel client) {
- Set<String> webOrigins = client.getWebOrigins();
- if (webOrigins != null && webOrigins.contains("+")) {
- webOrigins.remove(INCLUDE_REDIRECTS);
+ Set<String> 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 3d62a27..d3cd904 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/UsersSyncManager.java b/services/src/main/java/org/keycloak/services/managers/UsersSyncManager.java
index d8c5dee..21b0cad 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/managers/UserStorageSyncManager.java b/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java
index 05b07f2..b1114fa 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/resources/admin/AdminConsole.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java
index cd4d881..2b796c5 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 b7dcddf..5db1ea4 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 087022f..0128425 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<AuthenticationExecutionModel> 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 56c7ce7..d3ac358 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 8854a7b..fa1e139 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<String, String> getUserFederationInstanceWithFallback(@PathParam("id") String id) {
+ this.auth.requireView();
+
+ Map<String, String> 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/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
index 363d6f4..2ea9992 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 {
testsuite/integration/pom.xml 4(+4 -0)
diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml
index 873d99d..8a2dc4b 100755
--- a/testsuite/integration/pom.xml
+++ b/testsuite/integration/pom.xml
@@ -233,6 +233,10 @@
<artifactId>infinispan-core</artifactId>
</dependency>
<dependency>
+ <groupId>org.infinispan</groupId>
+ <artifactId>infinispan-cachestore-remote</artifactId>
+ </dependency>
+ <dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
</dependency>
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 f5cbccb..91075ae 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 0000000..f71d0da
--- /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 :
+ *
+ * <local-cache name="work" start="EAGER" batching="false" />
+ *
+ * Finally, add this system property when running the test: -Dkeycloak.connectionsInfinispan.remoteStoreEnabled=true
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@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<String, Object> 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<String, Object> 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<String, Object> getInvalidationsAndClear() {
+ Map<String, Object> 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 80f663f..60d92d9 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 f2fc3aa..824200d 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 0000000..0c7eff0
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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<String> 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<Object, Object> cache = ispnProvider.getCache(cacheName);
+ if (cache == null) {
+ log.errorf("Cache '%s' doesn't exist", cacheName);
+ throw new HandledException();
+ }
+
+ printCache(cache);
+ }
+
+ private void printCache(Cache<Object, Object> 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<Object, Object> entry : cache.entrySet()) {
+ log.infof("%s=%s", entry.getKey(), entry.getValue());
+ }
+ }
+ }
+
+ @Override
+ public String printUsage() {
+ return super.printUsage() + " <cache-name> . 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() + " <realm-name>";
+ }
+ }
+}
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 0000000..2c71c72
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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() + " <role-prefix> <role-container> <starting-role-offset> <total-count> <batch-size> . " +
+ "\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 0000000..9792f9d
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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 8e9582b..9b2c17a 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/log4j.properties b/testsuite/integration/src/test/resources/log4j.properties
index f0ff6ac..2fa1d70 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/testsuite/integration/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
index 06b4e52..3f4ddd1 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}"
}
},
testsuite/integration-arquillian/pom.xml 28(+28 -0)
diff --git a/testsuite/integration-arquillian/pom.xml b/testsuite/integration-arquillian/pom.xml
index c5adb34..dc30d10 100644
--- a/testsuite/integration-arquillian/pom.xml
+++ b/testsuite/integration-arquillian/pom.xml
@@ -139,12 +139,40 @@
<properties>
<migrated.auth.server.version>${migration.project.version}</migrated.auth.server.version>
</properties>
+ <build>
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <systemPropertyVariables>
+ <migrated.auth.server.version>${migrated.auth.server.version}</migrated.auth.server.version>
+ </systemPropertyVariables>
+ </configuration>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ </build>
</profile>
<profile>
<id>test-product-migration</id>
<properties>
<migrated.auth.server.version>${migration.product.version}</migrated.auth.server.version>
</properties>
+ <build>
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <systemPropertyVariables>
+ <migrated.auth.server.version>${migrated.auth.server.version}</migrated.auth.server.version>
+ </systemPropertyVariables>
+ </configuration>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ </build>
</profile>
</profiles>
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 a9ff31d..56c76ef 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -98,6 +100,13 @@ public class TestingExportImportResource {
}
@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)
public void setProvider(@QueryParam("exportImportProvider") String exportImportProvider) {
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 7dd6b24..81c5a53 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 978bdea..cc8bea2 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 3b457ec..b8aa42c 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 <a href="mailto:vramik@redhat.com">Vlastislav Ramik</a>
@@ -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 27fa360..0c2f106 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -65,6 +66,11 @@ public interface TestingExportImportResource {
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)
public void setProvider(@QueryParam("exportImportProvider") String exportImportProvider);
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 89d5c5b..87950ab 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<RequiredActionProviderRepresentation> 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 65a2e9b..90bf62b 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
* @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<String, AccountApplicationsPage.AppEntry> 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 0230842..e492672 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<String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<RoleRepresentation> 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<String, String> 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<RoleRepresentation> 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<String, String> 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/authentication/FlowTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/FlowTest.java
index 90f8874..bce9117 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<AuthenticationFlowRepresentation> 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<String, String> 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 7e61f51..1f0274e 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/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
index 81af8c6..545fbff 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/client/SAMLClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/SAMLClientRegistrationTest.java
index c3e60a2..6efbe9a 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -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 90dda59..2abf044 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 45808a3..e7ec574 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<RealmRepresentation> testRealms) {
}
-
- @Ignore // TODO: Restart and set system properties doesn't work on wildfly ATM. Figure and re-enable
@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);
-
- // Restart to enforce full import
- restartServer();
+ public void importPreviousProject() throws Exception {
+ String projectVersion = System.getProperty("migration.project.version");
+ assertNotNull(projectVersion);
+
+ testLegacyImport(projectVersion);
+ }
+
+ @Test
+ public void importPreviousProduct() throws Exception {
- // 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);
-
- ClientResource masterAdminConsoleClient = adminClient.realm(Config.getAdminRealm()).clients().get("22ed594d-8c21-43f0-a080-c8879a411f94");
- assertLocaleMapperPresent(masterAdminConsoleClient);
-
-
- // Assert "realm-management" role correctly set and contains all admin roles.
- ClientResource foo11RealmManagementClient = adminClient.realm("foo11").clients().get("c7a9cf59-feeb-44a4-a467-e008e157efa2");
- List<RoleRepresentation> roles = foo11RealmManagementClient.roles().list();
- assertRolesAvailable(roles);
-
- // Assert all admin roles are also available as composites of "realm-admin"
- Set<RoleRepresentation> realmAdminComposites = foo11RealmManagementClient.roles().get(AdminRoles.REALM_ADMIN).getRoleComposites();
- assertRolesAvailable(realmAdminComposites);
+ String productVersion = System.getProperty("migration.product.version");
+ assertNotNull(productVersion);
+
+ testLegacyImport(productVersion);
+ }
- // 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);
+ 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);
+ }
+ }
- // Assert all admin roles are also available as composites of "admin" role
- Set<RoleRepresentation> masterAdminComposites = adminClient.realm(Config.getAdminRealm()).roles().get(AdminRoles.ADMIN).getRoleComposites();
- assertRolesAvailable(masterAdminComposites);
+ //KEYCLOAK-1982
+ @Test
+ public void importFrom11() throws LifecycleException {
+ URL url = LegacyImportTest.class.getResource("/exportimport-test/kc11-exported-realm.json");
+ 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);
+
+ 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 "realm-management" role correctly set and contains all admin roles.
+ ClientResource foo11RealmManagementClient = adminClient.realm("foo11").clients().get("c7a9cf59-feeb-44a4-a467-e008e157efa2");
+ List<RoleRepresentation> roles = foo11RealmManagementClient.roles().list();
+ assertRolesAvailable(roles);
+
+ // Assert all admin roles are also available as composites of "realm-admin"
+ Set<RoleRepresentation> realmAdminComposites = foo11RealmManagementClient.roles().get(AdminRoles.REALM_ADMIN).getRoleComposites();
+ assertRolesAvailable(realmAdminComposites);
+
+ // Assert all admin roles are also available as composites of "admin" role
+ Set<RoleRepresentation> 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 72d0c70..fe789ac 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 4bb437c..7a01e4e 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<String> 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<RealmRepresentation> 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 d317af7..ac86d20 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<String, Object> roleMappings = (Map<String, Object>)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<String, Object> roleMappings = (Map<String, Object>)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<String, Object> roleMappings = (Map<String, Object>)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<String, Object> roleMappings = (Map<String, Object>)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<String, Object> roleMappings = (Map<String, Object>)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/clientreg-test/saml-entity-descriptor.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/clientreg-test/saml-entity-descriptor.xml
index 0bfce4b..694bb82 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"/>
<AssertionConsumerService
index="1"
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
- Location="https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp"/>
+ Location="https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/post"/>
+ <AssertionConsumerService
+ index="2"
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS"
+ Location="https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/paos"/>
+ <AssertionConsumerService
+ index="3"
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+ Location="https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/soap"/>
+ <AssertionConsumerService
+ index="4"
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ Location="https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/redirect"/>
</SPSSODescriptor>
</EntityDescriptor>
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 d421fe4..9e76d60 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/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
index d8c4dc1..deb5b64 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/testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
index c0b2b6c..b0e8767 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": [
@@ -109,6 +128,16 @@
"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,
"consentRequired": 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"
}
]
}
@@ -328,6 +398,31 @@
}
}
]
+ },
+ {
+ "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/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 0000000..d7fe93a
--- /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 0000000..7319dce
--- /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 0000000..5833b29
--- /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/clean-start/pom.xml b/testsuite/integration-arquillian/tests/other/clean-start/pom.xml
index 65e00bd..06f6ffe 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 @@
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-other</artifactId>
- <version>2.1.0-SNAPSHOT</version>
+ <version>2.4.0.CR1-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-smoke-clean-start</artifactId>
diff --git a/testsuite/integration-arquillian/tests/other/pom.xml b/testsuite/integration-arquillian/tests/other/pom.xml
index a36c641..ed5e119 100644
--- a/testsuite/integration-arquillian/tests/other/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/pom.xml
@@ -119,6 +119,12 @@
</dependencies>
</profile>
<profile>
+ <id>clean-start</id>
+ <modules>
+ <module>clean-start</module>
+ </modules>
+ </profile>
+ <profile>
<id>console-ui-tests</id>
<modules>
<module>console</module>
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 b11d5e4..68488cc 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<UserFederationProviderFactoryRepresentation> providerFactories = adminClient.realm(REALM_NAME).userFederation().getProviderFactories();
diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml
index 4f4f42d..f6838ab 100755
--- a/testsuite/integration-arquillian/tests/pom.xml
+++ b/testsuite/integration-arquillian/tests/pom.xml
@@ -182,6 +182,8 @@
<firefox_binary>${firefox_binary}</firefox_binary>
<project.version>${project.version}</project.version>
+ <migration.project.version>${migration.project.version}</migration.project.version>
+ <migration.product.version>${migration.product.version}</migration.product.version>
</systemPropertyVariables>
<properties>
<property>
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 c40c438..d4ffc54 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 <Extensions> 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 624e9a5..564313c 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 c3b7e1f..36b432f 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 84600c0..2574f2d 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 @@
</div>
<div class="input-group">
<select class="form-control search" data-ng-model="query.type"
- ng-options="p.type as p.name group by p.group for p in policyProviders track by p.type" data-ng-change="firstPage()">
+ ng-options="p.type as p.name for p in policyProviders track by p.type" data-ng-change="firstPage()">
<option value="" selected ng-click="query.type = ''">{{:: 'authz-all-types' | translate}}</option>
</select>
</div>
</div>
<div class="pull-right">
<select class="form-control" ng-model="policyType"
- ng-options="p.name group by p.group for p in policyProviders track by p.type"
+ ng-options="p.name for p in policyProviders track by p.type"
data-ng-change="addPolicy(policyType);">
<option value="" disabled selected>{{:: 'authz-create-policy' | translate}}...</option>
</select>
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 47ab627..fc10e6b 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 @@
</fieldset>
</form>
- <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageClients" data-ng-show="!client.fullScopeAllowed" data-ng-hide="client.useTemplateScope && template && template.fullScopeAllowed">
+ <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageClients" data-ng-hide="hideRoleSelector()">
<div class="form-group">
<label class="col-md-2 control-label" class="control-label">{{:: 'realm-roles' | translate}}</label>
<div class="col-md-10">
@@ -132,6 +132,6 @@
</div>
</div>
</form>
- </div>
+ </div>
<kc-menu></kc-menu>
\ 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 089a65f..a4f17a8 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 @@
<div data-ng-controller="LDAPTabCtrl">
<h1 data-ng-hide="create">
- {{instance.displayName|capitalize}}
+ {{instance.name|capitalize}}
<i class="pficon pficon-delete clickable" data-ng-show="!create && access.manageUsers" data-ng-click="removeUserFederation()"></i>
</h1>
<h1 data-ng-show="create">{{:: 'add-user-federation-provider' | translate}}</h1>
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 840fa2c..3c8f429 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 @@
<replacement placeholder="CACHE-CONTAINERS">
<cache-container name="keycloak" jndi-name="infinispan/Keycloak">
<transport lock-timeout="60000"/>
- <invalidation-cache name="realms" mode="SYNC"/>
- <invalidation-cache name="users" mode="SYNC">
+ <local-cache name="realms"/>
+ <local-cache name="users">
<eviction max-entries="10000" strategy="LRU"/>
- </invalidation-cache>
+ </local-cache>
<distributed-cache name="sessions" mode="SYNC" owners="1"/>
<distributed-cache name="offlineSessions" mode="SYNC" owners="1"/>
<distributed-cache name="loginFailures" mode="SYNC" owners="1"/>