keycloak-uncached

merge

6/21/2017 6:43:54 PM

Changes

Details

diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java
index fabf30e..7e235ae 100644
--- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java
+++ b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java
@@ -61,15 +61,19 @@ import org.springframework.util.Assert;
  * @version $Revision: 1 $
  */
 public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter implements ApplicationContextAware {
-    
+
+    public static final String DEFAULT_LOGIN_URL = "/sso/login";
     public static final String AUTHORIZATION_HEADER = "Authorization";
     public static final String SCHEME_BEARER = "bearer ";
     public static final String SCHEME_BASIC = "basic ";
 
+
     /**
-     * Request matcher that matches all requests.
+     * Request matcher that matches requests to the {@link KeycloakAuthenticationEntryPoint#DEFAULT_LOGIN_URI default login URI}
+     * and any request with a <code>Authorization</code> header.
      */
-    private static RequestMatcher DEFAULT_REQUEST_MATCHER = new AntPathRequestMatcher("/**");
+    public static final RequestMatcher DEFAULT_REQUEST_MATCHER =
+            new OrRequestMatcher(new AntPathRequestMatcher(DEFAULT_LOGIN_URL), new RequestHeaderRequestMatcher(AUTHORIZATION_HEADER));
 
     private static final Logger log = LoggerFactory.getLogger(KeycloakAuthenticationProcessingFilter.class);
 
@@ -107,7 +111,7 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
      *
      */
     public KeycloakAuthenticationProcessingFilter(AuthenticationManager authenticationManager, RequestMatcher
-                requiresAuthenticationRequestMatcher) {
+            requiresAuthenticationRequestMatcher) {
         super(requiresAuthenticationRequestMatcher);
         Assert.notNull(authenticationManager, "authenticationManager cannot be null");
         this.authenticationManager = authenticationManager;
@@ -138,20 +142,27 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
         log.debug("Auth outcome: {}", result);
 
         if (AuthOutcome.FAILED.equals(result)) {
-        	AuthChallenge challenge = authenticator.getChallenge();
+            AuthChallenge challenge = authenticator.getChallenge();
             if (challenge != null) {
                 challenge.challenge(facade);
             }
             throw new KeycloakAuthenticationException("Invalid authorization header, see WWW-Authenticate header for details");
         }
+
         if (AuthOutcome.NOT_ATTEMPTED.equals(result)) {
-        	AuthChallenge challenge = authenticator.getChallenge();
+            AuthChallenge challenge = authenticator.getChallenge();
             if (challenge != null) {
                 challenge.challenge(facade);
             }
-            throw new KeycloakAuthenticationException("Authorization header not found, see WWW-Authenticate header");
+            if (deployment.isBearerOnly()) {
+                // no redirection in this mode, throwing exception for the spring handler
+                throw new KeycloakAuthenticationException("Authorization header not found,  see WWW-Authenticate header");
+            } else {
+                // let continue if challenged, it may redirect
+                return null;
+            }
         }
-       
+
         else if (AuthOutcome.AUTHENTICATED.equals(result)) {
             Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
             Assert.notNull(authentication, "Authentication SecurityContextHolder was null");
@@ -193,7 +204,7 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
 
     @Override
     protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
-            Authentication authResult) throws IOException, ServletException {
+                                            Authentication authResult) throws IOException, ServletException {
 
         if (!(this.isBearerTokenRequest(request) || this.isBasicAuthRequest(request))) {
             super.successfulAuthentication(request, response, chain, authResult);
@@ -220,10 +231,10 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
     }
 
     @Override
-	protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
-			AuthenticationException failed) throws IOException, ServletException {
-		super.unsuccessfulAuthentication(request, response, failed);
-	}
+    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
+                                              AuthenticationException failed) throws IOException, ServletException {
+        super.unsuccessfulAuthentication(request, response, failed);
+    }
 
     @Override
     public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
@@ -259,4 +270,4 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati
     public final void setContinueChainBeforeSuccessfulAuthentication(boolean continueChainBeforeSuccessfulAuthentication) {
         throw new UnsupportedOperationException("This filter does not support explicitly setting a continue chain before success policy");
     }
-}
+}
\ No newline at end of file
diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/KeycloakConfiguration.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/KeycloakConfiguration.java
new file mode 100644
index 0000000..f434b97
--- /dev/null
+++ b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/KeycloakConfiguration.java
@@ -0,0 +1,25 @@
+package org.keycloak.adapters.springsecurity;
+
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Add this annotation to a class that extends {@code KeycloakWebSecurityConfigurerAdapter} to provide
+ * a keycloak based Spring security configuration.
+ *
+ * @author Hendrik Ebbers
+ */
+@Retention(value = RUNTIME)
+@Target(value = { TYPE })
+@Configuration
+@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
+@EnableWebSecurity
+public @interface KeycloakConfiguration {
+}
diff --git a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilterTest.java b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilterTest.java
index 2414c38..a6a378a 100755
--- a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilterTest.java
+++ b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilterTest.java
@@ -159,12 +159,10 @@ public class KeycloakAuthenticationProcessingFilterTest {
         when(keycloakDeployment.getStateCookieName()).thenReturn("kc-cookie");
         when(keycloakDeployment.getSslRequired()).thenReturn(SslRequired.NONE);
         when(keycloakDeployment.isBearerOnly()).thenReturn(Boolean.FALSE);
-        try {
-        	filter.attemptAuthentication(request, response);
-        } catch (KeycloakAuthenticationException e) {
-        	verify(response).setStatus(302);
-            verify(response).setHeader(eq("Location"), startsWith("http://localhost:8080/auth"));
-        }
+
+        filter.attemptAuthentication(request, response);
+        verify(response).setStatus(302);
+        verify(response).setHeader(eq("Location"), startsWith("http://localhost:8080/auth"));
     }
 
     @Test(expected = KeycloakAuthenticationException.class)
@@ -173,6 +171,13 @@ public class KeycloakAuthenticationProcessingFilterTest {
         filter.attemptAuthentication(request, response);
     }
 
+    @Test(expected = KeycloakAuthenticationException.class)
+    public void testAttemptAuthenticationWithInvalidTokenBearerOnly() throws Exception {
+        when(keycloakDeployment.isBearerOnly()).thenReturn(Boolean.TRUE);
+        request.addHeader("Authorization", "Bearer xxx");
+        filter.attemptAuthentication(request, response);
+    }
+
     @Test
     public void testSuccessfulAuthenticationInteractive() throws Exception {
         Authentication authentication = new KeycloakAuthenticationToken(keycloakAccount, authorities);
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProvider.java
new file mode 100644
index 0000000..2ae1bdc
--- /dev/null
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProvider.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.policy.provider.group;
+
+import static org.keycloak.models.utils.ModelToRepresentation.buildGroupPath;
+
+import java.util.function.Function;
+
+import org.keycloak.authorization.attribute.Attributes;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.policy.evaluation.Evaluation;
+import org.keycloak.authorization.policy.provider.PolicyProvider;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPolicyProvider implements PolicyProvider {
+
+    private final Function<Policy, GroupPolicyRepresentation> representationFunction;
+
+    public GroupPolicyProvider(Function<Policy, GroupPolicyRepresentation> representationFunction) {
+        this.representationFunction = representationFunction;
+    }
+
+    @Override
+    public void evaluate(Evaluation evaluation) {
+        GroupPolicyRepresentation policy = representationFunction.apply(evaluation.getPolicy());
+        RealmModel realm = evaluation.getAuthorizationProvider().getRealm();
+        Attributes.Entry groupsClaim = evaluation.getContext().getIdentity().getAttributes().getValue(policy.getGroupsClaim());
+
+        if (groupsClaim == null || groupsClaim.isEmpty()) {
+            return;
+        }
+
+        for (GroupPolicyRepresentation.GroupDefinition definition : policy.getGroups()) {
+            GroupModel allowedGroup = realm.getGroupById(definition.getId());
+
+            for (int i = 0; i < groupsClaim.size(); i++) {
+                String group = groupsClaim.asString(i);
+
+                if (group.indexOf('/') != -1) {
+                    String allowedGroupPath = buildGroupPath(allowedGroup);
+                    if (group.equals(allowedGroupPath) || (definition.isExtendChildren() && group.startsWith(allowedGroupPath))) {
+                        evaluation.grant();
+                        return;
+                    }
+                }
+
+                // in case the group from the claim does not represent a path, we just check an exact name match
+                if (group.equals(allowedGroup.getName())) {
+                    evaluation.grant();
+                    return;
+                }
+            }
+        }
+    }
+
+    @Override
+    public void close() {
+
+    }
+}
\ No newline at end of file
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProviderFactory.java
new file mode 100644
index 0000000..f558449
--- /dev/null
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProviderFactory.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.policy.provider.group;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.keycloak.Config;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.policy.provider.PolicyProvider;
+import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPolicyProviderFactory implements PolicyProviderFactory<GroupPolicyRepresentation> {
+
+    private GroupPolicyProvider provider = new GroupPolicyProvider(policy -> toRepresentation(policy, new GroupPolicyRepresentation()));
+
+    @Override
+    public String getId() {
+        return "group";
+    }
+
+    @Override
+    public String getName() {
+        return "Group";
+    }
+
+    @Override
+    public String getGroup() {
+        return "Identity Based";
+    }
+
+    @Override
+    public PolicyProvider create(AuthorizationProvider authorization) {
+        return provider;
+    }
+
+    @Override
+    public PolicyProvider create(KeycloakSession session) {
+        return provider;
+    }
+
+    @Override
+    public GroupPolicyRepresentation toRepresentation(Policy policy, GroupPolicyRepresentation representation) {
+        representation.setGroupsClaim(policy.getConfig().get("groupsClaim"));
+        try {
+            representation.setGroups(getGroupsDefinition(policy.getConfig()));
+        } catch (IOException cause) {
+            throw new RuntimeException("Failed to deserialize groups", cause);
+        }
+        return representation;
+    }
+
+    @Override
+    public Class<GroupPolicyRepresentation> getRepresentationType() {
+        return GroupPolicyRepresentation.class;
+    }
+
+    @Override
+    public void onCreate(Policy policy, GroupPolicyRepresentation representation, AuthorizationProvider authorization) {
+        updatePolicy(policy, representation.getGroupsClaim(), representation.getGroups(), authorization);
+    }
+
+    @Override
+    public void onUpdate(Policy policy, GroupPolicyRepresentation representation, AuthorizationProvider authorization) {
+        updatePolicy(policy, representation.getGroupsClaim(), representation.getGroups(), authorization);
+    }
+
+    @Override
+    public void onImport(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorization) {
+        try {
+            updatePolicy(policy, representation.getConfig().get("groupsClaim"), getGroupsDefinition(representation.getConfig()), authorization);
+        } catch (IOException cause) {
+            throw new RuntimeException("Failed to deserialize groups", cause);
+        }
+    }
+
+    @Override
+    public void onExport(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorizationProvider) {
+        Map<String, String> config = new HashMap<>();
+        GroupPolicyRepresentation groupPolicy = toRepresentation(policy, new GroupPolicyRepresentation());
+        Set<GroupPolicyRepresentation.GroupDefinition> groups = groupPolicy.getGroups();
+
+        for (GroupPolicyRepresentation.GroupDefinition definition: groups) {
+            GroupModel group = authorizationProvider.getRealm().getGroupById(definition.getId());
+            definition.setId(null);
+            definition.setPath(ModelToRepresentation.buildGroupPath(group));
+        }
+
+        try {
+            config.put("groupsClaim", groupPolicy.getGroupsClaim());
+            config.put("groups", JsonSerialization.writeValueAsString(groups));
+        } catch (IOException cause) {
+            throw new RuntimeException("Failed to export group policy [" + policy.getName() + "]", cause);
+        }
+
+        representation.setConfig(config);
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+        factory.register(event -> {
+        });
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    private void updatePolicy(Policy policy, String groupsClaim, Set<GroupPolicyRepresentation.GroupDefinition> groups, AuthorizationProvider authorization) {
+        if (groupsClaim == null) {
+            throw new RuntimeException("Group claims property not provided");
+        }
+
+        if (groups == null || groups.isEmpty()) {
+            throw new RuntimeException("You must provide at least one group");
+        }
+
+        Map<String, String> config = new HashMap<>(policy.getConfig());
+
+        config.put("groupsClaim", groupsClaim);
+
+        List<GroupModel> topLevelGroups = authorization.getRealm().getTopLevelGroups();
+
+        for (GroupPolicyRepresentation.GroupDefinition definition : groups) {
+            GroupModel group = null;
+
+            if (definition.getId() != null) {
+                group = authorization.getRealm().getGroupById(definition.getId());
+            }
+
+            if (group == null) {
+                String path = definition.getPath();
+                String canonicalPath = path.startsWith("/") ? path.substring(1, path.length()) : path;
+
+                if (canonicalPath != null) {
+                    String[] parts = canonicalPath.split("/");
+                    GroupModel parent = null;
+
+                    for (String part : parts) {
+                        if (parent == null) {
+                            parent = topLevelGroups.stream().filter(groupModel -> groupModel.getName().equals(part)).findFirst().orElseThrow(() -> new RuntimeException("Top level group with name [" + part + "] not found"));
+                        } else {
+                            group = parent.getSubGroups().stream().filter(groupModel -> groupModel.getName().equals(part)).findFirst().orElseThrow(() -> new RuntimeException("Group with name [" + part + "] not found"));
+                            parent = group;
+                        }
+                    }
+
+                    if (parts.length == 1) {
+                        group = parent;
+                    }
+                }
+            }
+
+            if (group == null) {
+                throw new RuntimeException("Group with id [" + definition.getId() + "] not found");
+            }
+
+            definition.setId(group.getId());
+            definition.setPath(null);
+        }
+
+        try {
+            config.put("groups", JsonSerialization.writeValueAsString(groups));
+        } catch (IOException cause) {
+            throw new RuntimeException("Failed to serialize groups", cause);
+        }
+
+        policy.setConfig(config);
+    }
+
+    private HashSet<GroupPolicyRepresentation.GroupDefinition> getGroupsDefinition(Map<String, String> config) throws IOException {
+        return new HashSet<>(Arrays.asList(JsonSerialization.readValue(config.get("groups"), GroupPolicyRepresentation.GroupDefinition[].class)));
+    }
+}
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java
index f875731..944ae02 100644
--- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java
@@ -17,43 +17,44 @@
  */
 package org.keycloak.authorization.policy.provider.js;
 
-import java.util.function.Supplier;
-
-import javax.script.ScriptEngine;
-import javax.script.ScriptException;
+import java.util.function.BiFunction;
 
+import org.keycloak.authorization.AuthorizationProvider;
 import org.keycloak.authorization.model.Policy;
 import org.keycloak.authorization.policy.evaluation.Evaluation;
 import org.keycloak.authorization.policy.provider.PolicyProvider;
+import org.keycloak.scripting.EvaluatableScriptAdapter;
 
 /**
  * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
  */
-public class JSPolicyProvider implements PolicyProvider {
+class JSPolicyProvider implements PolicyProvider {
 
-    private Supplier<ScriptEngine> engineProvider;
+    private final BiFunction<AuthorizationProvider, Policy, EvaluatableScriptAdapter> evaluatableScript;
 
-    public JSPolicyProvider(Supplier<ScriptEngine> engineProvider) {
-        this.engineProvider = engineProvider;
+    JSPolicyProvider(final BiFunction<AuthorizationProvider, Policy, EvaluatableScriptAdapter> evaluatableScript) {
+        this.evaluatableScript = evaluatableScript;
     }
 
     @Override
     public void evaluate(Evaluation evaluation) {
-        ScriptEngine engine = engineProvider.get();
-
-        engine.put("$evaluation", evaluation);
-
         Policy policy = evaluation.getPolicy();
+        AuthorizationProvider authorization = evaluation.getAuthorizationProvider();
+        final EvaluatableScriptAdapter adapter = evaluatableScript.apply(authorization, policy);
 
         try {
-            engine.eval(policy.getConfig().get("code"));
-        } catch (ScriptException e) {
+            //how to deal with long running scripts -> timeout?
+            adapter.eval(bindings -> {
+                bindings.put("script", adapter.getScriptModel());
+                bindings.put("$evaluation", evaluation);
+            });
+        }
+        catch (Exception e) {
             throw new RuntimeException("Error evaluating JS Policy [" + policy.getName() + "].", e);
         }
     }
 
     @Override
     public void close() {
-
     }
 }
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java
index 3e68d7f..e3e82ce 100644
--- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java
@@ -1,9 +1,5 @@
 package org.keycloak.authorization.policy.provider.js;
 
-import java.util.Map;
-
-import javax.script.ScriptEngineManager;
-
 import org.keycloak.Config;
 import org.keycloak.authorization.AuthorizationProvider;
 import org.keycloak.authorization.model.Policy;
@@ -11,17 +7,20 @@ import org.keycloak.authorization.policy.provider.PolicyProvider;
 import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.ScriptModel;
 import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
 import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.scripting.EvaluatableScriptAdapter;
+import org.keycloak.scripting.ScriptingProvider;
 
 /**
  * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
  */
 public class JSPolicyProviderFactory implements PolicyProviderFactory<JSPolicyRepresentation> {
 
-    private static final String ENGINE = "nashorn";
-
-    private JSPolicyProvider provider = new JSPolicyProvider(() -> new ScriptEngineManager().getEngineByName(ENGINE));
+    private final JSPolicyProvider provider = new JSPolicyProvider(this::getEvaluatableScript);
+    private ScriptCache scriptCache;
 
     @Override
     public String getName() {
@@ -69,13 +68,16 @@ public class JSPolicyProviderFactory implements PolicyProviderFactory<JSPolicyRe
         updatePolicy(policy, representation.getConfig().get("code"));
     }
 
-    private void updatePolicy(Policy policy, String code) {
-        policy.putConfig("code", code);
+    @Override
+    public void onRemove(final Policy policy, final AuthorizationProvider authorization) {
+        scriptCache.remove(policy.getId());
     }
 
     @Override
     public void init(Config.Scope config) {
-
+        int maxEntries = Integer.parseInt(config.get("cache-max-entries", "100"));
+        int maxAge = Integer.parseInt(config.get("cache-entry-max-age", "-1"));
+        scriptCache = new ScriptCache(maxEntries, maxAge);
     }
 
     @Override
@@ -92,4 +94,26 @@ public class JSPolicyProviderFactory implements PolicyProviderFactory<JSPolicyRe
     public String getId() {
         return "js";
     }
+
+    private EvaluatableScriptAdapter getEvaluatableScript(final AuthorizationProvider authz, final Policy policy) {
+        return scriptCache.computeIfAbsent(policy.getId(), id -> {
+            final ScriptingProvider scripting = authz.getKeycloakSession().getProvider(ScriptingProvider.class);
+            ScriptModel script = getScriptModel(policy, authz.getRealm(), scripting);
+            return scripting.prepareEvaluatableScript(script);
+        });
+    }
+
+    private ScriptModel getScriptModel(final Policy policy, final RealmModel realm, final ScriptingProvider scripting) {
+        String scriptName = policy.getName();
+        String scriptCode = policy.getConfig().get("code");
+        String scriptDescription = policy.getDescription();
+
+        //TODO lookup script by scriptId instead of creating it every time
+        return scripting.createScript(realm.getId(), ScriptModel.TEXT_JAVASCRIPT, scriptName, scriptCode, scriptDescription);
+    }
+
+    private void updatePolicy(Policy policy, String code) {
+        scriptCache.remove(policy.getId());
+        policy.putConfig("code", code);
+    }
 }
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/ScriptCache.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/ScriptCache.java
new file mode 100644
index 0000000..4180db6
--- /dev/null
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/ScriptCache.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.policy.provider.js;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.LockSupport;
+import java.util.function.Function;
+
+import org.keycloak.scripting.EvaluatableScriptAdapter;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ScriptCache {
+
+    /**
+     * The load factor.
+     */
+    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
+
+    private final Map<String, CacheEntry> cache;
+
+    private final AtomicBoolean writing = new AtomicBoolean(false);
+
+    private final long maxAge;
+
+    /**
+     * Creates a new instance.
+     *
+     * @param maxEntries the maximum number of entries to keep in the cache
+     */
+    public ScriptCache(int maxEntries) {
+        this(maxEntries, -1);
+    }
+
+    /**
+     * Creates a new instance.
+     *
+     * @param maxEntries the maximum number of entries to keep in the cache
+     * @param maxAge the time in milliseconds that an entry can stay in the cache. If {@code -1}, entries never expire
+     */
+    public ScriptCache(final int maxEntries, long maxAge) {
+        cache = new LinkedHashMap<String, CacheEntry>(16, DEFAULT_LOAD_FACTOR, true) {
+            @Override
+            protected boolean removeEldestEntry(Map.Entry eldest) {
+                return cache.size()  > maxEntries;
+            }
+        };
+        this.maxAge = maxAge;
+    }
+
+    public EvaluatableScriptAdapter computeIfAbsent(String id, Function<String, EvaluatableScriptAdapter> function) {
+        try {
+            if (parkForWriteAndCheckInterrupt()) {
+                return null;
+            }
+
+            CacheEntry entry = cache.computeIfAbsent(id, key -> new CacheEntry(key, function.apply(id), maxAge));
+
+            if (entry != null) {
+                return entry.value();
+            }
+
+            return null;
+        } finally {
+            writing.lazySet(false);
+        }
+    }
+
+    public EvaluatableScriptAdapter get(String uri) {
+        if (parkForReadAndCheckInterrupt()) {
+            return null;
+        }
+
+        CacheEntry cached = cache.get(uri);
+
+        if (cached != null) {
+            return removeIfExpired(cached);
+        }
+
+        return null;
+    }
+
+    public void remove(String key) {
+        try {
+            if (parkForWriteAndCheckInterrupt()) {
+                return;
+            }
+
+            cache.remove(key);
+        } finally {
+            writing.lazySet(false);
+        }
+    }
+
+    private EvaluatableScriptAdapter removeIfExpired(CacheEntry cached) {
+        if (cached == null) {
+            return null;
+        }
+
+        if (cached.isExpired()) {
+            remove(cached.key());
+            return null;
+        }
+
+        return cached.value();
+    }
+
+    private boolean parkForWriteAndCheckInterrupt() {
+        while (!writing.compareAndSet(false, true)) {
+            LockSupport.parkNanos(1L);
+            if (Thread.interrupted()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean parkForReadAndCheckInterrupt() {
+        while (writing.get()) {
+            LockSupport.parkNanos(1L);
+            if (Thread.interrupted()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static final class CacheEntry {
+
+        final String key;
+        final EvaluatableScriptAdapter value;
+        final long expiration;
+
+        CacheEntry(String key, EvaluatableScriptAdapter value, long maxAge) {
+            this.key = key;
+            this.value = value;
+            if(maxAge == -1) {
+                expiration = -1;
+            } else {
+                expiration = System.currentTimeMillis() + maxAge;
+            }
+        }
+
+        String key() {
+            return key;
+        }
+
+        EvaluatableScriptAdapter value() {
+            return value;
+        }
+
+        boolean isExpired() {
+            return expiration != -1 ? System.currentTimeMillis() > expiration : false;
+        }
+    }
+}
diff --git a/authz/policy/common/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory b/authz/policy/common/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
index e4588f8..e6fa1cc 100644
--- a/authz/policy/common/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
+++ b/authz/policy/common/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
@@ -41,4 +41,5 @@ org.keycloak.authorization.policy.provider.role.RolePolicyProviderFactory
 org.keycloak.authorization.policy.provider.scope.ScopePolicyProviderFactory
 org.keycloak.authorization.policy.provider.time.TimePolicyProviderFactory
 org.keycloak.authorization.policy.provider.user.UserPolicyProviderFactory
-org.keycloak.authorization.policy.provider.client.ClientPolicyProviderFactory
\ No newline at end of file
+org.keycloak.authorization.policy.provider.client.ClientPolicyProviderFactory
+org.keycloak.authorization.policy.provider.group.GroupPolicyProviderFactory
\ No newline at end of file
diff --git a/common/src/main/java/org/keycloak/common/util/ConcurrentMultivaluedHashMap.java b/common/src/main/java/org/keycloak/common/util/ConcurrentMultivaluedHashMap.java
index c092c6f..258f076 100755
--- a/common/src/main/java/org/keycloak/common/util/ConcurrentMultivaluedHashMap.java
+++ b/common/src/main/java/org/keycloak/common/util/ConcurrentMultivaluedHashMap.java
@@ -31,9 +31,9 @@ public class ConcurrentMultivaluedHashMap<K, V> extends ConcurrentHashMap<K, Lis
 {
    public void putSingle(K key, V value)
    {
-      List<V> list = new CopyOnWriteArrayList<>();
+      List<V> list = createListInstance();
       list.add(value);
-      put(key, list);
+      put(key, list); // Just override with new List instance
    }
 
    public void addAll(K key, V... newValues)
@@ -84,8 +84,15 @@ public class ConcurrentMultivaluedHashMap<K, V> extends ConcurrentHashMap<K, Lis
    public final List<V> getList(K key)
    {
       List<V> list = get(key);
-      if (list == null)
-         put(key, list = new CopyOnWriteArrayList<V>());
+
+      if (list == null) {
+         list = createListInstance();
+         List<V> existing = putIfAbsent(key, list);
+         if (existing != null) {
+            list = existing;
+         }
+      }
+
       return list;
    }
 
@@ -97,4 +104,8 @@ public class ConcurrentMultivaluedHashMap<K, V> extends ConcurrentHashMap<K, Lis
       }
    }
 
+   protected List<V> createListInstance() {
+      return new CopyOnWriteArrayList<>();
+   }
+
 }
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/GroupPolicyRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/GroupPolicyRepresentation.java
new file mode 100644
index 0000000..c063f8f
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/GroupPolicyRepresentation.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.representations.idm.authorization;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPolicyRepresentation extends AbstractPolicyRepresentation {
+
+    private String groupsClaim;
+    private Set<GroupDefinition> groups;
+
+    @Override
+    public String getType() {
+        return "group";
+    }
+
+    public String getGroupsClaim() {
+        return groupsClaim;
+    }
+
+    public void setGroupsClaim(String groupsClaim) {
+        this.groupsClaim = groupsClaim;
+    }
+
+    public Set<GroupDefinition> getGroups() {
+        return groups;
+    }
+
+    public void setGroups(Set<GroupDefinition> groups) {
+        this.groups = groups;
+    }
+
+    public void addGroup(String... ids) {
+        for (String id : ids) {
+            addGroup(id, false);
+        }
+    }
+
+    public void addGroup(String id, boolean extendChildren) {
+        if (groups == null) {
+            groups = new HashSet<>();
+        }
+        groups.add(new GroupDefinition(id, extendChildren));
+    }
+
+    public void addGroupPath(String... paths) {
+        for (String path : paths) {
+            addGroupPath(path, false);
+        }
+    }
+
+    public void addGroupPath(String path, boolean extendChildren) {
+        if (groups == null) {
+            groups = new HashSet<>();
+        }
+        groups.add(new GroupDefinition(null, path, extendChildren));
+    }
+
+    public void removeGroup(String... ids) {
+        if (groups != null) {
+            for (final String id : ids) {
+                if (!groups.remove(id)) {
+                    for (GroupDefinition group : new HashSet<>(groups)) {
+                        if (group.getPath().startsWith(id)) {
+                            groups.remove(group);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    public static class GroupDefinition {
+
+        private String id;
+        private String path;
+        private boolean extendChildren;
+
+        public GroupDefinition() {
+            this(null);
+        }
+
+        public GroupDefinition(String id) {
+            this(id, false);
+        }
+
+        public GroupDefinition(String id, boolean extendChildren) {
+            this(id, null, extendChildren);
+        }
+
+        public GroupDefinition(String id, String path, boolean extendChildren) {
+            this.id = id;
+            this.path = path;
+            this.extendChildren = extendChildren;
+        }
+
+        public String getId() {
+            return id;
+        }
+
+        public void setId(String id) {
+            this.id = id;
+        }
+
+        public String getPath() {
+            return path;
+        }
+
+        public void setPath(String path) {
+            this.path = path;
+        }
+
+        public boolean isExtendChildren() {
+            return extendChildren;
+        }
+
+        public void setExtendChildren(boolean extendChildren) {
+            this.extendChildren = extendChildren;
+        }
+    }
+}
diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProviderFactory.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProviderFactory.java
index e061f5e..75e9d18 100755
--- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProviderFactory.java
+++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProviderFactory.java
@@ -156,4 +156,9 @@ public class KerberosFederationProviderFactory implements UserStorageProviderFac
                 AuthenticationExecutionModel.Requirement.ALTERNATIVE, AuthenticationExecutionModel.Requirement.DISABLED);
     }
 
+    @Override
+    public void preRemove(KeycloakSession session, RealmModel realm, ComponentModel model) {
+        CredentialHelper.setOrReplaceAuthenticationRequirement(session, realm, CredentialRepresentation.KERBEROS,
+                AuthenticationExecutionModel.Requirement.DISABLED, null);
+    }
 }
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java
index c962af8..0d4c07b 100755
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java
@@ -384,8 +384,14 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
 
     }
 
-
-
+    @Override
+    public void preRemove(KeycloakSession session, RealmModel realm, ComponentModel model) {
+        String allowKerberosCfg = model.getConfig().getFirst(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION);
+        if (Boolean.valueOf(allowKerberosCfg)) {
+            CredentialHelper.setOrReplaceAuthenticationRequirement(session, realm, CredentialRepresentation.KERBEROS,
+                    AuthenticationExecutionModel.Requirement.DISABLED, null);
+        }
+    }
 
     @Override
     public SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model) {
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupPoliciesResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupPoliciesResource.java
new file mode 100644
index 0000000..1cc51b0
--- /dev/null
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupPoliciesResource.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.admin.client.resource;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface GroupPoliciesResource {
+
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    Response create(GroupPolicyRepresentation representation);
+
+    @Path("{id}")
+    GroupPolicyResource findById(@PathParam("id") String id);
+
+    @Path("/search")
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @NoCache
+    GroupPolicyRepresentation findByName(@QueryParam("name") String name);
+}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupPolicyResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupPolicyResource.java
new file mode 100644
index 0000000..6171868
--- /dev/null
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupPolicyResource.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.admin.client.resource;
+
+import java.util.List;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface GroupPolicyResource {
+
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @NoCache
+    GroupPolicyRepresentation toRepresentation();
+
+    @PUT
+    @Consumes(MediaType.APPLICATION_JSON)
+    void update(GroupPolicyRepresentation representation);
+
+    @DELETE
+    void remove();
+
+    @Path("/associatedPolicies")
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @NoCache
+    List<PolicyRepresentation> associatedPolicies();
+
+    @Path("/dependentPolicies")
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @NoCache
+    List<PolicyRepresentation> dependentPolicies();
+
+    @Path("/resources")
+    @GET
+    @Produces("application/json")
+    @NoCache
+    List<ResourceRepresentation> resources();
+
+}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PoliciesResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PoliciesResource.java
index a0af5d4..9ced12c 100644
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PoliciesResource.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PoliciesResource.java
@@ -89,4 +89,7 @@ public interface PoliciesResource {
 
     @Path("client")
     ClientPoliciesResource client();
+
+    @Path("group")
+    GroupPoliciesResource group();
 }
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 76b0779..53e496f 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
@@ -156,8 +156,12 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
             String nodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME));
             String jgroupsUdpMcastAddr = config.get("jgroupsUdpMcastAddr", System.getProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR));
             configureTransport(gcb, nodeName, jgroupsUdpMcastAddr);
+            gcb.globalJmxStatistics()
+              .jmxDomain(InfinispanConnectionProvider.JMX_DOMAIN + "-" + nodeName);
         }
-        gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
+        gcb.globalJmxStatistics()
+          .allowDuplicateDomains(allowDuplicateJMXDomains)
+          .enable();
 
         cacheManager = new DefaultCacheManager(gcb.build());
         containerManaged = false;
@@ -339,8 +343,13 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
                     channel.setName(nodeName);
                     JGroupsTransport transport = new JGroupsTransport(channel);
 
-                    gcb.transport().nodeName(nodeName);
-                    gcb.transport().transport(transport);
+                    gcb.transport()
+                      .nodeName(nodeName)
+                      .transport(transport)
+                      .globalJmxStatistics()
+                        .jmxDomain(InfinispanConnectionProvider.JMX_DOMAIN + "-" + nodeName)
+                        .enable()
+                      ;
 
                     logger.infof("Configured jgroups transport with the channel name: %s", nodeName);
                 } catch (Exception e) {
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
index 7fd2652..e8cdbf6 100755
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
@@ -55,6 +55,7 @@ public interface InfinispanConnectionProvider extends Provider {
     String JBOSS_NODE_NAME = "jboss.node.name";
     String JGROUPS_UDP_MCAST_ADDR = "jgroups.udp.mcast_addr";
 
+    String JMX_DOMAIN = "jboss.datagrid-infinispan";
 
     <K, V> Cache<K, V> getCache(String name);
 
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 33ca943..eba62db 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
@@ -1885,6 +1885,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
         ComponentEntity c = em.find(ComponentEntity.class, component.getId());
         if (c == null) return;
         session.users().preRemove(this, component);
+        ComponentUtil.notifyPreRemove(session, this, component);
         removeComponents(component.getId());
         getEntity().getComponents().remove(c);
     }
@@ -1896,7 +1897,10 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
         getEntity().getComponents().stream()
                 .filter(sameParent)
                 .map(this::entityToModel)
-                .forEach(c -> session.users().preRemove(this, c));
+                .forEach((ComponentModel c) -> {
+                    session.users().preRemove(this, c);
+                    ComponentUtil.notifyPreRemove(session, this, c);
+                });
 
         getEntity().getComponents().removeIf(sameParent);
     }
diff --git a/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java b/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java
index 695637f..b4c6f44 100644
--- a/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java
+++ b/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java
@@ -80,6 +80,18 @@ public interface ComponentFactory<CreatedType, ProviderType extends Provider> ex
     }
 
     /**
+     * Called before the component is removed.
+     *
+     * @param session
+     * @param realm
+     * @param model model of the component, which is going to be removed
+     */
+    default
+    void preRemove(KeycloakSession session, RealmModel realm, ComponentModel model) {
+
+    }
+
+    /**
      * These are config properties that are common across all implementation of this component type
      *
      * @return
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java b/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java
index c83d9f8..0719bab 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java
@@ -107,6 +107,10 @@ public interface Attributes {
             return values.length;
         }
 
+        public boolean isEmpty() {
+            return values.length == 0;
+        }
+
         public String asString(int idx) {
             if (idx >= values.length) {
                 throw new IllegalArgumentException("Invalid index [" + idx + "]. Values are [" + values + "].");
diff --git a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java
index ac435e3..cb203c1 100755
--- a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java
@@ -120,4 +120,6 @@ public interface LoginFormsProvider extends Provider {
     public LoginFormsProvider setStatus(Response.Status status);
 
     LoginFormsProvider setActionUri(URI requestUri);
+
+    LoginFormsProvider setExecution(String execution);
 }
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java
index 1d41f0d..0c603c4 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java
@@ -17,6 +17,7 @@
 
 package org.keycloak.models.utils;
 
+import org.jboss.logging.Logger;
 import org.keycloak.component.ComponentFactory;
 import org.keycloak.component.ComponentModel;
 import org.keycloak.models.KeycloakSession;
@@ -38,6 +39,8 @@ import java.util.Map;
  */
 public class ComponentUtil {
 
+    private static final Logger logger = Logger.getLogger(ComponentUtil.class);
+
     public static Map<String, ProviderConfigProperty> getComponentConfigProperties(KeycloakSession session, ComponentRepresentation component) {
         return getComponentConfigProperties(session, component.getProviderType(), component.getProviderId());
     }
@@ -102,5 +105,14 @@ public class ComponentUtil {
             ((OnUpdateComponent)session.userStorageManager()).onUpdate(session, realm, oldModel, newModel);
         }
     }
+    public static void notifyPreRemove(KeycloakSession session, RealmModel realm, ComponentModel model) {
+        try {
+            ComponentFactory factory = getComponentFactory(session, model);
+            factory.preRemove(session, realm, model);
+        } catch (IllegalArgumentException iae) {
+            // We allow to remove broken providers without throwing an exception
+            logger.warn(iae.getMessage());
+        }
+    }
 
 }
diff --git a/server-spi-private/src/main/java/org/keycloak/scripting/EvaluatableScriptAdapter.java b/server-spi-private/src/main/java/org/keycloak/scripting/EvaluatableScriptAdapter.java
new file mode 100644
index 0000000..2a76add
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/scripting/EvaluatableScriptAdapter.java
@@ -0,0 +1,14 @@
+package org.keycloak.scripting;
+
+import org.keycloak.models.ScriptModel;
+
+/**
+ * Wraps a {@link ScriptModel} so it can be evaluated with custom bindings.
+ *
+ * @author <a href="mailto:jay@anslow.me.uk">Jay Anslow</a>
+ */
+public interface EvaluatableScriptAdapter {
+    ScriptModel getScriptModel();
+
+    Object eval(ScriptBindingsConfigurer bindingsConfigurer) throws ScriptExecutionException;
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/scripting/InvocableScriptAdapter.java b/server-spi-private/src/main/java/org/keycloak/scripting/InvocableScriptAdapter.java
index c3859ab..17bb4a1 100644
--- a/server-spi-private/src/main/java/org/keycloak/scripting/InvocableScriptAdapter.java
+++ b/server-spi-private/src/main/java/org/keycloak/scripting/InvocableScriptAdapter.java
@@ -56,7 +56,7 @@ public class InvocableScriptAdapter implements Invocable {
         }
 
         this.scriptModel = scriptModel;
-        this.scriptEngine = loadScriptIntoEngine(scriptModel, scriptEngine);
+        this.scriptEngine = scriptEngine;
     }
 
     @Override
@@ -101,17 +101,6 @@ public class InvocableScriptAdapter implements Invocable {
         return candidate != null;
     }
 
-    private ScriptEngine loadScriptIntoEngine(ScriptModel script, ScriptEngine engine) {
-
-        try {
-            engine.eval(script.getCode());
-        } catch (ScriptException se) {
-            throw new ScriptExecutionException(script, se);
-        }
-
-        return engine;
-    }
-
     private Invocable getInvocableEngine() {
         return (Invocable) scriptEngine;
     }
diff --git a/server-spi-private/src/main/java/org/keycloak/scripting/ScriptingProvider.java b/server-spi-private/src/main/java/org/keycloak/scripting/ScriptingProvider.java
index 67bad5a..ef2990f 100644
--- a/server-spi-private/src/main/java/org/keycloak/scripting/ScriptingProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/scripting/ScriptingProvider.java
@@ -39,6 +39,14 @@ public interface ScriptingProvider extends Provider {
     InvocableScriptAdapter prepareInvocableScript(ScriptModel scriptModel, ScriptBindingsConfigurer bindingsConfigurer);
 
     /**
+     * Returns an {@link EvaluatableScriptAdapter} based on the given {@link ScriptModel}.
+     * <p>The {@code EvaluatableScriptAdapter} wraps a dedicated {@link ScriptEngine} that was populated with empty bindings.</p>
+     *
+     * @param scriptModel the scriptModel to wrap
+     */
+    EvaluatableScriptAdapter prepareEvaluatableScript(ScriptModel scriptModel);
+
+    /**
      * Creates a new {@link ScriptModel} instance.
      *
      * @param realmId
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
index 23d06e3..af7d2f7 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
@@ -471,6 +471,7 @@ public class AuthenticationProcessor {
             LoginFormsProvider provider = getSession().getProvider(LoginFormsProvider.class)
                     .setUser(getUser())
                     .setActionUri(action)
+                    .setExecution(getExecution().getId())
                     .setFormData(request.getDecodedFormParameters())
                     .setClientSessionCode(accessCode);
             if (getForwardedErrorMessage() != null) {
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
index 7189b95..ca841d0 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
@@ -169,6 +169,7 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator 
                 .setStatus(Response.Status.OK)
                 .setAttribute(LoginFormsProvider.IDENTITY_PROVIDER_BROKER_CONTEXT, brokerContext)
                 .setActionUri(action)
+                .setExecution(context.getExecution().getId())
                 .createIdpLinkEmailPage();
         context.forceChallenge(challenge);
     }
diff --git a/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java
index 82c12ec..575677d 100755
--- a/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java
+++ b/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java
@@ -270,6 +270,7 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
         URI actionUrl = getActionUrl(executionId, code);
         LoginFormsProvider form = processor.getSession().getProvider(LoginFormsProvider.class)
                 .setActionUri(actionUrl)
+                .setExecution(executionId)
                 .setClientSessionCode(code)
                 .setFormData(formData)
                 .setErrors(errors);
diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
index 1d9475a..3afb34c 100755
--- a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
+++ b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
@@ -137,11 +137,15 @@ public class RequiredActionContextResult implements RequiredActionContext {
         ClientModel client = authenticationSession.getClient();
         return LoginActionsService.requiredActionProcessor(getUriInfo())
                 .queryParam(OAuth2Constants.CODE, code)
-                .queryParam(Constants.EXECUTION, factory.getId())
+                .queryParam(Constants.EXECUTION, getExecution())
                 .queryParam(Constants.CLIENT_ID, client.getClientId())
                 .build(getRealm().getName());
     }
 
+    private String getExecution() {
+        return factory.getId();
+    }
+
     @Override
     public String generateCode() {
         ClientSessionCode<AuthenticationSessionModel> accessCode = new ClientSessionCode<>(session, getRealm(), getAuthenticationSession());
@@ -164,6 +168,7 @@ public class RequiredActionContextResult implements RequiredActionContext {
         LoginFormsProvider provider = getSession().getProvider(LoginFormsProvider.class)
                 .setUser(getUser())
                 .setActionUri(action)
+                .setExecution(getExecution())
                 .setClientSessionCode(accessCode);
         return provider;
     }
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
index 73afdf0..7c95281 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
@@ -86,6 +86,7 @@ public class ResourceSetService {
     }
 
     @POST
+    @NoCache
     @Consumes("application/json")
     @Produces("application/json")
     public Response create(@Context UriInfo uriInfo, ResourceRepresentation resource) {
@@ -288,8 +289,8 @@ public class ResourceSetService {
 
     @Path("/search")
     @GET
-    @Produces("application/json")
     @NoCache
+    @Produces("application/json")
     public Response find(@QueryParam("name") String name) {
         this.auth.realm().requireViewAuthorization();
         StoreFactory storeFactory = authorization.getStoreFactory();
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java b/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java
index ed328f5..1ab3546 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java
@@ -77,6 +77,7 @@ public class ScopeService {
     }
 
     @POST
+    @NoCache
     @Consumes(MediaType.APPLICATION_JSON)
     @Produces(MediaType.APPLICATION_JSON)
     public Response create(@Context UriInfo uriInfo,  ScopeRepresentation scope) {
@@ -150,6 +151,7 @@ public class ScopeService {
 
     @Path("{id}")
     @GET
+    @NoCache
     @Produces(MediaType.APPLICATION_JSON)
     public Response findById(@PathParam("id") String id) {
         this.auth.realm().requireViewAuthorization();
@@ -164,6 +166,7 @@ public class ScopeService {
 
     @Path("{id}/resources")
     @GET
+    @NoCache
     @Produces(MediaType.APPLICATION_JSON)
     public Response getResources(@PathParam("id") String id) {
         this.auth.realm().requireViewAuthorization();
@@ -186,6 +189,7 @@ public class ScopeService {
 
     @Path("{id}/permissions")
     @GET
+    @NoCache
     @Produces(MediaType.APPLICATION_JSON)
     public Response getPermissions(@PathParam("id") String id) {
         this.auth.realm().requireViewAuthorization();
@@ -231,6 +235,7 @@ public class ScopeService {
     }
 
     @GET
+    @NoCache
     @Produces("application/json")
     public Response findAll(@QueryParam("scopeId") String id,
                             @QueryParam("name") String name,
diff --git a/services/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeStatementMapper.java b/services/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeStatementMapper.java
new file mode 100644
index 0000000..4856fb6
--- /dev/null
+++ b/services/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeStatementMapper.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (c) eHealth
+ */
+package org.keycloak.broker.saml.mappers;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
+import org.keycloak.broker.provider.BrokeredIdentityContext;
+import org.keycloak.broker.saml.SAMLEndpoint;
+import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
+import org.keycloak.common.util.CollectionUtil;
+import org.keycloak.dom.saml.v2.assertion.AssertionType;
+import org.keycloak.dom.saml.v2.assertion.AttributeStatementType.ASTChoiceType;
+import org.keycloak.dom.saml.v2.assertion.AttributeType;
+import org.keycloak.models.IdentityProviderMapperModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.provider.ProviderConfigProperty;
+
+/**
+ * @author <a href="mailto:frelibert@yahoo.com">Frederik Libert</a>
+ *
+ */
+public class UserAttributeStatementMapper extends AbstractIdentityProviderMapper {
+
+    private static final String USER_ATTR_LOCALE = "locale";
+
+    private static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID};
+
+    private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>();
+
+    public static final String ATTRIBUTE_NAME_PATTERN = "attribute.name.pattern";
+
+    public static final String USER_ATTRIBUTE_FIRST_NAME = "user.attribute.firstName";
+
+    public static final String USER_ATTRIBUTE_LAST_NAME = "user.attribute.lastName";
+
+    public static final String USER_ATTRIBUTE_EMAIL = "user.attribute.email";
+
+    public static final String USER_ATTRIBUTE_LANGUAGE = "user.attribute.language";
+    
+    private static final String USE_FRIENDLY_NAMES = "use.friendly.names";
+
+    static {
+        ProviderConfigProperty property;
+        property = new ProviderConfigProperty();
+        property.setName(ATTRIBUTE_NAME_PATTERN);
+        property.setLabel("Attribute Name Pattern");
+        property.setHelpText("Pattern of attribute names in assertion that must be mapped. Leave blank to map all attributes.");
+        property.setType(ProviderConfigProperty.STRING_TYPE);
+        CONFIG_PROPERTIES.add(property);
+        property = new ProviderConfigProperty();
+        property.setName(USER_ATTRIBUTE_FIRST_NAME);
+        property.setLabel("User Attribute FirstName");
+        property.setHelpText("Define which saml Attribute must be mapped to the User property firstName.");
+        property.setType(ProviderConfigProperty.STRING_TYPE);
+        CONFIG_PROPERTIES.add(property);
+        property = new ProviderConfigProperty();
+        property.setName(USER_ATTRIBUTE_LAST_NAME);
+        property.setLabel("User Attribute LastName");
+        property.setHelpText("Define which saml Attribute must be mapped to the User property lastName.");
+        property.setType(ProviderConfigProperty.STRING_TYPE);
+        CONFIG_PROPERTIES.add(property);
+        property = new ProviderConfigProperty();
+        property.setName(USER_ATTRIBUTE_EMAIL);
+        property.setLabel("User Attribute Email");
+        property.setHelpText("Define which saml Attribute must be mapped to the User property email.");
+        property.setType(ProviderConfigProperty.STRING_TYPE);
+        CONFIG_PROPERTIES.add(property);
+        property = new ProviderConfigProperty();
+        property.setName(USER_ATTRIBUTE_LANGUAGE);
+        property.setLabel("User Attribute Language");
+        property.setHelpText("Define which saml Attribute must be mapped to the User attribute locale.");
+        property.setType(ProviderConfigProperty.STRING_TYPE);
+        CONFIG_PROPERTIES.add(property);
+        property = new ProviderConfigProperty();
+        property.setName(USE_FRIENDLY_NAMES);
+        property.setLabel("Use Attribute Friendly Name");
+        property.setHelpText("Define which name to give to each mapped user attribute: name or friendlyName.");
+        property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+        CONFIG_PROPERTIES.add(property);
+    }
+
+    public static final String PROVIDER_ID = "saml-user-attributestatement-idp-mapper";
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return CONFIG_PROPERTIES;
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+
+    @Override
+    public String[] getCompatibleProviders() {
+        return COMPATIBLE_PROVIDERS.clone();
+    }
+
+    @Override
+    public String getDisplayCategory() {
+        return "AttributeStatement Importer";
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "AttributeStatement Importer";
+    }
+
+    @Override
+    public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
+        String firstNameAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_FIRST_NAME);
+        String lastNameAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_LAST_NAME);
+        String emailAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_EMAIL);
+        String langAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_LANGUAGE);
+        Boolean useFriendlyNames = Boolean.valueOf(mapperModel.getConfig().get(USE_FRIENDLY_NAMES));
+        List<AttributeType> attributesInContext = findAttributesInContext(context, getAttributePattern(mapperModel));
+        for (AttributeType a : attributesInContext) {
+            String attribute = useFriendlyNames ? a.getFriendlyName() : a.getName();
+            List<String> attributeValuesInContext = a.getAttributeValue().stream().filter(Objects::nonNull).map(Object::toString).collect(Collectors.toList());
+            if (!attributeValuesInContext.isEmpty()) {
+                // set as attribute anyway
+                context.setUserAttribute(attribute, attributeValuesInContext);
+                // set as special field ?
+                if (Objects.equals(attribute, emailAttribute)) {
+                    setIfNotEmpty(context::setEmail, attributeValuesInContext);
+                } else if (Objects.equals(attribute, firstNameAttribute)) {
+                    setIfNotEmpty(context::setFirstName, attributeValuesInContext);
+                } else if (Objects.equals(attribute, lastNameAttribute)) {
+                    setIfNotEmpty(context::setLastName, attributeValuesInContext);
+                } else if (Objects.equals(attribute, langAttribute)) {
+                    context.setUserAttribute(USER_ATTR_LOCALE, attributeValuesInContext);
+                } 
+            }
+        }
+    }
+
+    @Override
+    public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
+        String firstNameAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_FIRST_NAME);
+        String lastNameAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_LAST_NAME);
+        String emailAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_EMAIL);
+        String langAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_LANGUAGE);
+        Boolean useFriendlyNames = Boolean.valueOf(mapperModel.getConfig().get(USE_FRIENDLY_NAMES));
+        List<AttributeType> attributesInContext = findAttributesInContext(context, getAttributePattern(mapperModel));
+
+        Set<String> assertedUserAttributes = new HashSet<String>();
+        for (AttributeType a : attributesInContext) {
+            String attribute = useFriendlyNames ? a.getFriendlyName() : a.getName();
+            List<String> attributeValuesInContext = a.getAttributeValue().stream().filter(Objects::nonNull).map(Object::toString).collect(Collectors.toList());
+            List<String> currentAttributeValues = user.getAttributes().get(attribute);
+            if (attributeValuesInContext == null) {
+                // attribute no longer sent by brokered idp, remove it
+                user.removeAttribute(attribute);
+            } else if (currentAttributeValues == null) {
+                // new attribute sent by brokered idp, add it
+                user.setAttribute(attribute, attributeValuesInContext);
+            } else if (!CollectionUtil.collectionEquals(attributeValuesInContext, currentAttributeValues)) {
+                // attribute sent by brokered idp has different values as before, update it
+                user.setAttribute(attribute, attributeValuesInContext);
+            }
+            if (Objects.equals(attribute, emailAttribute)) {
+                setIfNotEmpty(context::setEmail, attributeValuesInContext);
+            } else if (Objects.equals(attribute, firstNameAttribute)) {
+                setIfNotEmpty(context::setFirstName, attributeValuesInContext);
+            } else if (Objects.equals(attribute, lastNameAttribute)) {
+                setIfNotEmpty(context::setLastName, attributeValuesInContext);
+            } else if (Objects.equals(attribute, langAttribute)) {
+                if(attributeValuesInContext == null) {
+                    user.removeAttribute(USER_ATTR_LOCALE);
+                } else {
+                    user.setAttribute(USER_ATTR_LOCALE, attributeValuesInContext);
+                }
+                assertedUserAttributes.add(USER_ATTR_LOCALE);
+            } 
+            // Mark attribute as handled
+            assertedUserAttributes.add(attribute);
+        }
+        // Remove user attributes that were not referenced in assertion.
+        user.getAttributes().keySet().stream().filter(a -> !assertedUserAttributes.contains(a)).forEach(a -> user.removeAttribute(a));
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Import all saml attributes found in attributestatements in assertion into user properties or attributes.";
+    }
+
+    private Optional<Pattern> getAttributePattern(IdentityProviderMapperModel mapperModel) {
+        String attributePatternConfig = mapperModel.getConfig().get(ATTRIBUTE_NAME_PATTERN);
+        return Optional.ofNullable(attributePatternConfig != null ? Pattern.compile(attributePatternConfig) : null);
+    }
+
+    private List<AttributeType> findAttributesInContext(BrokeredIdentityContext context, Optional<Pattern> attributePattern) {
+        AssertionType assertion = (AssertionType) context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
+
+        return assertion.getAttributeStatements().stream()//
+            .flatMap(statement -> statement.getAttributes().stream())//
+            .filter(item -> !attributePattern.isPresent() || attributePattern.get().matcher(item.getAttribute().getName()).matches())//
+            .map(ASTChoiceType::getAttribute)//
+            .collect(Collectors.toList());
+    }
+
+    private void setIfNotEmpty(Consumer<String> consumer, List<String> values) {
+        if (values != null && !values.isEmpty()) {
+            consumer.accept(values.get(0));
+        }
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
index affaf20..d7eb01c 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -76,6 +76,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
     private Map<String, String> httpResponseHeaders = new HashMap<String, String>();
     private String accessRequestMessage;
     private URI actionUri;
+    private String execution;
 
     private List<FormMessage> messages = null;
     private MessageType messageType = MessageType.ERROR;
@@ -230,6 +231,11 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
                         b = UriBuilder.fromUri(baseUri).path(uriInfo.getPath());
                         break;
                 }
+
+                if (execution != null) {
+                    b.queryParam(Constants.EXECUTION, execution);
+                }
+
                 attributes.put("locale", new LocaleBean(realm, locale, b, messagesBundle));
             }
         }
@@ -366,7 +372,13 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
             attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri));
 
             if (realm.isInternationalizationEnabled()) {
-                UriBuilder b = UriBuilder.fromUri(baseUri).path(uriInfo.getPath());
+                UriBuilder b = UriBuilder.fromUri(baseUri)
+                        .path(uriInfo.getPath());
+
+                if (execution != null) {
+                    b.queryParam(Constants.EXECUTION, execution);
+                }
+
                 attributes.put("locale", new LocaleBean(realm, locale, b, messagesBundle));
             }
         }
@@ -591,6 +603,12 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
     }
 
     @Override
+    public LoginFormsProvider setExecution(String execution) {
+        this.execution = execution;
+        return this;
+    }
+
+    @Override
     public LoginFormsProvider setResponseHeader(String headerName, String headerValue) {
         this.httpResponseHeaders.put(headerName, headerValue);
         return this;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
index 26d012b..3a7e4c0 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
@@ -269,6 +269,12 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
     }
 
     private Response checkOIDCParams() {
+        // If request is not OIDC request, but pure OAuth2 request and response_type is just 'token', then 'nonce' is not mandatory
+        boolean isOIDCRequest = TokenUtil.isOIDCRequest(request.getScope());
+        if (!isOIDCRequest && parsedResponseType.toString().equals(OIDCResponseType.TOKEN)) {
+            return null;
+        }
+
         if (parsedResponseType.isImplicitOrHybridFlow() && request.getNonce() == null) {
             ServicesLogger.LOGGER.missingParameter(OIDCLoginProtocol.NONCE_PARAM);
             event.error(Errors.INVALID_REQUEST);
@@ -354,10 +360,12 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
 
     private void checkRedirectUri() {
         String redirectUriParam = request.getRedirectUriParam();
+        boolean isOIDCRequest = TokenUtil.isOIDCRequest(request.getScope());
 
         event.detail(Details.REDIRECT_URI, redirectUriParam);
 
-        redirectUri = RedirectUtils.verifyRedirectUri(uriInfo, redirectUriParam, realm, client);
+        // redirect_uri parameter is required per OpenID Connect, but optional per OAuth2
+        redirectUri = RedirectUtils.verifyRedirectUri(uriInfo, redirectUriParam, realm, client, isOIDCRequest);
         if (redirectUri == null) {
             event.error(Errors.INVALID_REDIRECT_URI);
             throw new ErrorPageException(session, Messages.INVALID_PARAMETER, OIDCLoginProtocol.REDIRECT_URI_PARAM);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index 6aa13e2..4870415 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -560,13 +560,9 @@ public class TokenEndpoint {
     // https://tools.ietf.org/html/rfc7636#section-4.6
     private String generateS256CodeChallenge(String codeVerifier) throws Exception {
         MessageDigest md = MessageDigest.getInstance("SHA-256");
-        md.update(codeVerifier.getBytes());
-        StringBuilder sb = new StringBuilder();
-        for (byte b : md.digest()) {
-            String hex = String.format("%02x", b);
-            sb.append(hex);
-        }
-        String codeVerifierEncoded = Base64Url.encode(sb.toString().getBytes());
+        md.update(codeVerifier.getBytes("ISO_8859_1"));
+        byte[] digestBytes = md.digest();
+        String codeVerifierEncoded = Base64Url.encode(digestBytes);
         return codeVerifierEncoded;
     }
  
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java
index 60f5493..c61bdd0 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java
@@ -26,6 +26,7 @@ import org.keycloak.services.Urls;
 
 import javax.ws.rs.core.UriInfo;
 import java.net.URI;
+import java.util.Collection;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -38,12 +39,16 @@ public class RedirectUtils {
 
     public static String verifyRealmRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm) {
         Set<String> validRedirects = getValidateRedirectUris(uriInfo, realm);
-        return verifyRedirectUri(uriInfo, null, redirectUri, realm, validRedirects);
+        return verifyRedirectUri(uriInfo, null, redirectUri, realm, validRedirects, true);
     }
 
     public static String verifyRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm, ClientModel client) {
+        return verifyRedirectUri(uriInfo, redirectUri, realm, client, true);
+    }
+
+    public static String verifyRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm, ClientModel client, boolean requireRedirectUri) {
         if (client != null)
-            return verifyRedirectUri(uriInfo, client.getRootUrl(), redirectUri, realm, client.getRedirectUris());
+            return verifyRedirectUri(uriInfo, client.getRootUrl(), redirectUri, realm, client.getRedirectUris(), requireRedirectUri);
         return null;
     }
 
@@ -69,10 +74,16 @@ public class RedirectUtils {
         return redirects;
     }
 
-    private static String verifyRedirectUri(UriInfo uriInfo, String rootUrl, String redirectUri, RealmModel realm, Set<String> validRedirects) {
+    private static String verifyRedirectUri(UriInfo uriInfo, String rootUrl, String redirectUri, RealmModel realm, Set<String> validRedirects, boolean requireRedirectUri) {
         if (redirectUri == null) {
-            logger.debug("No Redirect URI parameter specified");
-            return null;
+            if (!requireRedirectUri) {
+                redirectUri = getSingleValidRedirectUri(validRedirects);
+            }
+
+            if (redirectUri == null) {
+                logger.debug("No Redirect URI parameter specified");
+                return null;
+            }
         } else if (validRedirects.isEmpty()) {
             logger.debug("No Redirect URIs supplied");
             redirectUri = null;
@@ -149,4 +160,14 @@ public class RedirectUtils {
         return false;
     }
 
+    private static String getSingleValidRedirectUri(Collection<String> validRedirects) {
+        if (validRedirects.size() != 1) return null;
+        String validRedirect = validRedirects.iterator().next();
+        int idx = validRedirect.indexOf("/*");
+        if (idx > -1) {
+            validRedirect = validRedirect.substring(0, idx);
+        }
+        return validRedirect;
+    }
+
 }
diff --git a/services/src/main/java/org/keycloak/scripting/AbstractEvaluatableScriptAdapter.java b/services/src/main/java/org/keycloak/scripting/AbstractEvaluatableScriptAdapter.java
new file mode 100644
index 0000000..534883a
--- /dev/null
+++ b/services/src/main/java/org/keycloak/scripting/AbstractEvaluatableScriptAdapter.java
@@ -0,0 +1,76 @@
+package org.keycloak.scripting;
+
+import javax.script.Bindings;
+import javax.script.ScriptContext;
+import javax.script.ScriptEngine;
+import javax.script.ScriptException;
+
+import org.keycloak.models.ScriptModel;
+
+/**
+ * Abstract class for wrapping a {@link ScriptModel} to make it evaluatable.
+ *
+ * @author <a href="mailto:jay@anslow.me.uk">Jay Anslow</a>
+ */
+abstract class AbstractEvaluatableScriptAdapter implements EvaluatableScriptAdapter {
+    /**
+     * Holds the {@link ScriptModel}.
+     */
+    private final ScriptModel scriptModel;
+
+    AbstractEvaluatableScriptAdapter(final ScriptModel scriptModel) {
+        if (scriptModel == null) {
+            throw new IllegalArgumentException("scriptModel must not be null");
+        }
+        this.scriptModel = scriptModel;
+    }
+
+    @Override
+    public Object eval(final ScriptBindingsConfigurer bindingsConfigurer) throws ScriptExecutionException {
+        return evalUnchecked(createBindings(bindingsConfigurer));
+    }
+
+    @Override
+    public ScriptModel getScriptModel() {
+        return scriptModel;
+    }
+
+    /**
+     * Note, calling this method modifies the underlying {@link ScriptEngine},
+     * preventing concurrent use of the ScriptEngine (Nashorn's {@link ScriptEngine} and
+     * {@link javax.script.CompiledScript} is thread-safe, but {@link Bindings} isn't).
+     */
+    InvocableScriptAdapter prepareInvokableScript(final ScriptBindingsConfigurer bindingsConfigurer) {
+        final Bindings bindings = createBindings(bindingsConfigurer);
+        evalUnchecked(bindings);
+        final ScriptEngine engine = getEngine();
+        engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
+        return new InvocableScriptAdapter(scriptModel, engine);
+    }
+
+    protected String getCode() {
+        return scriptModel.getCode();
+    }
+
+    protected abstract ScriptEngine getEngine();
+
+    protected abstract Object eval(Bindings bindings) throws ScriptException;
+
+    private Object evalUnchecked(final Bindings bindings) {
+        try {
+            return eval(bindings);
+        }
+        catch (ScriptException e) {
+            throw new ScriptExecutionException(scriptModel, e);
+        }
+    }
+
+    private Bindings createBindings(final ScriptBindingsConfigurer bindingsConfigurer) {
+        if (bindingsConfigurer == null) {
+            throw new IllegalArgumentException("bindingsConfigurer must not be null");
+        }
+        final Bindings bindings = getEngine().createBindings();
+        bindingsConfigurer.configureBindings(bindings);
+        return bindings;
+    }
+}
diff --git a/services/src/main/java/org/keycloak/scripting/CompiledEvaluatableScriptAdapter.java b/services/src/main/java/org/keycloak/scripting/CompiledEvaluatableScriptAdapter.java
new file mode 100644
index 0000000..7359dc9
--- /dev/null
+++ b/services/src/main/java/org/keycloak/scripting/CompiledEvaluatableScriptAdapter.java
@@ -0,0 +1,40 @@
+package org.keycloak.scripting;
+
+import javax.script.Bindings;
+import javax.script.CompiledScript;
+import javax.script.ScriptEngine;
+import javax.script.ScriptException;
+
+import org.keycloak.models.ScriptModel;
+
+/**
+ * Wraps a compiled {@link ScriptModel} so it can be evaluated.
+ *
+ * @author <a href="mailto:jay@anslow.me.uk">Jay Anslow</a>
+ */
+class CompiledEvaluatableScriptAdapter extends AbstractEvaluatableScriptAdapter {
+    /**
+     * Holds the {@link CompiledScript} for the {@link ScriptModel}.
+     */
+    private final CompiledScript compiledScript;
+
+    CompiledEvaluatableScriptAdapter(final ScriptModel scriptModel, final CompiledScript compiledScript) {
+        super(scriptModel);
+
+        if (compiledScript == null) {
+            throw new IllegalArgumentException("compiledScript must not be null");
+        }
+
+        this.compiledScript = compiledScript;
+    }
+
+    @Override
+    protected ScriptEngine getEngine() {
+        return compiledScript.getEngine();
+    }
+
+    @Override
+    protected Object eval(final Bindings bindings) throws ScriptException {
+        return compiledScript.eval(bindings);
+    }
+}
diff --git a/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java b/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java
index 601da8e..d781460 100644
--- a/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java
+++ b/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java
@@ -16,12 +16,14 @@
  */
 package org.keycloak.scripting;
 
-import org.keycloak.models.ScriptModel;
-
 import javax.script.Bindings;
-import javax.script.ScriptContext;
+import javax.script.Compilable;
+import javax.script.CompiledScript;
 import javax.script.ScriptEngine;
 import javax.script.ScriptEngineManager;
+import javax.script.ScriptException;
+
+import org.keycloak.models.ScriptModel;
 
 /**
  * A {@link ScriptingProvider} that uses a {@link ScriptEngineManager} to evaluate scripts with a {@link ScriptEngine}.
@@ -32,8 +34,7 @@ public class DefaultScriptingProvider implements ScriptingProvider {
 
     private final ScriptEngineManager scriptEngineManager;
 
-    public DefaultScriptingProvider(ScriptEngineManager scriptEngineManager) {
-
+    DefaultScriptingProvider(ScriptEngineManager scriptEngineManager) {
         if (scriptEngineManager == null) {
             throw new IllegalStateException("scriptEngineManager must not be null!");
         }
@@ -44,13 +45,22 @@ public class DefaultScriptingProvider implements ScriptingProvider {
     /**
      * Wraps the provided {@link ScriptModel} in a {@link javax.script.Invocable} instance with bindings configured through the {@link ScriptBindingsConfigurer}.
      *
-     * @param scriptModel  must not be {@literal null}
+     * @param scriptModel        must not be {@literal null}
      * @param bindingsConfigurer must not be {@literal null}
-     * @return
      */
     @Override
     public InvocableScriptAdapter prepareInvocableScript(ScriptModel scriptModel, ScriptBindingsConfigurer bindingsConfigurer) {
+        final AbstractEvaluatableScriptAdapter evaluatable = prepareEvaluatableScript(scriptModel);
+        return evaluatable.prepareInvokableScript(bindingsConfigurer);
+    }
 
+    /**
+     * Wraps the provided {@link ScriptModel} in a {@link javax.script.Invocable} instance with bindings configured through the {@link ScriptBindingsConfigurer}.
+     *
+     * @param scriptModel must not be {@literal null}
+     */
+    @Override
+    public AbstractEvaluatableScriptAdapter prepareEvaluatableScript(ScriptModel scriptModel) {
         if (scriptModel == null) {
             throw new IllegalArgumentException("script must not be null");
         }
@@ -59,13 +69,18 @@ public class DefaultScriptingProvider implements ScriptingProvider {
             throw new IllegalArgumentException("script must not be null or empty");
         }
 
-        if (bindingsConfigurer == null) {
-            throw new IllegalArgumentException("bindingsConfigurer must not be null");
-        }
+        ScriptEngine engine = createPreparedScriptEngine(scriptModel);
 
-        ScriptEngine engine = createPreparedScriptEngine(scriptModel, bindingsConfigurer);
-
-        return new InvocableScriptAdapter(scriptModel, engine);
+        if (engine instanceof Compilable) {
+            try {
+                final CompiledScript compiledScript = ((Compilable) engine).compile(scriptModel.getCode());
+                return new CompiledEvaluatableScriptAdapter(scriptModel, compiledScript);
+            }
+            catch (ScriptException e) {
+                throw new ScriptExecutionException(scriptModel, e);
+            }
+        }
+        return new UncompiledEvaluatableScriptAdapter(scriptModel, engine);
     }
 
     //TODO allow scripts to be maintained independently of other components, e.g. with dedicated persistence
@@ -74,38 +89,27 @@ public class DefaultScriptingProvider implements ScriptingProvider {
 
     @Override
     public ScriptModel createScript(String realmId, String mimeType, String scriptName, String scriptCode, String scriptDescription) {
+        return new Script(null /* scriptId */, realmId, scriptName, mimeType, scriptCode, scriptDescription);
+    }
 
-        ScriptModel script = new Script(null /* scriptId */, realmId, scriptName, mimeType, scriptCode, scriptDescription);
-        return script;
+    @Override
+    public void close() {
+        //NOOP
     }
 
     /**
      * Looks-up a {@link ScriptEngine} with prepared {@link Bindings} for the given {@link ScriptModel Script}.
-     *
-     * @param script
-     * @param bindingsConfigurer
-     * @return
      */
-    private ScriptEngine createPreparedScriptEngine(ScriptModel script, ScriptBindingsConfigurer bindingsConfigurer) {
-
+    private ScriptEngine createPreparedScriptEngine(ScriptModel script) {
         ScriptEngine scriptEngine = lookupScriptEngineFor(script);
 
         if (scriptEngine == null) {
             throw new IllegalStateException("Could not find ScriptEngine for script: " + script);
         }
 
-        configureBindings(bindingsConfigurer, scriptEngine);
-
         return scriptEngine;
     }
 
-    private void configureBindings(ScriptBindingsConfigurer bindingsConfigurer, ScriptEngine engine) {
-
-        Bindings bindings = engine.createBindings();
-        bindingsConfigurer.configureBindings(bindings);
-        engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
-    }
-
     /**
      * Looks-up a {@link ScriptEngine} based on the MIME-type provided by the given {@link Script}.
      */
@@ -114,13 +118,9 @@ public class DefaultScriptingProvider implements ScriptingProvider {
         try {
             Thread.currentThread().setContextClassLoader(DefaultScriptingProvider.class.getClassLoader());
             return scriptEngineManager.getEngineByMimeType(script.getMimeType());
-        } finally {
+        }
+        finally {
             Thread.currentThread().setContextClassLoader(cl);
         }
     }
-
-    @Override
-    public void close() {
-        //NOOP
-    }
 }
diff --git a/services/src/main/java/org/keycloak/scripting/UncompiledEvaluatableScriptAdapter.java b/services/src/main/java/org/keycloak/scripting/UncompiledEvaluatableScriptAdapter.java
new file mode 100644
index 0000000..8464fdf
--- /dev/null
+++ b/services/src/main/java/org/keycloak/scripting/UncompiledEvaluatableScriptAdapter.java
@@ -0,0 +1,39 @@
+package org.keycloak.scripting;
+
+import javax.script.Bindings;
+import javax.script.ScriptEngine;
+import javax.script.ScriptException;
+
+import org.keycloak.models.ScriptModel;
+
+/**
+ * Wraps an uncompiled {@link ScriptModel} so it can be evaluated.
+ *
+ * @author <a href="mailto:jay@anslow.me.uk">Jay Anslow</a>
+ */
+class UncompiledEvaluatableScriptAdapter extends AbstractEvaluatableScriptAdapter {
+    /**
+     * Holds the {@link ScriptEngine} instance.
+     */
+    private final ScriptEngine scriptEngine;
+
+    UncompiledEvaluatableScriptAdapter(final ScriptModel scriptModel, final ScriptEngine scriptEngine) {
+        super(scriptModel);
+        if (scriptEngine == null) {
+            throw new IllegalArgumentException("scriptEngine must not be null");
+        }
+
+        this.scriptEngine = scriptEngine;
+    }
+
+    @Override
+    protected ScriptEngine getEngine() {
+        return scriptEngine;
+    }
+
+    @Override
+    protected Object eval(final Bindings bindings) throws ScriptException {
+        return getEngine().eval(getCode(), bindings);
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 07bd1f6..6c91759 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -644,12 +644,15 @@ public class AuthenticationManager {
 
             // Skip grant screen if everything was already approved by this user
             if (realmRoles.size() > 0 || resourceRoles.size() > 0 || protocolMappers.size() > 0) {
+                String execution = AuthenticatedClientSessionModel.Action.OAUTH_GRANT.name();
+
                 accessCode.
 
                         setAction(AuthenticatedClientSessionModel.Action.REQUIRED_ACTIONS.name());
-                authSession.setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, AuthenticatedClientSessionModel.Action.OAUTH_GRANT.name());
+                authSession.setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution);
 
                 return session.getProvider(LoginFormsProvider.class)
+                        .setExecution(execution)
                         .setClientSessionCode(accessCode.getCode())
                         .setAccessRequest(realmRoles, resourceRoles, protocolMappers)
                         .createOAuthGrant();
diff --git a/services/src/main/java/org/keycloak/services/ServicesLogger.java b/services/src/main/java/org/keycloak/services/ServicesLogger.java
index bd239a6..2d6c473 100644
--- a/services/src/main/java/org/keycloak/services/ServicesLogger.java
+++ b/services/src/main/java/org/keycloak/services/ServicesLogger.java
@@ -406,7 +406,7 @@ public interface ServicesLogger extends BasicLogger {
     void failedToCloseProviderSession(@Cause Throwable t);
 
     @LogMessage(level = WARN)
-    @Message(id=91, value="Request is missing scope 'openid' so it's not treated as OIDC, but just pure OAuth2 request. This can have impact in future versions (eg. removed IDToken from the Token Response)")
+    @Message(id=91, value="Request is missing scope 'openid' so it's not treated as OIDC, but just pure OAuth2 request.")
     @Once
     void oidcScopeMissing();
 
diff --git a/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java b/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java
index 3726b99..489f73f 100644
--- a/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java
+++ b/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java
@@ -58,6 +58,7 @@ public class AuthenticationFlowURLHelper {
 
         return session.getProvider(LoginFormsProvider.class)
                 .setActionUri(lastStepUrl)
+                .setExecution(getExecutionId(authSession))
                 .createLoginExpiredPage();
     }
 
@@ -76,7 +77,7 @@ public class AuthenticationFlowURLHelper {
 
 
     public URI getLastExecutionUrl(AuthenticationSessionModel authSession) {
-        String executionId = authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
+        String executionId = getExecutionId(authSession);
         String latestFlowPath = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
 
         if (latestFlowPath == null) {
@@ -90,4 +91,8 @@ public class AuthenticationFlowURLHelper {
         return getLastExecutionUrl(latestFlowPath, executionId, authSession.getClient().getClientId());
     }
 
+    private String getExecutionId(AuthenticationSessionModel authSession) {
+        return authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
+    }
+
 }
diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/pom.xml b/testsuite/integration-arquillian/servers/cache-server/jboss/pom.xml
index 8c7f830..9c2d1f9 100644
--- a/testsuite/integration-arquillian/servers/cache-server/jboss/pom.xml
+++ b/testsuite/integration-arquillian/servers/cache-server/jboss/pom.xml
@@ -100,7 +100,7 @@
                         <artifactId>xml-maven-plugin</artifactId>
                         <executions>
                             <execution>
-                                <id>configure-adapter-debug-log</id>
+                                <id>configure-keycloak-caches</id>
                                 <phase>process-test-resources</phase>
                                 <goals>
                                     <goal>transform</goal>
@@ -111,8 +111,9 @@
                                             <dir>${cache.server.jboss.home}/standalone/configuration</dir>
                                             <includes>
                                                 <include>standalone.xml</include>
+                                                <include>clustered.xml</include>
                                             </includes>
-                                            <stylesheet>${common.resources}/add-keycloak-remote-store.xsl</stylesheet>
+                                            <stylesheet>${common.resources}/add-keycloak-caches.xsl</stylesheet>
                                             <outputDir>${cache.server.jboss.home}/standalone/configuration</outputDir>
                                         </transformationSet>
                                     </transformationSets>
@@ -173,6 +174,23 @@
                                     <overwrite>true</overwrite>
                                 </configuration>
                             </execution>
+                            <execution>
+                                <id>copy-cache-server-configuration-for-dc-2</id>
+                                <phase>process-resources</phase>
+                                <goals>
+                                    <goal>copy-resources</goal>
+                                </goals>
+                                <configuration>
+                                    <outputDirectory>${cache.server.jboss.home}/standalone-dc-2/deployments</outputDirectory>
+                                    <includeEmptyDirs>true</includeEmptyDirs>
+                                    <resources>
+                                        <resource>
+                                            <directory>${cache.server.jboss.home}/standalone/deployments</directory>
+                                        </resource>
+                                    </resources>
+                                    <overwrite>true</overwrite>
+                                </configuration>
+                            </execution>
                         </executions>
                     </plugin>
 
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanCacheStatistics.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanCacheStatistics.java
new file mode 100644
index 0000000..2dd7bbc
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanCacheStatistics.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.arquillian.annotation;
+
+import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
+import org.keycloak.testsuite.arquillian.InfinispanStatistics;
+import org.keycloak.testsuite.arquillian.InfinispanStatistics.Constants;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation for a field / method parameter annotating {@link InfinispanStatistics} object that would be used
+ * to access statistics via JMX. By default, the access to "work" cache at remote infinispan / JDG server is requested
+ * yet the same annotation is used for other caches as well.
+ *
+ * @author hmlnarik
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.PARAMETER})
+public @interface JmxInfinispanCacheStatistics {
+
+    /** JMX domain. Should be set to default (@{code ""}) if the node to get the statistics from should be obtained from {@link #dcIndex()} and {@link #dcNodeIndex()}. */
+    String domain() default "";
+
+    // JMX address properties
+    String type() default Constants.TYPE_CACHE;
+    String cacheName() default "work";
+    String cacheMode() default "*";
+    String cacheManagerName() default "*";
+    String component() default Constants.COMPONENT_STATISTICS;
+
+    // Host address - either given by arrangement of DC ...
+
+    /** Index of the data center, starting from 0 */
+    int dcIndex() default -1;
+    /** Index of the node within data center, starting from 0. Nodes are ordered by arquillian qualifier as per {@link AuthServerTestEnricher} */
+    int dcNodeIndex() default -1;
+
+    // ... or by specific host/port
+
+    /** Port for management */
+    int managementPort() default -1;
+    /** Name of system property to obtain management port from */
+    String managementPortProperty() default "";
+    /** Host name to connect to */
+    String host() default "";
+    /** Name of system property to obtain host name from */
+    String hostProperty() default "";
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanChannelStatistics.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanChannelStatistics.java
new file mode 100644
index 0000000..41e9f20
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/JmxInfinispanChannelStatistics.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.arquillian.annotation;
+
+import org.keycloak.testsuite.arquillian.InfinispanStatistics.Constants;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ *
+ * @author hmlnarik
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.PARAMETER})
+public @interface JmxInfinispanChannelStatistics {
+
+    /** JMX domain. Should be set to default (@{code ""}) if the node to get the statistics from should be obtained from {@link #dcIndex()} and {@link #dcNodeIndex()}. */
+    String domain() default "";
+
+    // JMX address properties
+    String type() default Constants.TYPE_CHANNEL;
+    String cluster() default "*";
+
+    // Host address - either given by arrangement of DC ...
+
+    /** Index of the data center, starting from 0 */
+    int dcIndex() default -1;
+    /** Index of the node within data center, starting from 0. Nodes are ordered by arquillian qualifier as per {@link AuthServerTestEnricher} */
+    int dcNodeIndex() default -1;
+
+    /** Port for management */
+    int managementPort() default -1;
+    /** Name of system property to obtain management port from */
+    String managementPortProperty() default "";
+    /** Host name to connect to */
+    String host() default "";
+    /** Name of system property to obtain host name from */
+    String hostProperty() default "";
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java
index 94293dd..97347d9 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java
@@ -142,6 +142,7 @@ public class AuthServerTestEnricher {
 
             containers.stream()
               .filter(c -> c.getQualifier().startsWith(AUTH_SERVER_CONTAINER + "-cross-dc-"))
+              .sorted((a, b) -> a.getQualifier().compareTo(b.getQualifier()))
               .forEach(c -> {
                 String portOffsetString = c.getArquillianContainer().getContainerConfiguration().getContainerProperties().getOrDefault("bindHttpPortOffset", "0");
                 String dcString = c.getArquillianContainer().getContainerConfiguration().getContainerProperties().getOrDefault("dataCenter", "0");
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CacheStatisticsControllerEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CacheStatisticsControllerEnricher.java
new file mode 100644
index 0000000..4091ca4
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/CacheStatisticsControllerEnricher.java
@@ -0,0 +1,356 @@
+package org.keycloak.testsuite.arquillian;
+
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.testsuite.Retry;
+import java.util.Map;
+import org.jboss.arquillian.core.api.Instance;
+import org.jboss.arquillian.core.api.annotation.Inject;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import javax.management.MBeanServerConnection;
+import javax.management.ObjectName;
+import javax.management.remote.JMXConnector;
+import javax.management.remote.JMXServiceURL;
+import org.jboss.arquillian.container.spi.Container;
+import org.jboss.arquillian.container.spi.ContainerRegistry;
+import org.jboss.arquillian.test.spi.TestEnricher;
+import java.io.IOException;
+import java.lang.reflect.Parameter;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+import javax.management.Attribute;
+import javax.management.AttributeNotFoundException;
+import javax.management.InstanceNotFoundException;
+import javax.management.IntrospectionException;
+import javax.management.MBeanAttributeInfo;
+import javax.management.MBeanException;
+import javax.management.MBeanInfo;
+import javax.management.MalformedObjectNameException;
+import javax.management.ReflectionException;
+import org.keycloak.testsuite.arquillian.annotation.JmxInfinispanCacheStatistics;
+import java.util.Set;
+import org.keycloak.testsuite.arquillian.annotation.JmxInfinispanChannelStatistics;
+import org.keycloak.testsuite.arquillian.jmx.JmxConnectorRegistry;
+import org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow;
+import java.io.NotSerializableException;
+import java.lang.management.ManagementFactory;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.jboss.arquillian.core.spi.Validate;
+import org.jboss.logging.Logger;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class CacheStatisticsControllerEnricher implements TestEnricher {
+
+    private static final Logger LOG = Logger.getLogger(CacheStatisticsControllerEnricher.class);
+
+    @Inject
+    private Instance<ContainerRegistry> registry;
+
+    @Inject
+    private Instance<JmxConnectorRegistry> jmxConnectorRegistry;
+
+    @Inject
+    private Instance<SuiteContext> suiteContext;
+
+    @Override
+    public void enrich(Object testCase) {
+        Validate.notNull(registry.get(), "registry should not be null");
+        Validate.notNull(jmxConnectorRegistry.get(), "jmxConnectorRegistry should not be null");
+        Validate.notNull(suiteContext.get(), "suiteContext should not be null");
+
+        for (Field field : FieldUtils.getAllFields(testCase.getClass())) {
+            JmxInfinispanCacheStatistics annotation = field.getAnnotation(JmxInfinispanCacheStatistics.class);
+
+            if (annotation == null) {
+                continue;
+            }
+
+            try {
+                FieldUtils.writeField(field, testCase, getInfinispanCacheStatistics(annotation), true);
+            } catch (IOException | IllegalAccessException | MalformedObjectNameException e) {
+                throw new RuntimeException("Could not set value on field " + field);
+            }
+        }
+    }
+
+    private InfinispanStatistics getInfinispanCacheStatistics(JmxInfinispanCacheStatistics annotation) throws MalformedObjectNameException, IOException, MalformedURLException {
+        MBeanServerConnection mbsc = getJmxServerConnection(annotation);
+
+        ObjectName mbeanName = new ObjectName(String.format(
+          "%s:type=%s,name=\"%s(%s)\",manager=\"%s\",component=%s",
+          annotation.domain().isEmpty() ? getDefaultDomain(annotation.dcIndex(), annotation.dcNodeIndex()) : InfinispanConnectionProvider.JMX_DOMAIN,
+          annotation.type(),
+          annotation.cacheName(),
+          annotation.cacheMode(),
+          annotation.cacheManagerName(),
+          annotation.component()
+        ));
+
+        InfinispanStatistics value = new InfinispanCacheStatisticsImpl(mbsc, mbeanName);
+
+        if (annotation.domain().isEmpty()) {
+            try {
+                Retry.execute(() -> value.reset(), 2, 150);
+            } catch (RuntimeException ex) {
+                if (annotation.dcIndex() != -1 && annotation.dcNodeIndex() != -1
+                   && suiteContext.get().getAuthServerBackendsInfo(annotation.dcIndex()).get(annotation.dcNodeIndex()).isStarted()) {
+                    LOG.warn("Could not reset statistics for " + mbeanName);
+                }
+            }
+        }
+
+        return value;
+    }
+
+    private InfinispanStatistics getJGroupsChannelStatistics(JmxInfinispanChannelStatistics annotation) throws MalformedObjectNameException, IOException, MalformedURLException {
+        MBeanServerConnection mbsc = getJmxServerConnection(annotation);
+
+        ObjectName mbeanName = new ObjectName(String.format(
+          "%s:type=%s,cluster=\"%s\"",
+          annotation.domain().isEmpty() ? getDefaultDomain(annotation.dcIndex(), annotation.dcNodeIndex()) : InfinispanConnectionProvider.JMX_DOMAIN,
+          annotation.type(),
+          annotation.cluster()
+        ));
+
+        InfinispanStatistics value = new InfinispanChannelStatisticsImpl(mbsc, mbeanName);
+
+        if (annotation.domain().isEmpty()) {
+            try {
+                Retry.execute(() -> value.reset(), 2, 150);
+            } catch (RuntimeException ex) {
+                if (annotation.dcIndex() != -1 && annotation.dcNodeIndex() != -1
+                   && suiteContext.get().getAuthServerBackendsInfo(annotation.dcIndex()).get(annotation.dcNodeIndex()).isStarted()) {
+                    LOG.warn("Could not reset statistics for " + mbeanName);
+                }
+            }
+        }
+
+        return value;
+    }
+
+    @Override
+    public Object[] resolve(Method method) {
+        Object[] values = new Object[method.getParameterCount()];
+
+        for (int i = 0; i < method.getParameterCount(); i ++) {
+            Parameter param = method.getParameters()[i];
+
+            JmxInfinispanCacheStatistics annotation = param.getAnnotation(JmxInfinispanCacheStatistics.class);
+            if (annotation != null) try {
+                values[i] = getInfinispanCacheStatistics(annotation);
+            } catch (IOException | MalformedObjectNameException e) {
+                throw new RuntimeException("Could not set value on field " + param);
+            }
+
+            JmxInfinispanChannelStatistics channelAnnotation = param.getAnnotation(JmxInfinispanChannelStatistics.class);
+            if (channelAnnotation != null) try {
+                values[i] = getJGroupsChannelStatistics(channelAnnotation);
+            } catch (IOException | MalformedObjectNameException e) {
+                throw new RuntimeException("Could not set value on field " + param);
+            }
+        }
+
+        return values;
+    }
+
+    private String getDefaultDomain(int dcIndex, int dcNodeIndex) {
+        if (dcIndex != -1 && dcNodeIndex != -1) {
+            return InfinispanConnectionProvider.JMX_DOMAIN + "-" + suiteContext.get().getAuthServerBackendsInfo(dcIndex).get(dcNodeIndex).getQualifier();
+        }
+        return InfinispanConnectionProvider.JMX_DOMAIN;
+    }
+
+    private MBeanServerConnection getJmxServerConnection(JmxInfinispanCacheStatistics annotation) throws MalformedURLException, IOException {
+        final String host;
+        final int port;
+
+        if (annotation.dcIndex() != -1 && annotation.dcNodeIndex() != -1) {
+            ContainerInfo node = suiteContext.get().getAuthServerBackendsInfo(annotation.dcIndex()).get(annotation.dcNodeIndex());
+            Container container = node.getArquillianContainer();
+            if (container.getDeployableContainer() instanceof KeycloakOnUndertow) {
+                return ManagementFactory.getPlatformMBeanServer();
+            }
+            host = "localhost";
+            port = container.getContainerConfiguration().getContainerProperties().containsKey("managementPort")
+              ? Integer.valueOf(container.getContainerConfiguration().getContainerProperties().get("managementPort"))
+              : 9990;
+        } else {
+            host = annotation.host().isEmpty()
+              ? System.getProperty((annotation.hostProperty().isEmpty()
+                ? "keycloak.connectionsInfinispan.remoteStoreServer"
+                : annotation.hostProperty()))
+              : annotation.host();
+
+            port = annotation.managementPort() == -1
+              ? Integer.valueOf(System.getProperty((annotation.managementPortProperty().isEmpty()
+                ? "cache.server.management.port"
+                : annotation.managementPortProperty())))
+              : annotation.managementPort();
+        }
+
+        JMXServiceURL url = new JMXServiceURL("service:jmx:remote+http://" + host + ":" + port);
+        JMXConnector jmxc = jmxConnectorRegistry.get().getConnection(url);
+
+        return jmxc.getMBeanServerConnection();
+    }
+
+    private MBeanServerConnection getJmxServerConnection(JmxInfinispanChannelStatistics annotation) throws MalformedURLException, IOException {
+        final String host;
+        final int port;
+
+        if (annotation.dcIndex() != -1 && annotation.dcNodeIndex() != -1) {
+            ContainerInfo node = suiteContext.get().getAuthServerBackendsInfo(annotation.dcIndex()).get(annotation.dcNodeIndex());
+            Container container = node.getArquillianContainer();
+            if (container.getDeployableContainer() instanceof KeycloakOnUndertow) {
+                return ManagementFactory.getPlatformMBeanServer();
+            }
+            host = "localhost";
+            port = container.getContainerConfiguration().getContainerProperties().containsKey("managementPort")
+              ? Integer.valueOf(container.getContainerConfiguration().getContainerProperties().get("managementPort"))
+              : 9990;
+        } else {
+            host = annotation.host().isEmpty()
+              ? System.getProperty((annotation.hostProperty().isEmpty()
+                ? "keycloak.connectionsInfinispan.remoteStoreServer"
+                : annotation.hostProperty()))
+              : annotation.host();
+
+            port = annotation.managementPort() == -1
+              ? Integer.valueOf(System.getProperty((annotation.managementPortProperty().isEmpty()
+                ? "cache.server.management.port"
+                : annotation.managementPortProperty())))
+              : annotation.managementPort();
+        }
+
+        JMXServiceURL url = new JMXServiceURL("service:jmx:remote+http://" + host + ":" + port);
+        JMXConnector jmxc = jmxConnectorRegistry.get().getConnection(url);
+
+        return jmxc.getMBeanServerConnection();
+    }
+
+    private static abstract class CacheStatisticsImpl implements InfinispanStatistics {
+
+        protected final MBeanServerConnection mbsc;
+        private final ObjectName mbeanNameTemplate;
+        private ObjectName mbeanName;
+
+        public CacheStatisticsImpl(MBeanServerConnection mbsc, ObjectName mbeanNameTemplate) {
+            this.mbsc = mbsc;
+            this.mbeanNameTemplate = mbeanNameTemplate;
+        }
+
+        @Override
+        public boolean exists() {
+            try {
+                getMbeanName();
+                return true;
+            } catch (Exception ex) {
+                return false;
+            }
+        }
+
+        @Override
+        public Map<String, Object> getStatistics() {
+            try {
+                MBeanInfo mBeanInfo = mbsc.getMBeanInfo(getMbeanName());
+                String[] statAttrs = Arrays.asList(mBeanInfo.getAttributes()).stream()
+                  .filter(MBeanAttributeInfo::isReadable)
+                  .map(MBeanAttributeInfo::getName)
+                  .collect(Collectors.toList())
+                  .toArray(new String[] {});
+                return mbsc.getAttributes(getMbeanName(), statAttrs)
+                  .asList()
+                  .stream()
+                  .collect(Collectors.toMap(Attribute::getName, Attribute::getValue));
+            } catch (IOException | InstanceNotFoundException | ReflectionException | IntrospectionException ex) {
+                throw new RuntimeException(ex);
+            }
+        }
+
+        protected ObjectName getMbeanName() throws IOException, RuntimeException {
+            if (this.mbeanName == null) {
+                Set<ObjectName> queryNames = mbsc.queryNames(mbeanNameTemplate, null);
+                if (queryNames.isEmpty()) {
+                    throw new RuntimeException("No MBean of template " + mbeanNameTemplate + " found at JMX server");
+                }
+                this.mbeanName = queryNames.iterator().next();
+            }
+
+            return this.mbeanName;
+        }
+
+        @Override
+        public Comparable getSingleStatistics(String statisticsName) {
+            try {
+                return (Comparable) mbsc.getAttribute(getMbeanName(), statisticsName);
+            } catch (IOException | InstanceNotFoundException | MBeanException | ReflectionException | AttributeNotFoundException ex) {
+                throw new RuntimeException(ex);
+            }
+        }
+
+        @Override
+        public void waitToBecomeAvailable(int time, TimeUnit unit) {
+            long timeInMillis = TimeUnit.MILLISECONDS.convert(time, unit);
+            Retry.execute(() -> {
+                try {
+                    getMbeanName();
+                    if (! isAvailable()) throw new RuntimeException("Not available");
+                } catch (Exception ex) {
+                    throw new RuntimeException("Timed out while waiting for " + mbeanNameTemplate + " to become available", ex);
+                }
+            }, 1 + (int) timeInMillis / 100, 100);
+        }
+
+        protected abstract boolean isAvailable();
+    }
+
+    private static class InfinispanCacheStatisticsImpl extends CacheStatisticsImpl {
+
+        public InfinispanCacheStatisticsImpl(MBeanServerConnection mbsc, ObjectName mbeanName) {
+            super(mbsc, mbeanName);
+        }
+
+        @Override
+        public void reset() {
+            try {
+                mbsc.invoke(getMbeanName(), "resetStatistics", new Object[] {}, new String[] {});
+            } catch (IOException | InstanceNotFoundException | MBeanException | ReflectionException ex) {
+                throw new RuntimeException(ex);
+            }
+        }
+
+        @Override
+        protected boolean isAvailable() {
+            return getSingleStatistics(Constants.STAT_CACHE_ELAPSED_TIME) != null;
+        }
+    }
+
+    private static class InfinispanChannelStatisticsImpl extends CacheStatisticsImpl {
+
+        public InfinispanChannelStatisticsImpl(MBeanServerConnection mbsc, ObjectName mbeanName) {
+            super(mbsc, mbeanName);
+        }
+
+        @Override
+        public void reset() {
+            try {
+                mbsc.invoke(getMbeanName(), "resetStats", new Object[] {}, new String[] {});
+            } catch (NotSerializableException ex) {
+                // Ignore return value not serializable, the invocation has already done its job
+            } catch (IOException | InstanceNotFoundException | MBeanException | ReflectionException ex) {
+                throw new RuntimeException(ex);
+            }
+        }
+
+        @Override
+        protected boolean isAvailable() {
+            return Objects.equals(getSingleStatistics(Constants.STAT_CHANNEL_CONNECTED), Boolean.TRUE);
+       }
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/RegistryCreator.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/RegistryCreator.java
index a2b6ea7..41278fc 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/RegistryCreator.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/RegistryCreator.java
@@ -35,6 +35,7 @@ import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
+import org.mvel2.MVEL;
 import static org.keycloak.testsuite.arquillian.containers.SecurityActions.isClassPresent;
 import static org.keycloak.testsuite.arquillian.containers.SecurityActions.loadClass;
 
@@ -97,10 +98,14 @@ public class RegistryCreator {
 
     private static final String ENABLED = "enabled";
 
-    private boolean isEnabled(ContainerDef containerDef) {
+    private static boolean isEnabled(ContainerDef containerDef) {
         Map<String, String> props = containerDef.getContainerProperties();
-        return !props.containsKey(ENABLED)
-                || (props.containsKey(ENABLED) && props.get(ENABLED).equals("true"));
+        try {
+            return !props.containsKey(ENABLED)
+                    || (props.containsKey(ENABLED) && ! props.get(ENABLED).isEmpty() && MVEL.evalToBoolean(props.get(ENABLED), (Object) null));
+        } catch (Exception ex) {
+            return false;
+        }
     }
 
     @SuppressWarnings("rawtypes")
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/InfinispanStatistics.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/InfinispanStatistics.java
new file mode 100644
index 0000000..b315937
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/InfinispanStatistics.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.arquillian;
+
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public interface InfinispanStatistics {
+
+    public static class Constants {
+        public static final String DOMAIN_INFINISPAN_DATAGRID = InfinispanConnectionProvider.JMX_DOMAIN;
+
+        public static final String TYPE_CHANNEL = "channel";
+        public static final String TYPE_CACHE = "Cache";
+        public static final String TYPE_CACHE_MANAGER = "CacheManager";
+
+        public static final String COMPONENT_STATISTICS = "Statistics";
+
+        /** Cache statistics */
+        public static final String STAT_CACHE_AVERAGE_READ_TIME = "averageReadTime";
+        public static final String STAT_CACHE_AVERAGE_WRITE_TIME = "averageWriteTime";
+        public static final String STAT_CACHE_ELAPSED_TIME = "elapsedTime";
+        public static final String STAT_CACHE_EVICTIONS = "evictions";
+        public static final String STAT_CACHE_HITS = "hits";
+        public static final String STAT_CACHE_HIT_RATIO = "hitRatio";
+        public static final String STAT_CACHE_MISSES = "misses";
+        public static final String STAT_CACHE_NUMBER_OF_ENTRIES = "numberOfEntries";
+        public static final String STAT_CACHE_NUMBER_OF_ENTRIES_IN_MEMORY = "numberOfEntriesInMemory";
+        public static final String STAT_CACHE_READ_WRITE_RATIO = "readWriteRatio";
+        public static final String STAT_CACHE_REMOVE_HITS = "removeHits";
+        public static final String STAT_CACHE_REMOVE_MISSES = "removeMisses";
+        public static final String STAT_CACHE_STORES = "stores";
+        public static final String STAT_CACHE_TIME_SINCE_RESET = "timeSinceReset";
+
+        /** JGroups channel statistics */
+        public static final String STAT_CHANNEL_ADDRESS = "address";
+        public static final String STAT_CHANNEL_ADDRESS_UUID = "address_uuid";
+        public static final String STAT_CHANNEL_CLOSED = "closed";
+        public static final String STAT_CHANNEL_CLUSTER_NAME = "cluster_name";
+        public static final String STAT_CHANNEL_CONNECTED = "connected";
+        public static final String STAT_CHANNEL_CONNECTING = "connecting";
+        public static final String STAT_CHANNEL_DISCARD_OWN_MESSAGES = "discard_own_messages";
+        public static final String STAT_CHANNEL_OPEN = "open";
+        public static final String STAT_CHANNEL_RECEIVED_BYTES = "received_bytes";
+        public static final String STAT_CHANNEL_RECEIVED_MESSAGES = "received_messages";
+        public static final String STAT_CHANNEL_SENT_BYTES = "sent_bytes";
+        public static final String STAT_CHANNEL_SENT_MESSAGES = "sent_messages";
+        public static final String STAT_CHANNEL_STATE = "state";
+        public static final String STAT_CHANNEL_STATS = "stats";
+        public static final String STAT_CHANNEL_VIEW = "view";
+
+    }
+
+    Map<String, Object> getStatistics();
+
+    Comparable getSingleStatistics(String statisticsName);
+
+    void waitToBecomeAvailable(int time, TimeUnit unit);
+
+    /**
+     * Resets the statistics counters.
+     */
+    void reset();
+
+    /**
+     * Returns {@code true} iff the statistics represented by this object can be retrieved from the server.
+     */
+    boolean exists();
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/jmx/JmxConnectorRegistry.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/jmx/JmxConnectorRegistry.java
new file mode 100644
index 0000000..3a87c5b
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/jmx/JmxConnectorRegistry.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.arquillian.jmx;
+
+import javax.management.remote.JMXConnector;
+import javax.management.remote.JMXServiceURL;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public interface JmxConnectorRegistry {
+    JMXConnector getConnection(JMXServiceURL url);
+
+    void closeAll();
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/jmx/JmxConnectorRegistryCreator.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/jmx/JmxConnectorRegistryCreator.java
new file mode 100644
index 0000000..50c9b96
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/jmx/JmxConnectorRegistryCreator.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.arquillian.jmx;
+
+import java.io.IOException;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import javax.management.remote.JMXConnector;
+import javax.management.remote.JMXConnectorFactory;
+import javax.management.remote.JMXServiceURL;
+import org.jboss.arquillian.core.api.InstanceProducer;
+import org.jboss.arquillian.core.api.annotation.ApplicationScoped;
+import org.jboss.arquillian.core.api.annotation.Inject;
+import org.jboss.arquillian.core.api.annotation.Observes;
+import org.jboss.arquillian.test.spi.event.suite.BeforeSuite;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class JmxConnectorRegistryCreator {
+
+    @Inject
+    @ApplicationScoped
+    private InstanceProducer<JmxConnectorRegistry> connectorRegistry;
+
+    public void configureJmxConnectorRegistry(@Observes BeforeSuite event) {
+        if (connectorRegistry.get() == null) {
+            connectorRegistry.set(new JmxConnectorRegistry() {
+
+                private volatile ConcurrentMap<JMXServiceURL, JMXConnector> connectors = new ConcurrentHashMap<>();
+
+                @Override
+                public JMXConnector getConnection(JMXServiceURL url) {
+                    JMXConnector res = connectors.get(url);
+                    if (res == null) {
+                        try {
+                            final JMXConnector conn = JMXConnectorFactory.newJMXConnector(url, null);
+                            res = connectors.putIfAbsent(url, conn);
+                            if (res == null) {
+                                res = conn;
+                            }
+                            res.connect();
+                        } catch (IOException ex) {
+                            throw new RuntimeException("Could not instantiate JMX connector for " + url, ex);
+                        }
+                    }
+                    return res;
+                }
+
+                @Override
+                public void closeAll() {
+                    connectors.values().forEach(c -> { try { c.close(); } catch (IOException e) {} });
+                    connectors.clear();
+                }
+            });
+        }
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java
index 7757b07..33dc8c2 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java
@@ -32,6 +32,7 @@ import org.jboss.arquillian.graphene.location.CustomizableURLResourceProvider;
 import org.jboss.arquillian.test.spi.enricher.resource.ResourceProvider;
 import org.jboss.arquillian.test.spi.execution.TestExecutionDecider;
 import org.keycloak.testsuite.arquillian.h2.H2TestEnricher;
+import org.keycloak.testsuite.arquillian.jmx.JmxConnectorRegistryCreator;
 import org.keycloak.testsuite.arquillian.karaf.CustomKarafContainer;
 import org.keycloak.testsuite.arquillian.migration.MigrationTestExecutionDecider;
 import org.keycloak.testsuite.arquillian.provider.AdminClientProvider;
@@ -44,6 +45,7 @@ import org.keycloak.testsuite.drone.HtmlUnitScreenshots;
 import org.keycloak.testsuite.drone.KeycloakDronePostSetup;
 import org.keycloak.testsuite.drone.KeycloakHtmlUnitInstantiator;
 import org.keycloak.testsuite.drone.KeycloakWebDriverConfigurator;
+import org.jboss.arquillian.test.spi.TestEnricher;
 
 /**
  *
@@ -65,6 +67,8 @@ public class KeycloakArquillianExtension implements LoadableExtension {
                 .service(DeploymentScenarioGenerator.class, DeploymentTargetModifier.class)
                 .service(ApplicationArchiveProcessor.class, DeploymentArchiveProcessor.class)
                 .service(DeployableContainer.class, CustomKarafContainer.class)
+                .service(TestEnricher.class, CacheStatisticsControllerEnricher.class)
+                .observer(JmxConnectorRegistryCreator.class)
                 .observer(AuthServerTestEnricher.class)
                 .observer(AppServerTestEnricher.class)
                 .observer(H2TestEnricher.class);
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/LoadBalancerControllerProvider.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/LoadBalancerControllerProvider.java
index 4f99feb..af1703d 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/LoadBalancerControllerProvider.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/LoadBalancerControllerProvider.java
@@ -2,14 +2,8 @@ package org.keycloak.testsuite.arquillian.provider;
 
 import org.keycloak.testsuite.arquillian.annotation.LoadBalancer;
 import java.lang.annotation.Annotation;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import org.jboss.arquillian.container.spi.event.KillContainer;
-import org.jboss.arquillian.container.spi.event.StartContainer;
-import org.jboss.arquillian.container.spi.event.StopContainer;
 import org.jboss.arquillian.core.api.Instance;
 import org.jboss.arquillian.core.api.annotation.Inject;
-import org.jboss.arquillian.core.api.annotation.Observes;
 import org.jboss.arquillian.test.api.ArquillianResource;
 import org.jboss.arquillian.test.spi.enricher.resource.ResourceProvider;
 import org.keycloak.testsuite.arquillian.LoadBalancerController;
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/TestContext.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/TestContext.java
index 30b0405..cfe6d84 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/TestContext.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/TestContext.java
@@ -20,6 +20,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 
 import javax.ws.rs.NotFoundException;
 
@@ -54,7 +55,7 @@ public final class TestContext {
     private boolean initialized;
 
     // Key is realmName, value are objects to clean after the test method
-    private Map<String, TestCleanup> cleanups = new HashMap<>();
+    private Map<String, TestCleanup> cleanups = new ConcurrentHashMap<>();
 
     public TestContext(SuiteContext suiteContext, Class testClass) {
         this.suiteContext = suiteContext;
@@ -146,7 +147,11 @@ public final class TestContext {
         TestCleanup cleanup = cleanups.get(realmName);
         if (cleanup == null) {
             cleanup = new TestCleanup(adminClient, realmName);
-            cleanups.put(realmName, cleanup);
+            TestCleanup existing = cleanups.putIfAbsent(realmName, cleanup);
+
+            if (existing != null) {
+                cleanup = existing;
+            }
         }
         return cleanup;
     }
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LanguageComboboxAwarePage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LanguageComboboxAwarePage.java
new file mode 100644
index 0000000..29d512e
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LanguageComboboxAwarePage.java
@@ -0,0 +1,44 @@
+/*
+ * 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.pages;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class LanguageComboboxAwarePage extends AbstractPage {
+
+    @FindBy(id = "kc-current-locale-link")
+    private WebElement languageText;
+
+    @FindBy(id = "kc-locale-dropdown")
+    private WebElement localeDropdown;
+
+    public String getLanguageDropdownText() {
+        return languageText.getText();
+    }
+
+    public void openLanguage(String language){
+        WebElement langLink = localeDropdown.findElement(By.xpath("//a[text()='" + language + "']"));
+        String url = langLink.getAttribute("href");
+        driver.navigate().to(url);
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java
index 11d8fb2..b025ec7 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java
@@ -26,7 +26,7 @@ import org.openqa.selenium.support.FindBy;
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
-public class LoginPage extends AbstractPage {
+public class LoginPage extends LanguageComboboxAwarePage {
 
     @ArquillianResource
     protected OAuthClient oauth;
@@ -75,12 +75,6 @@ public class LoginPage extends AbstractPage {
     private WebElement instruction;
 
 
-    @FindBy(id = "kc-current-locale-link")
-    private WebElement languageText;
-
-    @FindBy(id = "kc-locale-dropdown")
-    private WebElement localeDropdown;
-
     public void login(String username, String password) {
         usernameInput.clear();
         usernameInput.sendKeys(username);
@@ -191,14 +185,4 @@ public class LoginPage extends AbstractPage {
         assertCurrent();
     }
 
-    public String getLanguageDropdownText() {
-        return languageText.getText();
-    }
-
-    public void openLanguage(String language){
-        WebElement langLink = localeDropdown.findElement(By.xpath("//a[text()='" +language +"']"));
-        String url = langLink.getAttribute("href");
-        driver.navigate().to(url);
-    }
-
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java
index 93d203d..7a963e1 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java
@@ -22,7 +22,7 @@ import org.openqa.selenium.support.FindBy;
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
-public class LoginPasswordUpdatePage extends AbstractPage {
+public class LoginPasswordUpdatePage extends LanguageComboboxAwarePage {
 
     @FindBy(id = "password-new")
     private WebElement newPasswordInput;
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/OAuthGrantPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/OAuthGrantPage.java
index 1a550ec..cfb1f06 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/OAuthGrantPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/OAuthGrantPage.java
@@ -22,7 +22,7 @@ import org.openqa.selenium.support.FindBy;
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
-public class OAuthGrantPage extends AbstractPage {
+public class OAuthGrantPage extends LanguageComboboxAwarePage {
 
     @FindBy(css = "input[name=\"accept\"]")
     private WebElement acceptButton;
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
index 4c89eaa..207a317 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
@@ -102,7 +102,7 @@ public class OAuthClient {
 
     private String maxAge;
 
-    private String responseType = OAuth2Constants.CODE;
+    private String responseType;
 
     private String responseMode;
 
@@ -171,6 +171,8 @@ public class OAuthClient {
         clientSessionState = null;
         clientSessionHost = null;
         maxAge = null;
+        responseType = OAuth2Constants.CODE;
+        responseMode = null;
         nonce = null;
         request = null;
         requestUri = null;
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TestCleanup.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TestCleanup.java
index 2101c0b..192712e 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TestCleanup.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TestCleanup.java
@@ -17,13 +17,13 @@
 
 package org.keycloak.testsuite.util;
 
-import java.util.LinkedList;
 import java.util.List;
 
 import javax.ws.rs.NotFoundException;
 
 import org.keycloak.admin.client.Keycloak;
 import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.common.util.ConcurrentMultivaluedHashMap;
 
 /**
  * Enlist resources to be cleaned after test method
@@ -32,18 +32,21 @@ import org.keycloak.admin.client.resource.RealmResource;
  */
 public class TestCleanup {
 
+    private static final String IDENTITY_PROVIDER_ALIASES = "IDENTITY_PROVIDER_ALIASES";
+    private static final String USER_IDS = "USER_IDS";
+    private static final String COMPONENT_IDS = "COMPONENT_IDS";
+    private static final String CLIENT_UUIDS = "CLIENT_UUIDS";
+    private static final String ROLE_IDS = "ROLE_IDS";
+    private static final String GROUP_IDS = "GROUP_IDS";
+    private static final String AUTH_FLOW_IDS = "AUTH_FLOW_IDS";
+    private static final String AUTH_CONFIG_IDS = "AUTH_CONFIG_IDS";
+
     private final Keycloak adminClient;
     private final String realmName;
 
+    // Key is kind of entity (eg. "client", "role", "user" etc), Values are all kind of entities of given type to cleanup
+    private ConcurrentMultivaluedHashMap<String, String> entities = new ConcurrentMultivaluedHashMap<>();
 
-    private List<String> identityProviderAliases;
-    private List<String> userIds;
-    private List<String> componentIds;
-    private List<String> clientUuids;
-    private List<String> roleIds;
-    private List<String> groupIds;
-    private List<String> authFlowIds;
-    private List<String> authConfigIds;
 
     public TestCleanup(Keycloak adminClient, String realmName) {
         this.adminClient = adminClient;
@@ -52,74 +55,49 @@ public class TestCleanup {
 
 
     public void addUserId(String userId) {
-        if (userIds == null) {
-            userIds = new LinkedList<>();
-        }
-        userIds.add(userId);
+        entities.add(USER_IDS, userId);
     }
 
 
     public void addIdentityProviderAlias(String identityProviderAlias) {
-        if (identityProviderAliases == null) {
-            identityProviderAliases = new LinkedList<>();
-        }
-        identityProviderAliases.add(identityProviderAlias);
+        entities.add(IDENTITY_PROVIDER_ALIASES, identityProviderAlias);
     }
 
 
     public void addComponentId(String componentId) {
-        if (componentIds == null) {
-            componentIds = new LinkedList<>();
-        }
-        if (componentId == null) return;
-        componentIds.add(componentId);
+        entities.add(COMPONENT_IDS, componentId);
     }
 
 
     public void addClientUuid(String clientUuid) {
-        if (clientUuids == null) {
-            clientUuids = new LinkedList<>();
-        }
-        clientUuids.add(clientUuid);
+        entities.add(CLIENT_UUIDS, clientUuid);
     }
 
 
     public void addRoleId(String roleId) {
-        if (roleIds == null) {
-            roleIds = new LinkedList<>();
-        }
-        roleIds.add(roleId);
+        entities.add(ROLE_IDS, roleId);
     }
 
 
     public void addGroupId(String groupId) {
-        if (groupIds == null) {
-            groupIds = new LinkedList<>();
-        }
-        groupIds.add(groupId);
+        entities.add(GROUP_IDS, groupId);
     }
 
 
     public void addAuthenticationFlowId(String flowId) {
-        if (authFlowIds == null) {
-            authFlowIds = new LinkedList<>();
-        }
-        authFlowIds.add(flowId);
+        entities.add(AUTH_FLOW_IDS, flowId);
     }
 
 
     public void addAuthenticationConfigId(String executionConfigId) {
-        if (authConfigIds == null) {
-            authConfigIds = new LinkedList<>();
-        }
-        authConfigIds.add(executionConfigId);
+        entities.add(AUTH_CONFIG_IDS, executionConfigId);
     }
 
 
     public void executeCleanup() {
-        if (adminClient == null) throw new RuntimeException("ADMIN CLIENT NULL");
         RealmResource realm = adminClient.realm(realmName);
 
+        List<String> userIds = entities.get(USER_IDS);
         if (userIds != null) {
             for (String userId : userIds) {
                 try {
@@ -130,6 +108,7 @@ public class TestCleanup {
             }
         }
 
+        List<String> identityProviderAliases = entities.get(IDENTITY_PROVIDER_ALIASES);
         if (identityProviderAliases != null) {
             for (String idpAlias : identityProviderAliases) {
                 try {
@@ -140,6 +119,7 @@ public class TestCleanup {
             }
         }
 
+        List<String> componentIds = entities.get(COMPONENT_IDS);
         if (componentIds != null) {
             for (String componentId : componentIds) {
                 try {
@@ -150,6 +130,7 @@ public class TestCleanup {
             }
         }
 
+        List<String> clientUuids = entities.get(CLIENT_UUIDS);
         if (clientUuids != null) {
             for (String clientUuId : clientUuids) {
                 try {
@@ -160,6 +141,7 @@ public class TestCleanup {
             }
         }
 
+        List<String> roleIds = entities.get(ROLE_IDS);
         if (roleIds != null) {
             for (String roleId : roleIds) {
                 try {
@@ -170,6 +152,7 @@ public class TestCleanup {
             }
         }
 
+        List<String> groupIds = entities.get(GROUP_IDS);
         if (groupIds != null) {
             for (String groupId : groupIds) {
                 try {
@@ -180,6 +163,7 @@ public class TestCleanup {
             }
         }
 
+        List<String> authFlowIds = entities.get(AUTH_FLOW_IDS);
         if (authFlowIds != null) {
             for (String flowId : authFlowIds) {
                 try {
@@ -190,6 +174,7 @@ public class TestCleanup {
             }
         }
 
+        List<String> authConfigIds = entities.get(AUTH_CONFIG_IDS);
         if (authConfigIds != null) {
             for (String configId : authConfigIds) {
                 try {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GroupPolicyManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GroupPolicyManagementTest.java
new file mode 100644
index 0000000..57d86a7
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GroupPolicyManagementTest.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.admin.client.authorization;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.core.Response;
+
+import org.junit.Test;
+import org.keycloak.admin.client.resource.AuthorizationResource;
+import org.keycloak.admin.client.resource.GroupPoliciesResource;
+import org.keycloak.admin.client.resource.GroupPolicyResource;
+import org.keycloak.admin.client.resource.PolicyResource;
+import org.keycloak.admin.client.resource.RolePoliciesResource;
+import org.keycloak.admin.client.resource.RolePolicyResource;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.authorization.DecisionStrategy;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.Logic;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+import org.keycloak.testsuite.util.GroupBuilder;
+import org.keycloak.testsuite.util.RealmBuilder;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPolicyManagementTest extends AbstractPolicyManagementTest {
+
+    @Override
+    protected RealmBuilder createTestRealm() {
+        return super.createTestRealm().group(GroupBuilder.create().name("Group A")
+                .subGroups(Arrays.asList("Group B", "Group D").stream().map(name -> {
+                    if ("Group B".equals(name)) {
+                        return GroupBuilder.create().name(name).subGroups(Arrays.asList("Group C", "Group E").stream().map(new Function<String, GroupRepresentation>() {
+                            @Override
+                            public GroupRepresentation apply(String name) {
+                                return GroupBuilder.create().name(name).build();
+                            }
+                        }).collect(Collectors.toList())).build();
+                    }
+                    return GroupBuilder.create().name(name).build();
+                }).collect(Collectors.toList()))
+                .build()).group(GroupBuilder.create().name("Group E").build());
+    }
+
+    @Test
+    public void testCreate() {
+        AuthorizationResource authorization = getClient().authorization();
+        GroupPolicyRepresentation representation = new GroupPolicyRepresentation();
+
+        representation.setName("Group Policy");
+        representation.setDescription("description");
+        representation.setDecisionStrategy(DecisionStrategy.CONSENSUS);
+        representation.setLogic(Logic.NEGATIVE);
+        representation.setGroupsClaim("groups");
+        representation.addGroupPath("/Group A/Group B/Group C", true);
+        representation.addGroupPath("Group E");
+
+        assertCreated(authorization, representation);
+    }
+
+    @Test
+    public void testUpdate() {
+        AuthorizationResource authorization = getClient().authorization();
+        GroupPolicyRepresentation representation = new GroupPolicyRepresentation();
+
+        representation.setName("Update Group Policy");
+        representation.setDescription("description");
+        representation.setDecisionStrategy(DecisionStrategy.CONSENSUS);
+        representation.setLogic(Logic.NEGATIVE);
+        representation.setGroupsClaim("groups");
+        representation.addGroupPath("/Group A/Group B/Group C", true);
+        representation.addGroupPath("Group E");
+
+        assertCreated(authorization, representation);
+
+        representation.setName("changed");
+        representation.setDescription("changed");
+        representation.setDecisionStrategy(DecisionStrategy.AFFIRMATIVE);
+        representation.setLogic(Logic.POSITIVE);
+        representation.removeGroup("/Group A/Group B");
+
+        GroupPoliciesResource policies = authorization.policies().group();
+        GroupPolicyResource permission = policies.findById(representation.getId());
+
+        permission.update(representation);
+        assertRepresentation(representation, permission);
+
+        for (GroupPolicyRepresentation.GroupDefinition roleDefinition : representation.getGroups()) {
+            if (roleDefinition.getPath().equals("Group E")) {
+                roleDefinition.setExtendChildren(true);
+            }
+        }
+
+        permission.update(representation);
+        assertRepresentation(representation, permission);
+
+        representation.getGroups().clear();
+        representation.addGroupPath("/Group A/Group B");
+
+        permission.update(representation);
+        assertRepresentation(representation, permission);
+    }
+
+    @Test
+    public void testDelete() {
+        AuthorizationResource authorization = getClient().authorization();
+        GroupPolicyRepresentation representation = new GroupPolicyRepresentation();
+
+        representation.setName("Delete Group Policy");
+        representation.setGroupsClaim("groups");
+        representation.addGroupPath("/Group A/Group B/Group C", true);
+        representation.addGroupPath("Group E");
+
+        GroupPoliciesResource policies = authorization.policies().group();
+        Response response = policies.create(representation);
+        GroupPolicyRepresentation created = response.readEntity(GroupPolicyRepresentation.class);
+
+        policies.findById(created.getId()).remove();
+
+        GroupPolicyResource removed = policies.findById(created.getId());
+
+        try {
+            removed.toRepresentation();
+            fail("Permission not removed");
+        } catch (NotFoundException ignore) {
+
+        }
+    }
+
+    @Test
+    public void testGenericConfig() {
+        AuthorizationResource authorization = getClient().authorization();
+        GroupPolicyRepresentation representation = new GroupPolicyRepresentation();
+
+        representation.setName("Test Generic Config Permission");
+        representation.setGroupsClaim("groups");
+        representation.addGroupPath("/Group A");
+
+        GroupPoliciesResource policies = authorization.policies().group();
+        Response response = policies.create(representation);
+        GroupPolicyRepresentation created = response.readEntity(GroupPolicyRepresentation.class);
+
+        PolicyResource policy = authorization.policies().policy(created.getId());
+        PolicyRepresentation genericConfig = policy.toRepresentation();
+
+        assertNotNull(genericConfig.getConfig());
+        assertNotNull(genericConfig.getConfig().get("groups"));
+
+        GroupRepresentation group = getRealm().groups().groups().stream().filter(groupRepresentation -> groupRepresentation.getName().equals("Group A")).findFirst().get();
+
+        assertTrue(genericConfig.getConfig().get("groups").contains(group.getId()));
+    }
+
+    private void assertCreated(AuthorizationResource authorization, GroupPolicyRepresentation representation) {
+        GroupPoliciesResource policies = authorization.policies().group();
+        Response response = policies.create(representation);
+        GroupPolicyRepresentation created = response.readEntity(GroupPolicyRepresentation.class);
+        GroupPolicyResource policy = policies.findById(created.getId());
+        assertRepresentation(representation, policy);
+    }
+
+    private void assertRepresentation(GroupPolicyRepresentation representation, GroupPolicyResource permission) {
+        GroupPolicyRepresentation actual = permission.toRepresentation();
+        assertRepresentation(representation, actual, () -> permission.resources(), () -> Collections.emptyList(), () -> permission.associatedPolicies());
+        assertEquals(representation.getGroups().size(), actual.getGroups().size());
+        assertEquals(0, actual.getGroups().stream().filter(actualDefinition -> !representation.getGroups().stream()
+                .filter(groupDefinition -> getGroupPath(actualDefinition.getId()).equals(getCanonicalGroupPath(groupDefinition.getPath())) && actualDefinition.isExtendChildren() == groupDefinition.isExtendChildren())
+                .findFirst().isPresent())
+                .count());
+    }
+
+    private String getGroupPath(String id) {
+        return getRealm().groups().group(id).toRepresentation().getPath();
+    }
+
+    private String getCanonicalGroupPath(String path) {
+        if (path.charAt(0) == '/') {
+            return path;
+        }
+        return "/" + path;
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserStorageRestTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserStorageRestTest.java
index 15f0564..d7f494f 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserStorageRestTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserStorageRestTest.java
@@ -156,6 +156,65 @@ public class UserStorageRestTest extends AbstractAdminTest {
 
     }
 
+
+    // KEYCLOAK-4438
+    @Test
+    public void testKerberosAuthenticatorDisabledWhenProviderRemoved() {
+        // Assert kerberos authenticator DISABLED
+        AuthenticationExecutionInfoRepresentation kerberosExecution = findKerberosExecution();
+        Assert.assertEquals(kerberosExecution.getRequirement(), AuthenticationExecutionModel.Requirement.DISABLED.toString());
+
+        // create LDAP provider with kerberos
+        ComponentRepresentation ldapRep = new ComponentRepresentation();
+        ldapRep.setName("ldap2");
+        ldapRep.setProviderId("ldap");
+        ldapRep.setProviderType(UserStorageProvider.class.getName());
+        ldapRep.setConfig(new MultivaluedHashMap<>());
+        ldapRep.getConfig().putSingle("priority", Integer.toString(2));
+        ldapRep.getConfig().putSingle(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, "true");
+
+
+        String id = createComponent(ldapRep);
+
+        // Assert kerberos authenticator ALTERNATIVE
+        kerberosExecution = findKerberosExecution();
+        Assert.assertEquals(kerberosExecution.getRequirement(), AuthenticationExecutionModel.Requirement.ALTERNATIVE.toString());
+
+        // Remove LDAP provider
+        realm.components().component(id).remove();
+
+        // Assert kerberos authenticator DISABLED
+        kerberosExecution = findKerberosExecution();
+        Assert.assertEquals(kerberosExecution.getRequirement(), AuthenticationExecutionModel.Requirement.DISABLED.toString());
+
+        // Add kerberos provider
+        ComponentRepresentation kerberosRep = new ComponentRepresentation();
+        kerberosRep.setName("kerberos");
+        kerberosRep.setProviderId("kerberos");
+        kerberosRep.setProviderType(UserStorageProvider.class.getName());
+        kerberosRep.setConfig(new MultivaluedHashMap<>());
+        kerberosRep.getConfig().putSingle("priority", Integer.toString(2));
+
+        id = createComponent(kerberosRep);
+
+
+        // Assert kerberos authenticator ALTERNATIVE
+        kerberosExecution = findKerberosExecution();
+        Assert.assertEquals(kerberosExecution.getRequirement(), AuthenticationExecutionModel.Requirement.ALTERNATIVE.toString());
+
+        // Switch kerberos authenticator to REQUIRED
+        kerberosExecution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED.toString());
+        realm.flows().updateExecutions("browser", kerberosExecution);
+
+        // Remove Kerberos provider
+        realm.components().component(id).remove();
+
+        // Assert kerberos authenticator DISABLED
+        kerberosExecution = findKerberosExecution();
+        Assert.assertEquals(kerberosExecution.getRequirement(), AuthenticationExecutionModel.Requirement.DISABLED.toString());
+    }
+
+
     @Test
     public void testValidateAndCreateLdapProvider() {
         // Invalid filter
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupNamePolicyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupNamePolicyTest.java
new file mode 100644
index 0000000..cc4b911
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupNamePolicyTest.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.authz;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.AuthorizationResource;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.authorization.client.AuthorizationDeniedException;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.Configuration;
+import org.keycloak.authorization.client.representation.AuthorizationRequest;
+import org.keycloak.authorization.client.representation.AuthorizationResponse;
+import org.keycloak.authorization.client.representation.PermissionRequest;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.mappers.GroupMembershipMapper;
+import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
+import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.representations.idm.ProtocolMapperRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.util.AdminClientUtil;
+import org.keycloak.testsuite.util.ClientBuilder;
+import org.keycloak.testsuite.util.GroupBuilder;
+import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.util.RoleBuilder;
+import org.keycloak.testsuite.util.RolesBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupNamePolicyTest extends AbstractKeycloakTest {
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+        ProtocolMapperRepresentation groupProtocolMapper = new ProtocolMapperRepresentation();
+
+        groupProtocolMapper.setName("groups");
+        groupProtocolMapper.setProtocolMapper(GroupMembershipMapper.PROVIDER_ID);
+        groupProtocolMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        groupProtocolMapper.setConsentRequired(false);
+        Map<String, String> config = new HashMap<>();
+        config.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, "groups");
+        config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
+        config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
+        groupProtocolMapper.setConfig(config);
+
+        testRealms.add(RealmBuilder.create().name("authz-test")
+                .roles(RolesBuilder.create()
+                        .realmRole(RoleBuilder.create().name("uma_authorization").build())
+                )
+                .group(GroupBuilder.create().name("Group A")
+                    .subGroups(Arrays.asList("Group B", "Group D").stream().map(name -> {
+                        if ("Group B".equals(name)) {
+                            return GroupBuilder.create().name(name).subGroups(Arrays.asList("Group C", "Group E").stream().map(new Function<String, GroupRepresentation>() {
+                                @Override
+                                public GroupRepresentation apply(String name) {
+                                    return GroupBuilder.create().name(name).build();
+                                }
+                            }).collect(Collectors.toList())).build();
+                        }
+                        return GroupBuilder.create().name(name).build();
+                    }).collect(Collectors.toList())).build())
+                .group(GroupBuilder.create().name("Group E").build())
+                .user(UserBuilder.create().username("marta").password("password").addRoles("uma_authorization").addGroups("Group A"))
+                .user(UserBuilder.create().username("alice").password("password").addRoles("uma_authorization"))
+                .user(UserBuilder.create().username("kolo").password("password").addRoles("uma_authorization"))
+                .client(ClientBuilder.create().clientId("resource-server-test")
+                    .secret("secret")
+                    .authorizationServicesEnabled(true)
+                    .redirectUris("http://localhost/resource-server-test")
+                    .defaultRoles("uma_protection")
+                    .directAccessGrants()
+                    .protocolMapper(groupProtocolMapper))
+                .build());
+    }
+
+    @Before
+    public void configureAuthorization() throws Exception {
+        createResource("Resource A");
+        createResource("Resource B");
+        createResource("Resource C");
+
+        createGroupPolicy("Only Group A Policy", "/Group A", true);
+        createGroupPolicy("Only Group B Policy", "/Group A/Group B", false);
+        createGroupPolicy("Only Group C Policy", "/Group A/Group B/Group C", false);
+
+        createResourcePermission("Resource A Permission", "Resource A", "Only Group A Policy");
+        createResourcePermission("Resource B Permission", "Resource B", "Only Group B Policy");
+        createResourcePermission("Resource C Permission", "Resource C", "Only Group C Policy");
+
+        RealmResource realm = getRealm();
+        GroupRepresentation group = getGroup("/Group A/Group B/Group C");
+        UserRepresentation user = realm.users().search("kolo").get(0);
+
+        realm.users().get(user.getId()).joinGroup(group.getId());
+
+        group = getGroup("/Group A/Group B");
+        user = realm.users().search("alice").get(0);
+
+        realm.users().get(user.getId()).joinGroup(group.getId());
+    }
+
+    @Test
+    public void testExactNameMatch() {
+        AuthzClient authzClient = getAuthzClient();
+        PermissionRequest request = new PermissionRequest();
+
+        request.setResourceSetName("Resource A");
+
+        String ticket = authzClient.protection().permission().forResource(request).getTicket();
+        AuthorizationResponse response = authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
+
+        assertNotNull(response.getRpt());
+
+        try {
+            authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+            fail("Should fail because user is not granted with expected group");
+        } catch (AuthorizationDeniedException ignore) {
+
+        }
+
+        try {
+            authzClient.authorization("alice", "password").authorize(new AuthorizationRequest(ticket));
+            fail("Should fail because user is not granted with expected group");
+        } catch (AuthorizationDeniedException ignore) {
+
+        }
+    }
+
+    @Test
+    public void testOnlyChildrenPolicy() throws Exception {
+        RealmResource realm = getRealm();
+        AuthzClient authzClient = getAuthzClient();
+        PermissionRequest request = new PermissionRequest();
+
+        request.setResourceSetName("Resource B");
+
+        String ticket = authzClient.protection().permission().forResource(request).getTicket();
+
+        try {
+            authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+            fail("Should fail because user is not granted with expected group");
+        } catch (AuthorizationDeniedException ignore) {
+
+        }
+
+        AuthorizationResponse response = authzClient.authorization("alice", "password").authorize(new AuthorizationRequest(ticket));
+
+        assertNotNull(response.getRpt());
+
+        try {
+            authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
+            fail("Should fail because user is not granted with expected role");
+        } catch (AuthorizationDeniedException ignore) {
+
+        }
+
+        request = new PermissionRequest();
+
+        request.setResourceSetName("Resource C");
+
+        ticket = authzClient.protection().permission().forResource(request).getTicket();
+
+        response = authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+
+        assertNotNull(response.getRpt());
+    }
+
+    private void createGroupPolicy(String name, String groupPath, boolean extendChildren) {
+        GroupPolicyRepresentation policy = new GroupPolicyRepresentation();
+
+        policy.setName(name);
+        policy.setGroupsClaim("groups");
+        policy.addGroupPath(groupPath, extendChildren);
+
+        getClient().authorization().policies().group().create(policy);
+    }
+
+    private void createResourcePermission(String name, String resource, String... policies) {
+        ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
+
+        permission.setName(name);
+        permission.addResource(resource);
+        permission.addPolicy(policies);
+
+        getClient().authorization().permissions().resource().create(permission);
+    }
+
+    private void createResource(String name) {
+        AuthorizationResource authorization = getClient().authorization();
+        ResourceRepresentation resource = new ResourceRepresentation(name);
+
+        authorization.resources().create(resource);
+    }
+
+    private RealmResource getRealm() {
+        try {
+            return AdminClientUtil.createAdminClient().realm("authz-test");
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to create admin client");
+        }
+    }
+
+    private ClientResource getClient(RealmResource realm) {
+        ClientsResource clients = realm.clients();
+        return clients.findByClientId("resource-server-test").stream().map(representation -> clients.get(representation.getId())).findFirst().orElseThrow(() -> new RuntimeException("Expected client [resource-server-test]"));
+    }
+
+    private AuthzClient getAuthzClient() {
+        try {
+            return AuthzClient.create(JsonSerialization.readValue(getClass().getResourceAsStream("/authorization-test/default-keycloak.json"), Configuration.class));
+        } catch (IOException cause) {
+            throw new RuntimeException("Failed to create authz client", cause);
+        }
+    }
+
+    private ClientResource getClient() {
+        return getClient(getRealm());
+    }
+
+    private GroupRepresentation getGroup(String path) {
+        String[] parts = path.split("/");
+        RealmResource realm = getRealm();
+        GroupRepresentation parent = null;
+
+        for (String part : parts) {
+            if ("".equals(part)) {
+                continue;
+            }
+            if (parent == null) {
+                parent = realm.groups().groups().stream().filter(new Predicate<GroupRepresentation>() {
+                    @Override
+                    public boolean test(GroupRepresentation groupRepresentation) {
+                        return part.equals(groupRepresentation.getName());
+                    }
+                }).findFirst().get();
+                continue;
+            }
+
+            GroupRepresentation group = getGroup(part, parent.getSubGroups());
+
+            if (path.endsWith(group.getName())) {
+                return group;
+            }
+
+            parent = group;
+        }
+
+        return null;
+    }
+
+    private GroupRepresentation getGroup(String name, List<GroupRepresentation> groups) {
+        for (GroupRepresentation group : groups) {
+            if (name.equals(group.getName())) {
+                return group;
+            }
+
+            GroupRepresentation child = getGroup(name, group.getSubGroups());
+
+            if (child != null && name.equals(child.getName())) {
+                return child;
+            }
+        }
+
+        return null;
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupPathPolicyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupPathPolicyTest.java
new file mode 100644
index 0000000..19f74b4
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupPathPolicyTest.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.authz;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.AuthorizationResource;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.authorization.client.AuthorizationDeniedException;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.Configuration;
+import org.keycloak.authorization.client.representation.AuthorizationRequest;
+import org.keycloak.authorization.client.representation.AuthorizationResponse;
+import org.keycloak.authorization.client.representation.PermissionRequest;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.mappers.GroupMembershipMapper;
+import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
+import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.representations.idm.ProtocolMapperRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.util.AdminClientUtil;
+import org.keycloak.testsuite.util.ClientBuilder;
+import org.keycloak.testsuite.util.GroupBuilder;
+import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.util.RoleBuilder;
+import org.keycloak.testsuite.util.RolesBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPathPolicyTest extends AbstractKeycloakTest {
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+        ProtocolMapperRepresentation groupProtocolMapper = new ProtocolMapperRepresentation();
+
+        groupProtocolMapper.setName("groups");
+        groupProtocolMapper.setProtocolMapper(GroupMembershipMapper.PROVIDER_ID);
+        groupProtocolMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        groupProtocolMapper.setConsentRequired(false);
+        Map<String, String> config = new HashMap<>();
+        config.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, "groups");
+        config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
+        config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
+        config.put("full.path", "true");
+        groupProtocolMapper.setConfig(config);
+
+        testRealms.add(RealmBuilder.create().name("authz-test")
+                .roles(RolesBuilder.create()
+                        .realmRole(RoleBuilder.create().name("uma_authorization").build())
+                )
+                .group(GroupBuilder.create().name("Group A")
+                    .subGroups(Arrays.asList("Group B", "Group D").stream().map(name -> {
+                        if ("Group B".equals(name)) {
+                            return GroupBuilder.create().name(name).subGroups(Arrays.asList("Group C", "Group E").stream().map(new Function<String, GroupRepresentation>() {
+                                @Override
+                                public GroupRepresentation apply(String name) {
+                                    return GroupBuilder.create().name(name).build();
+                                }
+                            }).collect(Collectors.toList())).build();
+                        }
+                        return GroupBuilder.create().name(name).build();
+                    }).collect(Collectors.toList())).build())
+                .group(GroupBuilder.create().name("Group E").build())
+                .user(UserBuilder.create().username("marta").password("password").addRoles("uma_authorization").addGroups("Group A"))
+                .user(UserBuilder.create().username("alice").password("password").addRoles("uma_authorization"))
+                .user(UserBuilder.create().username("kolo").password("password").addRoles("uma_authorization"))
+                .client(ClientBuilder.create().clientId("resource-server-test")
+                    .secret("secret")
+                    .authorizationServicesEnabled(true)
+                    .redirectUris("http://localhost/resource-server-test")
+                    .defaultRoles("uma_protection")
+                    .directAccessGrants()
+                    .protocolMapper(groupProtocolMapper))
+                .build());
+    }
+
+    @Before
+    public void configureAuthorization() throws Exception {
+        createResource("Resource A");
+        createResource("Resource B");
+
+        createGroupPolicy("Parent And Children Policy", "/Group A", true);
+        createGroupPolicy("Only Children Policy", "/Group A/Group B/Group C", false);
+
+        createResourcePermission("Resource A Permission", "Resource A", "Parent And Children Policy");
+        createResourcePermission("Resource B Permission", "Resource B", "Only Children Policy");
+    }
+
+    @Test
+    public void testAllowParentAndChildren() {
+        AuthzClient authzClient = getAuthzClient();
+        PermissionRequest request = new PermissionRequest();
+
+        request.setResourceSetName("Resource A");
+
+        String ticket = authzClient.protection().permission().forResource(request).getTicket();
+        AuthorizationResponse response = authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
+
+        assertNotNull(response.getRpt());
+
+        RealmResource realm = getRealm();
+        GroupRepresentation group = getGroup("/Group A/Group B/Group C");
+        UserRepresentation user = realm.users().search("kolo").get(0);
+
+        realm.users().get(user.getId()).joinGroup(group.getId());
+
+        ticket = authzClient.protection().permission().forResource(request).getTicket();
+        response = authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+
+        assertNotNull(response.getRpt());
+    }
+
+    @Test
+    public void testOnlyChildrenPolicy() throws Exception {
+        RealmResource realm = getRealm();
+        AuthzClient authzClient = getAuthzClient();
+        PermissionRequest request = new PermissionRequest();
+
+        request.setResourceSetName("Resource B");
+
+        String ticket = authzClient.protection().permission().forResource(request).getTicket();
+
+        try {
+            authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+            fail("Should fail because user is not granted with expected role");
+        } catch (AuthorizationDeniedException ignore) {
+
+        }
+
+        GroupRepresentation group = getGroup("/Group A/Group B/Group C");
+        UserRepresentation user = realm.users().search("kolo").get(0);
+
+        realm.users().get(user.getId()).joinGroup(group.getId());
+
+        AuthorizationResponse response = authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+
+        assertNotNull(response.getRpt());
+
+        try {
+            authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
+            fail("Should fail because user is not granted with expected role");
+        } catch (AuthorizationDeniedException ignore) {
+
+        }
+    }
+
+    private void createGroupPolicy(String name, String groupPath, boolean extendChildren) {
+        GroupPolicyRepresentation policy = new GroupPolicyRepresentation();
+
+        policy.setName(name);
+        policy.setGroupsClaim("groups");
+        policy.addGroupPath(groupPath, extendChildren);
+
+        getClient().authorization().policies().group().create(policy);
+    }
+
+    private void createResourcePermission(String name, String resource, String... policies) {
+        ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
+
+        permission.setName(name);
+        permission.addResource(resource);
+        permission.addPolicy(policies);
+
+        getClient().authorization().permissions().resource().create(permission);
+    }
+
+    private void createResource(String name) {
+        AuthorizationResource authorization = getClient().authorization();
+        ResourceRepresentation resource = new ResourceRepresentation(name);
+
+        authorization.resources().create(resource);
+    }
+
+    private RealmResource getRealm() {
+        try {
+            return AdminClientUtil.createAdminClient().realm("authz-test");
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to create admin client");
+        }
+    }
+
+    private ClientResource getClient(RealmResource realm) {
+        ClientsResource clients = realm.clients();
+        return clients.findByClientId("resource-server-test").stream().map(representation -> clients.get(representation.getId())).findFirst().orElseThrow(() -> new RuntimeException("Expected client [resource-server-test]"));
+    }
+
+    private AuthzClient getAuthzClient() {
+        try {
+            return AuthzClient.create(JsonSerialization.readValue(getClass().getResourceAsStream("/authorization-test/default-keycloak.json"), Configuration.class));
+        } catch (IOException cause) {
+            throw new RuntimeException("Failed to create authz client", cause);
+        }
+    }
+
+    private ClientResource getClient() {
+        return getClient(getRealm());
+    }
+
+    private GroupRepresentation getGroup(String path) {
+        String[] parts = path.split("/");
+        RealmResource realm = getRealm();
+        GroupRepresentation parent = null;
+
+        for (String part : parts) {
+            if ("".equals(part)) {
+                continue;
+            }
+            if (parent == null) {
+                parent = realm.groups().groups().stream().filter(new Predicate<GroupRepresentation>() {
+                    @Override
+                    public boolean test(GroupRepresentation groupRepresentation) {
+                        return part.equals(groupRepresentation.getName());
+                    }
+                }).findFirst().get();
+                continue;
+            }
+
+            GroupRepresentation group = getGroup(part, parent.getSubGroups());
+
+            if (path.endsWith(group.getName())) {
+                return group;
+            }
+
+            parent = group;
+        }
+
+        return null;
+    }
+
+    private GroupRepresentation getGroup(String name, List<GroupRepresentation> groups) {
+        for (GroupRepresentation group : groups) {
+            if (name.equals(group.getName())) {
+                return group;
+            }
+
+            GroupRepresentation child = getGroup(name, group.getSubGroups());
+
+            if (child != null && name.equals(child.getName())) {
+                return child;
+            }
+        }
+
+        return null;
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java
index 84527f7..2baa336 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java
@@ -19,13 +19,20 @@ package org.keycloak.testsuite.crossdc;
 import org.keycloak.admin.client.resource.RealmResource;
 import org.keycloak.events.log.JBossLoggingEventListenerProviderFactory;
 import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.Retry;
+import org.keycloak.testsuite.arquillian.InfinispanStatistics;
 import org.keycloak.testsuite.events.EventsListenerProviderFactory;
 import org.keycloak.testsuite.util.TestCleanup;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import org.hamcrest.Matcher;
 import org.junit.Before;
+import static org.junit.Assert.assertThat;
 
 /**
  *
@@ -78,4 +85,31 @@ public abstract class AbstractAdminCrossDCTest extends AbstractCrossDCTest {
     protected TestCleanup getCleanup() {
         return getCleanup(REALM_NAME);
     }
+
+    protected <T extends Comparable> void assertSingleStatistics(InfinispanStatistics stats, String key, Runnable testedCode, Function<T, Matcher<? super T>> matcherOnOldStat) {
+        stats.reset();
+
+        T oldStat = (T) stats.getSingleStatistics(key);
+        testedCode.run();
+
+        Retry.execute(() -> {
+            T newStat = (T) stats.getSingleStatistics(key);
+
+            Matcher<? super T> matcherInstance = matcherOnOldStat.apply(oldStat);
+            assertThat(newStat, matcherInstance);
+        }, 5, 200);
+    }
+
+    protected void assertStatistics(InfinispanStatistics stats, Runnable testedCode, BiConsumer<Map<String, Object>, Map<String, Object>> assertionOnStats) {
+        stats.reset();
+
+        Map<String, Object> oldStat = stats.getStatistics();
+        testedCode.run();
+
+        Retry.execute(() -> {
+            Map<String, Object> newStat = stats.getStatistics();
+            assertionOnStats.accept(oldStat, newStat);
+        }, 5, 200);
+    }
+
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java
index aa674ca..c88c0c1 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java
@@ -32,14 +32,21 @@ import org.jboss.arquillian.test.api.ArquillianResource;
 import org.junit.After;
 import org.junit.Before;
 
+import static org.hamcrest.Matchers.lessThan;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
 /**
  *
  * @author hmlnarik
  */
 public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest {
 
+    // Keep the following constants in sync with arquillian
+    public static final String QUALIFIER_NODE_BALANCER = "auth-server-balancer-cross-dc";
+
     @ArquillianResource
-    @LoadBalancer(value = "auth-server-balancer-cross-dc")
+    @LoadBalancer(value = QUALIFIER_NODE_BALANCER)
     protected LoadBalancerController loadBalancerCtrl;
 
     @ArquillianResource
@@ -103,6 +110,11 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest 
         return Keycloak.getInstance(node.getContextRoot() + "/auth", AuthRealm.MASTER, AuthRealm.ADMIN, AuthRealm.ADMIN, Constants.ADMIN_CLI_CLIENT_ID);
     }
 
+    /**
+     * Creates admin client directed to the given node.
+     * @param node
+     * @return
+     */
     protected Keycloak getAdminClientFor(ContainerInfo node) {
         Keycloak adminClient = backendAdminClients.get(node);
         if (adminClient == null && node.equals(suiteContext.getAuthServerInfo())) {
@@ -111,13 +123,17 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest 
         return adminClient;
     }
 
+    /**
+     * Disables routing requests to the given data center in the load balancer.
+     * @param dcIndex
+     */
     public void disableDcOnLoadBalancer(int dcIndex) {
         log.infof("Disabling load balancer for dc=%d", dcIndex);
         this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex).forEach(c -> loadBalancerCtrl.disableBackendNodeByName(c.getQualifier()));
     }
 
     /**
-     * Enables all started nodes in the given data center
+     * Enables routing requests to all started nodes to the given data center in the load balancer.
      * @param dcIndex
      */
     public void enableDcOnLoadBalancer(int dcIndex) {
@@ -132,11 +148,21 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest 
         }
     }
 
+    /**
+     * Disables routing requests to the given node within the given data center in the load balancer.
+     * @param dcIndex
+     * @param nodeIndex
+     */
     public void disableLoadBalancerNode(int dcIndex, int nodeIndex) {
         log.infof("Disabling load balancer for dc=%d, node=%d", dcIndex, nodeIndex);
         loadBalancerCtrl.disableBackendNodeByName(this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex).get(nodeIndex).getQualifier());
     }
 
+    /**
+     * Enables routing requests to the given node within the given data center in the load balancer.
+     * @param dcIndex
+     * @param nodeIndex
+     */
     public void enableLoadBalancerNode(int dcIndex, int nodeIndex) {
         log.infof("Enabling load balancer for dc=%d, node=%d", dcIndex, nodeIndex);
         final ContainerInfo backendNode = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex).get(nodeIndex);
@@ -149,11 +175,53 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest 
         loadBalancerCtrl.enableBackendNodeByName(backendNode.getQualifier());
     }
 
+    /**
+     * Starts a manually-controlled backend auth-server node in cross-DC scenario.
+     * @param dcIndex
+     * @param nodeIndex
+     * @return Started instance descriptor.
+     */
+    public ContainerInfo startBackendNode(int dcIndex, int nodeIndex) {
+        assertThat((Integer) dcIndex, lessThan(this.suiteContext.getDcAuthServerBackendsInfo().size()));
+        final List<ContainerInfo> dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex);
+        assertThat((Integer) nodeIndex, lessThan(dcNodes.size()));
+        ContainerInfo dcNode = dcNodes.get(nodeIndex);
+        assertTrue("Node " + dcNode.getQualifier() + " has to be controlled manually", dcNode.isManual());
+        containerController.start(dcNode.getQualifier());
+        return dcNode;
+    }
+
+    /**
+     * Stops a manually-controlled backend auth-server node in cross-DC scenario.
+     * @param dcIndex
+     * @param nodeIndex
+     * @return Stopped instance descriptor.
+     */
+    public ContainerInfo stopBackendNode(int dcIndex, int nodeIndex) {
+        assertThat((Integer) dcIndex, lessThan(this.suiteContext.getDcAuthServerBackendsInfo().size()));
+        final List<ContainerInfo> dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex);
+        assertThat((Integer) nodeIndex, lessThan(dcNodes.size()));
+        ContainerInfo dcNode = dcNodes.get(nodeIndex);
+        assertTrue("Node " + dcNode.getQualifier() + " has to be controlled manually", dcNode.isManual());
+        containerController.stop(dcNode.getQualifier());
+        return dcNode;
+    }
+
+    /**
+     * Returns stream of all nodes in the given dc that are started manually.
+     * @param dcIndex
+     * @return
+     */
     public Stream<ContainerInfo> getManuallyStartedBackendNodes(int dcIndex) {
         final List<ContainerInfo> dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex);
         return dcNodes.stream().filter(ContainerInfo::isManual);
     }
 
+    /**
+     * Returns stream of all nodes in the given dc that are started automatically.
+     * @param dcIndex
+     * @return
+     */
     public Stream<ContainerInfo> getAutomaticallyStartedBackendNodes(int dcIndex) {
         final List<ContainerInfo> dcNodes = this.suiteContext.getDcAuthServerBackendsInfo().get(dcIndex);
         return dcNodes.stream().filter(c -> ! c.isManual());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java
index 45e7571..dbef2fc 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java
@@ -17,16 +17,13 @@
 package org.keycloak.testsuite.crossdc;
 
 import org.keycloak.admin.client.resource.UserResource;
-import org.keycloak.events.admin.OperationType;
-import org.keycloak.events.admin.ResourceType;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
 import org.keycloak.models.UserModel;
 import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.testsuite.Retry;
 import org.keycloak.testsuite.admin.ApiUtil;
-import org.keycloak.testsuite.arquillian.ContainerInfo;
 import org.keycloak.testsuite.page.LoginPasswordUpdatePage;
 import org.keycloak.testsuite.pages.ErrorPage;
-import org.keycloak.testsuite.util.AdminEventPaths;
 import org.keycloak.testsuite.util.GreenMailRule;
 import org.keycloak.testsuite.util.MailUtils;
 import java.io.IOException;
@@ -36,12 +33,20 @@ import javax.mail.MessagingException;
 import javax.mail.internet.MimeMessage;
 import javax.ws.rs.core.Response;
 import org.jboss.arquillian.graphene.page.Page;
-import org.jboss.arquillian.test.api.ArquillianResource;
 import org.junit.Assert;
 import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import static org.junit.Assert.assertEquals;
+import org.keycloak.testsuite.arquillian.annotation.JmxInfinispanCacheStatistics;
+import org.keycloak.testsuite.arquillian.annotation.JmxInfinispanChannelStatistics;
+import org.keycloak.testsuite.arquillian.InfinispanStatistics;
+import org.keycloak.testsuite.arquillian.InfinispanStatistics.Constants;
+import java.util.concurrent.TimeUnit;
+import org.hamcrest.Matchers;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
 
 /**
  *
@@ -69,7 +74,16 @@ public class ActionTokenCrossDCTest extends AbstractAdminCrossDCTest {
     }
 
     @Test
-    public void sendResetPasswordEmailSuccessWorksInCrossDc() throws IOException, MessagingException {
+    public void sendResetPasswordEmailSuccessWorksInCrossDc(
+      @JmxInfinispanCacheStatistics(dcIndex=0, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.ACTION_TOKEN_CACHE) InfinispanStatistics cacheDc0Node0Statistics,
+      @JmxInfinispanCacheStatistics(dcIndex=0, dcNodeIndex=1, cacheName=InfinispanConnectionProvider.ACTION_TOKEN_CACHE) InfinispanStatistics cacheDc0Node1Statistics,
+      @JmxInfinispanCacheStatistics(dcIndex=1, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.ACTION_TOKEN_CACHE) InfinispanStatistics cacheDc1Node0Statistics,
+      @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
+        startBackendNode(0, 1);
+        cacheDc0Node1Statistics.waitToBecomeAvailable(10, TimeUnit.SECONDS);
+
+        Comparable originalNumberOfEntries = cacheDc0Node0Statistics.getSingleStatistics(Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
+
         UserRepresentation userRep = new UserRepresentation();
         userRep.setEnabled(true);
         userRep.setUsername("user1");
@@ -88,21 +102,33 @@ public class ActionTokenCrossDCTest extends AbstractAdminCrossDCTest {
 
         String link = MailUtils.getPasswordResetEmailLink(message);
 
-        driver.navigate().to(link);
+        Retry.execute(() -> channelStatisticsCrossDc.reset(), 3, 100);
+
+        assertSingleStatistics(cacheDc0Node0Statistics, Constants.STAT_CACHE_NUMBER_OF_ENTRIES,
+          () -> driver.navigate().to(link),
+          Matchers::is
+        );
 
         passwordUpdatePage.assertCurrent();
 
-        passwordUpdatePage.changePassword("new-pass", "new-pass");
+        // Verify that there was at least one message sent via the channel
+        assertSingleStatistics(channelStatisticsCrossDc, Constants.STAT_CHANNEL_SENT_MESSAGES,
+          () -> passwordUpdatePage.changePassword("new-pass", "new-pass"),
+          old -> greaterThan((Comparable) 0l)
+        );
+
+        // Verify that the caches are synchronized
+        assertThat(cacheDc0Node0Statistics.getSingleStatistics(Constants.STAT_CACHE_NUMBER_OF_ENTRIES), greaterThan(originalNumberOfEntries));
+        assertThat(cacheDc0Node0Statistics.getSingleStatistics(Constants.STAT_CACHE_NUMBER_OF_ENTRIES),
+                is(cacheDc1Node0Statistics.getSingleStatistics(Constants.STAT_CACHE_NUMBER_OF_ENTRIES)));
 
         assertEquals("Your account has been updated.", driver.getTitle());
 
         disableDcOnLoadBalancer(0);
         enableDcOnLoadBalancer(1);
 
-        Retry.execute(() -> {
-            driver.navigate().to(link);
-            errorPage.assertCurrent();
-        }, 3, 400);
+        driver.navigate().to(link);
+        errorPage.assertCurrent();
     }
 
     @Ignore("KEYCLOAK-5030")
@@ -144,9 +170,10 @@ public class ActionTokenCrossDCTest extends AbstractAdminCrossDCTest {
               loadBalancerCtrl.enableBackendNodeByName(c.getQualifier());
           });
 
-        driver.navigate().to(link);
-
-        errorPage.assertCurrent();
+        Retry.execute(() -> {
+            driver.navigate().to(link);
+            errorPage.assertCurrent();
+        }, 3, 400);
     }
 
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java
index b29abc1..677430d 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java
@@ -627,12 +627,13 @@ public class ExportImportUtil {
         assertPredicate(scopes, scopePredicates);
 
         List<PolicyRepresentation> policies = authzResource.policies().policies();
-        Assert.assertEquals(13, policies.size());
+        Assert.assertEquals(14, policies.size());
         List<Predicate<PolicyRepresentation>> policyPredicates = new ArrayList<>();
         policyPredicates.add(policyRepresentation -> "Any Admin Policy".equals(policyRepresentation.getName()));
         policyPredicates.add(policyRepresentation -> "Any User Policy".equals(policyRepresentation.getName()));
         policyPredicates.add(representation -> "Client and Realm Role Policy".equals(representation.getName()));
         policyPredicates.add(representation -> "Client Test Policy".equals(representation.getName()));
+        policyPredicates.add(representation -> "Group Policy Test".equals(representation.getName()));
         policyPredicates.add(policyRepresentation -> "Only Premium User Policy".equals(policyRepresentation.getName()));
         policyPredicates.add(policyRepresentation -> "wburke policy".equals(policyRepresentation.getName()));
         policyPredicates.add(policyRepresentation -> "All Users Policy".equals(policyRepresentation.getName()));
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java
index 5c9ff74..2d92719 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java
@@ -16,19 +16,30 @@
  */
 package org.keycloak.testsuite.i18n;
 
+import java.util.Arrays;
+
 import org.apache.http.impl.client.DefaultHttpClient;
 import org.jboss.resteasy.client.jaxrs.ResteasyClient;
 import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
 import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient4Engine;
 import org.junit.Assert;
 import org.junit.Test;
+import org.keycloak.OAuth2Constants;
 import org.keycloak.adapters.HttpClientBuilder;
+import org.keycloak.admin.client.resource.UserResource;
+import org.keycloak.models.UserModel;
 import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
 import org.keycloak.testsuite.pages.LoginPage;
 
 import javax.ws.rs.core.Response;
 import org.jboss.arquillian.graphene.page.Page;
 import org.keycloak.testsuite.ProfileAssume;
+import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
+import org.keycloak.testsuite.pages.OAuthGrantPage;
 import org.keycloak.testsuite.util.IdentityProviderBuilder;
 
 /**
@@ -38,8 +49,18 @@ import org.keycloak.testsuite.util.IdentityProviderBuilder;
 public class LoginPageTest extends AbstractI18NTest {
 
     @Page
+    protected AppPage appPage;
+
+    @Page
     protected LoginPage loginPage;
 
+    @Page
+    protected LoginPasswordUpdatePage changePasswordPage;
+
+    @Page
+    protected OAuthGrantPage grantPage;
+
+
     @Override
     public void configureTestRealm(RealmRepresentation testRealm) {
         testRealm.addIdentityProvider(IdentityProviderBuilder.create()
@@ -63,11 +84,7 @@ public class LoginPageTest extends AbstractI18NTest {
         loginPage.open();
         Assert.assertEquals("English", loginPage.getLanguageDropdownText());
 
-        loginPage.openLanguage("Deutsch");
-        Assert.assertEquals("Deutsch", loginPage.getLanguageDropdownText());
-
-        loginPage.openLanguage("English");
-        Assert.assertEquals("English", loginPage.getLanguageDropdownText());
+        switchLanguageToGermanAndBack("Username or email", "Benutzername oder E-Mail", loginPage);
     }
 
     @Test
@@ -109,6 +126,8 @@ public class LoginPageTest extends AbstractI18NTest {
 
         response = client.target(driver.getCurrentUrl()).request().acceptLanguage("en").get();
         Assert.assertTrue(response.readEntity(String.class).contains("Log in to test"));
+
+        client.close();
     }
 
     @Test
@@ -119,4 +138,73 @@ public class LoginPageTest extends AbstractI18NTest {
         Assert.assertEquals("MyOIDC", loginPage.findSocialButton("myoidc").getText());
 
     }
+
+
+    // KEYCLOAK-3887
+    @Test
+    public void languageChangeRequiredActions() {
+        UserResource user = ApiUtil.findUserByUsernameId(testRealm(), "test-user@localhost");
+        UserRepresentation userRep = user.toRepresentation();
+        userRep.setRequiredActions(Arrays.asList(UserModel.RequiredAction.UPDATE_PASSWORD.toString()));
+        user.update(userRep);
+
+        loginPage.open();
+
+        loginPage.login("test-user@localhost", "password");
+        changePasswordPage.assertCurrent();
+        Assert.assertEquals("English", changePasswordPage.getLanguageDropdownText());
+
+        // Switch language
+        switchLanguageToGermanAndBack("Update password", "Passwort aktualisieren", changePasswordPage);
+
+        // Update password
+        changePasswordPage.changePassword("password", "password");
+
+        Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
+    }
+
+
+    // KEYCLOAK-3887
+    @Test
+    public void languageChangeConsentScreen() {
+        // Set client, which requires consent
+        oauth.clientId("third-party");
+
+        loginPage.open();
+
+        loginPage.login("test-user@localhost", "password");
+
+        grantPage.assertCurrent();
+        Assert.assertEquals("English", grantPage.getLanguageDropdownText());
+
+        // Switch language
+        switchLanguageToGermanAndBack("Do you grant these access privileges?", "Wollen Sie diese Zugriffsrechte", changePasswordPage);
+
+        // Confirm grant
+        grantPage.accept();
+
+        Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
+
+        // Revert client
+        oauth.clientId("test-app");
+    }
+
+
+    private void switchLanguageToGermanAndBack(String expectedEnglishMessage, String expectedGermanMessage, LanguageComboboxAwarePage page) {
+        // Switch language to Deutsch
+        page.openLanguage("Deutsch");
+        Assert.assertEquals("Deutsch", page.getLanguageDropdownText());
+        String pageSource = driver.getPageSource();
+        Assert.assertFalse(pageSource.contains(expectedEnglishMessage));
+        Assert.assertTrue(pageSource.contains(expectedGermanMessage));
+
+        // Revert language
+        page.openLanguage("English");
+        Assert.assertEquals("English", page.getLanguageDropdownText());
+        pageSource = driver.getPageSource();
+        Assert.assertTrue(pageSource.contains(expectedEnglishMessage));
+        Assert.assertFalse(pageSource.contains(expectedGermanMessage));
+    }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthProofKeyForCodeExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthProofKeyForCodeExchangeTest.java
index a72aa3a..20757c1 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthProofKeyForCodeExchangeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthProofKeyForCodeExchangeTest.java
@@ -444,14 +444,10 @@ public class OAuthProofKeyForCodeExchangeTest extends AbstractKeycloakTest {
     
     private String generateS256CodeChallenge(String codeVerifier) throws Exception {
         MessageDigest md = MessageDigest.getInstance("SHA-256");
-        md.update(codeVerifier.getBytes());
-        StringBuilder sb = new StringBuilder();
-        for (byte b : md.digest()) {
-            String hex = String.format("%02x", b);
-            sb.append(hex);
-        }
-        String codeChallenge = Base64Url.encode(sb.toString().getBytes());
-    	return codeChallenge;
+        md.update(codeVerifier.getBytes("ISO_8859_1"));
+        byte[] digestBytes = md.digest();
+        String codeChallenge = Base64Url.encode(digestBytes);
+        return codeChallenge;
     }
  
     private void expectSuccessfulResponseFromTokenEndpoint(String codeId, String sessionId, String code)  throws Exception {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java
index b4f3130..842e406 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java
@@ -18,7 +18,9 @@
 package org.keycloak.testsuite.util;
 
 import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.ProtocolMapperRepresentation;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.LinkedList;
@@ -175,7 +177,15 @@ public class ClientBuilder {
     }
 
     public ClientBuilder authorizationServicesEnabled(boolean enable) {
-        rep.setAuthorizationServicesEnabled(true);
+        rep.setAuthorizationServicesEnabled(enable);
+        return this;
+    }
+
+    public ClientBuilder protocolMapper(ProtocolMapperRepresentation... mappers) {
+        if (rep.getProtocolMappers() == null) {
+            rep.setProtocolMappers(new ArrayList<>());
+        }
+        rep.getProtocolMappers().addAll(Arrays.asList(mappers));
         return this;
     }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
index ecf986f..aa8649e 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
@@ -49,7 +49,7 @@
     
     <container qualifier="auth-server-undertow" mode="suite" >
         <configuration>
-            <property name="enabled">${auth.server.undertow}</property>
+            <property name="enabled">${auth.server.undertow} &amp;&amp; ! ${auth.server.undertow.crossdc}</property>
             <property name="bindAddress">localhost</property>
             <property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
             <property name="bindHttpPort">${auth.server.http.port}</property>
@@ -169,12 +169,12 @@
 
     <!-- Cross DC with embedded undertow. Node numbering is [centre #].[node #] -->
     <group qualifier="auth-server-undertow-cross-dc">
-        <container qualifier="cache-server-cross-dc" mode="suite" >
+        <container qualifier="cache-server-cross-dc-1" mode="suite" >
             <configuration>
                 <property name="enabled">${auth.server.undertow.crossdc}</property>
                 <property name="adapterImplClass">org.jboss.as.arquillian.container.managed.ManagedDeployableContainer</property>
                 <property name="jbossHome">${cache.server.home}</property>
-                <property name="serverConfig">standalone.xml</property>
+                <property name="serverConfig">clustered.xml</property>
                 <property name="jbossArguments">
                     -Djboss.socket.binding.port-offset=${cache.server.port.offset}
                     -Djboss.default.multicast.address=234.56.78.99
@@ -192,30 +192,54 @@
             </configuration>
         </container>
 
+        <container qualifier="cache-server-cross-dc-2" mode="suite" >
+            <configuration>
+                <property name="enabled">${auth.server.undertow.crossdc}</property>
+                <property name="adapterImplClass">org.jboss.as.arquillian.container.managed.ManagedDeployableContainer</property>
+                <property name="jbossHome">${cache.server.home}</property>
+                <property name="setupCleanServerBaseDir">true</property>
+                <property name="cleanServerBaseDir">${cache.server.home}/standalone-dc-2</property>
+                <property name="serverConfig">clustered.xml</property>
+                <property name="jbossArguments">
+                    -Djboss.socket.binding.port-offset=${cache.server.2.port.offset}
+                    -Djboss.default.multicast.address=234.56.78.99
+                    -Djboss.node.name=cache-server-dc-2
+                    ${adapter.test.props}
+                    ${auth.server.profile}
+                </property>
+                <property name="javaVmArguments">
+                    ${auth.server.memory.settings}
+                    -Djava.net.preferIPv4Stack=true
+                </property>
+                <property name="outputToConsole">${cache.server.console.output}</property>
+                <property name="managementPort">${cache.server.2.management.port}</property>
+                <property name="startupTimeoutInSeconds">${auth.server.jboss.startup.timeout}</property>
+            </configuration>
+        </container>
+
         <container qualifier="auth-server-balancer-cross-dc" mode="suite" >
             <configuration>
                 <property name="enabled">${auth.server.undertow.crossdc}</property>
                 <property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.lb.SimpleUndertowLoadBalancerContainer</property>
                 <property name="bindAddress">localhost</property>
                 <property name="bindHttpPort">${auth.server.http.port}</property>
-                <property name="bindHttpPortOffset">5</property>
                 <property name="nodes">auth-server-undertow-cross-dc-0.1=http://localhost:8101,auth-server-undertow-cross-dc-0.2-manual=http://localhost:8102,auth-server-undertow-cross-dc-1.1=http://localhost:8111,auth-server-undertow-cross-dc-1.2-manual=http://localhost:8112</property>
             </configuration>
         </container>
 
-        <container qualifier="auth-server-undertow-cross-dc-0.1" mode="suite" >
+        <container qualifier="auth-server-undertow-cross-dc-0_1" mode="suite" >
             <configuration>
                 <property name="enabled">${auth.server.undertow.crossdc}</property>
                 <property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
                 <property name="bindAddress">localhost</property>
                 <property name="bindHttpPort">${auth.server.http.port}</property>
                 <property name="bindHttpPortOffset">-79</property>
-                <property name="route">auth-server-undertow-cross-dc-0.1</property>
+                <property name="route">auth-server-undertow-cross-dc-0_1</property>
                 <property name="remoteMode">${undertow.remote}</property>
                 <property name="dataCenter">0</property>
                 <property name="keycloakConfigPropertyOverrides">{
                     "keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.1",
-                    "keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-0.1",
+                    "keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-0_1",
                     "keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}",
                     "keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}",
                     "keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}",
@@ -226,19 +250,19 @@
                 }</property>
             </configuration>
         </container>
-        <container qualifier="auth-server-undertow-cross-dc-0.2-manual" mode="manual" >
+        <container qualifier="auth-server-undertow-cross-dc-0_2-manual" mode="manual" >
             <configuration>
                 <property name="enabled">${auth.server.undertow.crossdc}</property>
                 <property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
                 <property name="bindAddress">localhost</property>
                 <property name="bindHttpPort">${auth.server.http.port}</property>
                 <property name="bindHttpPortOffset">-78</property>
-                <property name="route">auth-server-undertow-cross-dc-0.2</property>
+                <property name="route">auth-server-undertow-cross-dc-0_2-manual</property>
                 <property name="remoteMode">${undertow.remote}</property>
                 <property name="dataCenter">0</property>
                 <property name="keycloakConfigPropertyOverrides">{
                     "keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.1",
-                    "keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-0.2",
+                    "keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-0_2-manual",
                     "keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}",
                     "keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}",
                     "keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}",
@@ -250,22 +274,22 @@
             </configuration>
         </container>
 
-        <container qualifier="auth-server-undertow-cross-dc-1.1" mode="suite" >
+        <container qualifier="auth-server-undertow-cross-dc-1_1" mode="suite" >
             <configuration>
                 <property name="enabled">${auth.server.undertow.crossdc}</property>
                 <property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
                 <property name="bindAddress">localhost</property>
                 <property name="bindHttpPort">${auth.server.http.port}</property>
                 <property name="bindHttpPortOffset">-69</property>
-                <property name="route">auth-server-undertow-cross-dc-1.1</property>
+                <property name="route">auth-server-undertow-cross-dc-1_1</property>
                 <property name="remoteMode">${undertow.remote}</property>
                 <property name="dataCenter">1</property>
                 <property name="keycloakConfigPropertyOverrides">{
                     "keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.2",
-                    "keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-1.1",
+                    "keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-1_1",
                     "keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}",
                     "keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}",
-                    "keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}",
+                    "keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort.2:11222}",
                     "keycloak.connectionsInfinispan.remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:true}",
                     "keycloak.connectionsJpa.url": "${keycloak.connectionsJpa.url.crossdc:jdbc:h2:mem:test-dc-shared}",
                     "keycloak.connectionsJpa.driver": "${keycloak.connectionsJpa.driver.crossdc:org.h2.Driver}",
@@ -273,22 +297,22 @@
                 }</property>
             </configuration>
         </container>
-        <container qualifier="auth-server-undertow-cross-dc-1.2-manual" mode="manual" >
+        <container qualifier="auth-server-undertow-cross-dc-1_2-manual" mode="manual" >
             <configuration>
                 <property name="enabled">${auth.server.undertow.crossdc}</property>
                 <property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
                 <property name="bindAddress">localhost</property>
                 <property name="bindHttpPort">${auth.server.http.port}</property>
                 <property name="bindHttpPortOffset">-68</property>
-                <property name="route">auth-server-undertow-cross-dc-1.2</property>
+                <property name="route">auth-server-undertow-cross-dc-1_2-manual</property>
                 <property name="remoteMode">${undertow.remote}</property>
                 <property name="dataCenter">1</property>
                 <property name="keycloakConfigPropertyOverrides">{
                     "keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.2",
-                    "keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-1.2",
+                    "keycloak.connectionsInfinispan.nodeName": "auth-server-undertow-cross-dc-1_2-manual",
                     "keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}",
                     "keycloak.connectionsInfinispan.remoteStoreServer": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}",
-                    "keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}",
+                    "keycloak.connectionsInfinispan.remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort.2:11222}",
                     "keycloak.connectionsInfinispan.remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:true}",
                     "keycloak.connectionsJpa.url": "${keycloak.connectionsJpa.url.crossdc:jdbc:h2:mem:test-dc-shared}",
                     "keycloak.connectionsJpa.driver": "${keycloak.connectionsJpa.driver.crossdc:org.h2.Driver}",
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 2c80ddd..7a0e6b6 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
@@ -85,7 +85,7 @@
 
     "connectionsJpa": {
         "default": {
-            "url": "${keycloak.connectionsJpa.url:jdbc:h2:mem:test}",
+            "url": "${keycloak.connectionsJpa.url:jdbc:h2:mem:test;MVCC=TRUE}",
             "driver": "${keycloak.connectionsJpa.driver:org.h2.Driver}",
             "driverDialect": "${keycloak.connectionsJpa.driverDialect:}",
             "user": "${keycloak.connectionsJpa.user:sa}",
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json
index 5f84e38..fb1a7e0 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json
@@ -71,6 +71,50 @@
             }
         }
     ],
+    "groups": [
+        {
+            "name": "Group A",
+            "path": "/Group A",
+            "attributes": {
+                "topAttribute": [
+                    "true"
+                ]
+            },
+            "subGroups": [
+                {
+                    "name": "Group B",
+                    "path": "/Group A/Group B",
+                    "attributes": {
+                        "level2Attribute": [
+                            "true"
+                        ]
+                    },
+                    "subGroups": []
+                }
+            ]
+        },
+        {
+            "name": "Group C",
+            "path": "/Group C",
+            "attributes": {
+                "topAttribute": [
+                    "true"
+                ]
+            },
+            "subGroups": [
+                {
+                    "name": "Group D",
+                    "path": "/Group C/Group D",
+                    "attributes": {
+                        "level2Attribute": [
+                            "true"
+                        ]
+                    },
+                    "subGroups": []
+                }
+            ]
+        }
+    ],
     "users": [
         {
             "username": "wburke",
@@ -299,6 +343,14 @@
                         }
                     },
                     {
+                        "name": "Group Policy Test",
+                        "type": "group",
+                        "config": {
+                            "groupsClaim": "groups",
+                            "groups": "[{\"path\":\"/Group A\",\"extendChildren\":true},{\"path\":\"/Group A/Group B\",\"extendChildren\":false},{\"path\":\"/Group C/Group D\",\"extendChildren\":true}]"
+                        }
+                    },
+                    {
                         "name": "Only Premium User Policy",
                         "description": "Defines that only premium users can do something",
                         "type": "role",
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/GroupPolicy.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/GroupPolicy.java
new file mode 100644
index 0000000..2fd68f4
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/GroupPolicy.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.console.page.clients.authorization.policy;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPolicy implements PolicyTypeUI {
+
+    @Page
+    private GroupPolicyForm form;
+
+    public GroupPolicyForm form() {
+        return form;
+    }
+
+    public GroupPolicyRepresentation toRepresentation() {
+        return form.toRepresentation();
+    }
+
+    public void update(GroupPolicyRepresentation expected) {
+        form().populate(expected);
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/GroupPolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/GroupPolicyForm.java
new file mode 100644
index 0000000..389a214
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/GroupPolicyForm.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.console.page.clients.authorization.policy;
+
+import static org.openqa.selenium.By.tagName;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import org.jboss.arquillian.drone.api.annotation.Drone;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.Logic;
+import org.keycloak.testsuite.page.Form;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.ui.Select;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPolicyForm extends Form {
+
+    @FindBy(id = "name")
+    private WebElement name;
+
+    @FindBy(id = "description")
+    private WebElement description;
+
+    @FindBy(id = "groupsClaim")
+    private WebElement groupsClaim;
+
+    @FindBy(id = "logic")
+    private Select logic;
+
+    @FindBy(xpath = "//i[contains(@class,'pficon-delete')]")
+    private WebElement deleteButton;
+
+    @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
+    private WebElement confirmDelete;
+
+    @FindBy(id = "selectGroup")
+    private WebElement selectGroupButton;
+
+    @Drone
+    private WebDriver driver;
+
+    public void populate(GroupPolicyRepresentation expected) {
+        setInputValue(name, expected.getName());
+        setInputValue(description, expected.getDescription());
+        setInputValue(groupsClaim, expected.getGroupsClaim());
+        logic.selectByValue(expected.getLogic().name());
+
+
+        for (GroupPolicyRepresentation.GroupDefinition definition : toRepresentation().getGroups()) {
+            boolean isExpected = false;
+
+            for (GroupPolicyRepresentation.GroupDefinition expectedDef : expected.getGroups()) {
+                if (definition.getPath().equals(expectedDef.getPath())) {
+                    isExpected = true;
+                    break;
+                }
+            }
+
+            if (!isExpected) {
+                unselect(definition.getPath());
+            }
+        }
+
+        for (GroupPolicyRepresentation.GroupDefinition definition : expected.getGroups()) {
+            String path = definition.getPath();
+            String groupName = path.substring(path.lastIndexOf('/') + 1);
+            WebElement element = driver.findElement(By.xpath("//span[text()='" + groupName + "']"));
+            element.click();
+            selectGroupButton.click();
+            driver.findElements(By.xpath("(//table[@id='selected-groups'])/tbody/tr")).stream()
+                    .filter(webElement -> webElement.findElements(tagName("td")).size() > 1)
+                    .map(webElement -> webElement.findElements(tagName("td")))
+                    .filter(tds -> tds.get(0).getText().equals(definition.getPath()))
+                    .forEach(tds -> {
+                        if (!tds.get(1).findElement(By.tagName("input")).isSelected()) {
+                            if (definition.isExtendChildren()) {
+                                tds.get(1).findElement(By.tagName("input")).click();
+                            }
+                        } else {
+                            if (!definition.isExtendChildren() && tds.get(1).findElement(By.tagName("input")).isSelected()) {
+                                tds.get(1).findElement(By.tagName("input")).click();
+                            }
+                        }
+                    });
+        }
+
+        save();
+    }
+
+    private void unselect(String path) {
+        for (WebElement webElement : driver.findElements(By.xpath("(//table[@id='selected-groups'])/tbody/tr"))) {
+            List<WebElement> tds = webElement.findElements(tagName("td"));
+
+            if (tds.size() > 1) {
+                if (tds.get(0).getText().equals(path)) {
+                    tds.get(2).findElement(By.tagName("button")).click();
+                    return;
+                }
+            }
+        }
+    }
+
+    public void delete() {
+        deleteButton.click();
+        confirmDelete.click();
+    }
+
+    public GroupPolicyRepresentation toRepresentation() {
+        GroupPolicyRepresentation representation = new GroupPolicyRepresentation();
+
+        representation.setName(getInputValue(name));
+        representation.setDescription(getInputValue(description));
+        representation.setGroupsClaim(getInputValue(groupsClaim));
+        representation.setLogic(Logic.valueOf(logic.getFirstSelectedOption().getText().toUpperCase()));
+        representation.setGroups(new HashSet<>());
+
+        driver.findElements(By.xpath("(//table[@id='selected-groups'])/tbody/tr")).stream()
+                .filter(webElement -> webElement.findElements(tagName("td")).size() > 1)
+                .forEach(webElement -> {
+                    List<WebElement> tds = webElement.findElements(tagName("td"));
+                    representation.addGroupPath(tds.get(0).getText(), tds.get(1).findElement(By.tagName("input")).isSelected());
+                });
+
+        return representation;
+    }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java
index af2a540..7be563e 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java
@@ -22,6 +22,7 @@ import org.jboss.arquillian.graphene.page.Page;
 import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
 import org.keycloak.representations.idm.authorization.AggregatePolicyRepresentation;
 import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
 import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
 import org.keycloak.representations.idm.authorization.PolicyRepresentation;
 import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
@@ -66,6 +67,9 @@ public class Policies extends Form {
     @Page
     private ClientPolicy clientPolicy;
 
+    @Page
+    private GroupPolicy groupPolicy;
+
     public PoliciesTable policies() {
         return table;
     }
@@ -103,6 +107,10 @@ public class Policies extends Form {
             clientPolicy.form().populate((ClientPolicyRepresentation) expected);
             clientPolicy.form().save();
             return (P) clientPolicy;
+        } else if ("group".equals(type)) {
+            groupPolicy.form().populate((GroupPolicyRepresentation) expected);
+            groupPolicy.form().save();
+            return (P) groupPolicy;
         }
 
         return null;
@@ -130,6 +138,8 @@ public class Policies extends Form {
                     rulePolicy.form().populate((RulePolicyRepresentation) representation);
                 } else if ("client".equals(type)) {
                     clientPolicy.form().populate((ClientPolicyRepresentation) representation);
+                } else if ("group".equals(type)) {
+                    groupPolicy.form().populate((GroupPolicyRepresentation) representation);
                 }
 
                 return;
@@ -158,6 +168,8 @@ public class Policies extends Form {
                     return (P) rulePolicy;
                 } else if ("client".equals(type)) {
                     return (P) clientPolicy;
+                } else if ("group".equals(type)) {
+                    return (P) groupPolicy;
                 }
             }
         }
@@ -187,6 +199,8 @@ public class Policies extends Form {
                     rulePolicy.form().delete();
                 } else if ("client".equals(type)) {
                     clientPolicy.form().delete();
+                } else if ("group".equals(type)) {
+                    groupPolicy.form().delete();
                 }
 
                 return;
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/GroupPolicyManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/GroupPolicyManagementTest.java
new file mode 100644
index 0000000..e8b05bf
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/GroupPolicyManagementTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.console.authorization;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.util.Arrays;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.admin.client.resource.RolesResource;
+import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.Logic;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.console.page.clients.authorization.policy.GroupPolicy;
+import org.keycloak.testsuite.console.page.clients.authorization.policy.RolePolicy;
+import org.keycloak.testsuite.console.page.clients.authorization.policy.UserPolicy;
+import org.keycloak.testsuite.util.GroupBuilder;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPolicyManagementTest extends AbstractAuthorizationSettingsTest {
+
+    @Before
+    public void configureTest() {
+        super.configureTest();
+        RealmResource realmResource = testRealmResource();
+        String groupAId = ApiUtil.getCreatedId(realmResource.groups().add(GroupBuilder.create().name("Group A").build()));
+        String groupBId = ApiUtil.getCreatedId(realmResource.groups().group(groupAId).subGroup(GroupBuilder.create().name("Group B").build()));
+        realmResource.groups().group(groupBId).subGroup(GroupBuilder.create().name("Group D").build());
+        realmResource.groups().group(groupBId).subGroup(GroupBuilder.create().name("Group E").build());
+        realmResource.groups().group(groupAId).subGroup(GroupBuilder.create().name("Group C").build());
+        realmResource.groups().add(GroupBuilder.create().name("Group F").build());
+    }
+
+    @Test
+    public void testUpdate() throws InterruptedException {
+        authorizationPage.navigateTo();
+        GroupPolicyRepresentation expected = new GroupPolicyRepresentation();
+
+        expected.setName("Test Group Policy");
+        expected.setDescription("description");
+        expected.setGroupsClaim("groups");
+        expected.addGroupPath("/Group A", true);
+        expected.addGroupPath("/Group A/Group B/Group D");
+        expected.addGroupPath("Group F");
+
+        expected = createPolicy(expected);
+
+        String previousName = expected.getName();
+
+        expected.setName("Changed Test Group Policy");
+        expected.setDescription("Changed description");
+        expected.setLogic(Logic.NEGATIVE);
+
+        authorizationPage.navigateTo();
+        authorizationPage.authorizationTabs().policies().update(previousName, expected);
+        assertAlertSuccess();
+
+        authorizationPage.navigateTo();
+        GroupPolicy actual = authorizationPage.authorizationTabs().policies().name(expected.getName());
+
+        assertPolicy(expected, actual);
+
+        expected.getGroups().clear();
+        expected.addGroupPath("/Group A", false);
+        expected.addGroupPath("/Group A/Group B/Group D");
+
+        authorizationPage.navigateTo();
+        authorizationPage.authorizationTabs().policies().update(expected.getName(), expected);
+        assertAlertSuccess();
+
+        authorizationPage.navigateTo();
+        actual = authorizationPage.authorizationTabs().policies().name(expected.getName());
+
+        assertPolicy(expected, actual);
+
+        expected.getGroups().clear();
+        expected.addGroupPath("/Group E");
+        expected.addGroupPath("/Group A/Group B", true);
+        expected.addGroupPath("/Group A/Group C");
+
+
+        authorizationPage.navigateTo();
+        authorizationPage.authorizationTabs().policies().update(expected.getName(), expected);
+        assertAlertSuccess();
+
+        authorizationPage.navigateTo();
+        actual = authorizationPage.authorizationTabs().policies().name(expected.getName());
+
+        assertPolicy(expected, actual);
+    }
+
+    @Test
+    public void testDelete() throws InterruptedException {
+        authorizationPage.navigateTo();
+        GroupPolicyRepresentation expected = new GroupPolicyRepresentation();
+
+        expected.setName("Test Delete Group Policy");
+        expected.setDescription("description");
+        expected.setGroupsClaim("groups");
+        expected.addGroupPath("/Group A", true);
+        expected.addGroupPath("/Group A/Group B/Group D");
+        expected.addGroupPath("Group F");
+
+        expected = createPolicy(expected);
+        authorizationPage.navigateTo();
+        authorizationPage.authorizationTabs().policies().delete(expected.getName());
+        assertAlertSuccess();
+        authorizationPage.navigateTo();
+        assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName()));
+    }
+
+    private GroupPolicyRepresentation createPolicy(GroupPolicyRepresentation expected) {
+        GroupPolicy policy = authorizationPage.authorizationTabs().policies().create(expected);
+        assertAlertSuccess();
+        return assertPolicy(expected, policy);
+    }
+
+    private GroupPolicyRepresentation assertPolicy(GroupPolicyRepresentation expected, GroupPolicy policy) {
+        GroupPolicyRepresentation actual = policy.toRepresentation();
+
+        assertEquals(expected.getName(), actual.getName());
+        assertEquals(expected.getDescription(), actual.getDescription());
+        assertEquals(expected.getLogic(), actual.getLogic());
+
+        assertNotNull(actual.getGroups());
+        assertEquals(expected.getGroups().size(), actual.getGroups().size());
+        assertEquals(0, actual.getGroups().stream().filter(actualDefinition -> !expected.getGroups().stream()
+                .filter(groupDefinition -> actualDefinition.getPath().contains(groupDefinition.getPath()) && actualDefinition.isExtendChildren() == groupDefinition.isExtendChildren())
+                .findFirst().isPresent())
+                .count());
+        return actual;
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml
index 5ff3e02..e5bba34 100755
--- a/testsuite/integration-arquillian/tests/pom.xml
+++ b/testsuite/integration-arquillian/tests/pom.xml
@@ -73,9 +73,12 @@
         <cache.server.home>${containers.home}/${cache.server.container}</cache.server.home>
         <cache.server.port.offset>1010</cache.server.port.offset>
         <cache.server.management.port>11000</cache.server.management.port>
+        <cache.server.2.port.offset>2010</cache.server.2.port.offset>
+        <cache.server.2.management.port>12000</cache.server.2.management.port>
         <cache.server.console.output>true</cache.server.console.output>
         <keycloak.connectionsInfinispan.remoteStoreServer>localhost</keycloak.connectionsInfinispan.remoteStoreServer>
         <keycloak.connectionsInfinispan.remoteStorePort>12232</keycloak.connectionsInfinispan.remoteStorePort>
+        <keycloak.connectionsInfinispan.remoteStorePort.2>13232</keycloak.connectionsInfinispan.remoteStorePort.2>
         <keycloak.connectionsJpa.url.crossdc>jdbc:h2:mem:test-dc-shared</keycloak.connectionsJpa.url.crossdc>
 
         <adapter.test.props/>
@@ -177,6 +180,23 @@
                     </executions>
                 </plugin>
                 <plugin>
+                    <artifactId>maven-antrun-plugin</artifactId>
+                    <executions>
+                        <execution>
+                            <id>clean-second-cache-server-arquillian-bug-workaround</id>
+                            <phase>process-test-resources</phase>
+                            <goals><goal>run</goal></goals>
+                            <configuration>
+                                <target>
+                                    <echo>${cache.server.home}/standalone-dc-2</echo>
+                                    <delete failonerror="false" dir="${cache.server.home}/standalone-dc-2" />
+                                    <mkdir dir="${cache.server.home}/standalone-dc-2/deployments" />
+                                </target>
+                            </configuration>
+                        </execution>
+                    </executions>
+                </plugin>
+                <plugin>
                     <artifactId>maven-surefire-plugin</artifactId>
                     <configuration>
                         <systemPropertyVariables>
@@ -252,8 +272,11 @@
                             <cache.server.home>${cache.server.home}</cache.server.home>
                             <cache.server.console.output>${cache.server.console.output}</cache.server.console.output>
                             <cache.server.management.port>${cache.server.management.port}</cache.server.management.port>
+                            <cache.server.2.port.offset>${cache.server.2.port.offset}</cache.server.2.port.offset>
+                            <cache.server.2.management.port>${cache.server.2.management.port}</cache.server.2.management.port>
 
                             <keycloak.connectionsInfinispan.remoteStorePort>${keycloak.connectionsInfinispan.remoteStorePort}</keycloak.connectionsInfinispan.remoteStorePort>
+                            <keycloak.connectionsInfinispan.remoteStorePort.2>${keycloak.connectionsInfinispan.remoteStorePort.2}</keycloak.connectionsInfinispan.remoteStorePort.2>
                             <keycloak.connectionsInfinispan.remoteStoreServer>${keycloak.connectionsInfinispan.remoteStoreServer}</keycloak.connectionsInfinispan.remoteStoreServer>
 
                             <keycloak.connectionsJpa.url.crossdc>${keycloak.connectionsJpa.url.crossdc}</keycloak.connectionsJpa.url.crossdc>
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 11fe2c5..94d214e 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -1209,6 +1209,13 @@ authz-policy-js-code.tooltip=The JavaScript code providing the conditions for th
 authz-aggregated=Aggregated
 authz-add-aggregated-policy=Add Aggregated Policy
 
+# Authz Group Policy Detail
+authz-add-group-policy=Add Group Policy
+authz-no-groups-assigned=No groups assigned.
+authz-policy-group-claim=Groups Claim
+authz-policy-group-claim.tooltip=A claim to use as the source for user’s group. If the claim is present it must be an array of strings.
+authz-policy-group-groups.tooltip=Specifies the groups allowed by this policy.
+
 # Authz Permission List
 authz-no-permissions-available=No permissions available.
 
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js
index 047fa49..a59ebf3 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js
@@ -324,7 +324,29 @@ module.config(['$routeProvider', function ($routeProvider) {
             }
         },
         controller: 'ResourceServerPolicyRoleDetailCtrl'
-    }).when('/realms/:realm/clients/:client/authz/resource-server/policy/js/create', {
+    }).when('/realms/:realm/clients/:client/authz/resource-server/policy/group/create', {
+          templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-group-detail.html',
+          resolve: {
+              realm: function (RealmLoader) {
+                  return RealmLoader();
+              },
+              client : function(ClientLoader) {
+                  return ClientLoader();
+              }
+          },
+          controller: 'ResourceServerPolicyGroupDetailCtrl'
+      }).when('/realms/:realm/clients/:client/authz/resource-server/policy/group/:id', {
+          templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-group-detail.html',
+          resolve: {
+              realm: function (RealmLoader) {
+                  return RealmLoader();
+              },
+              client : function(ClientLoader) {
+                  return ClientLoader();
+              }
+          },
+          controller: 'ResourceServerPolicyGroupDetailCtrl'
+      }).when('/realms/:realm/clients/:client/authz/resource-server/policy/js/create', {
         templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-js-detail.html',
         resolve: {
             realm: function (RealmLoader) {
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
index a2dfe0c..86444b8 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
@@ -1663,6 +1663,119 @@ module.controller('ResourceServerPolicyRoleDetailCtrl', function($scope, $route,
     }
 });
 
+module.controller('ResourceServerPolicyGroupDetailCtrl', function($scope, $route, realm, client, Client, Groups, Group, PolicyController) {
+    PolicyController.onInit({
+        getPolicyType : function() {
+            return "group";
+        },
+
+        onInit : function() {
+            $scope.tree = [];
+
+            Groups.query({realm: $route.current.params.realm}, function(groups) {
+                $scope.groups = groups;
+                $scope.groupList = [
+                    {"id" : "realm", "name": "Groups",
+                                "subGroups" : groups}
+                ];
+            });
+
+            var isLeaf = function(node) {
+                return node.id != "realm" && (!node.subGroups || node.subGroups.length == 0);
+            }
+
+            $scope.getGroupClass = function(node) {
+                if (node.id == "realm") {
+                    return 'pficon pficon-users';
+                }
+                if (isLeaf(node)) {
+                    return 'normal';
+                }
+                if (node.subGroups.length && node.collapsed) return 'collapsed';
+                if (node.subGroups.length && !node.collapsed) return 'expanded';
+                return 'collapsed';
+
+            }
+
+            $scope.getSelectedClass = function(node) {
+                if (node.selected) {
+                    return 'selected';
+                } else if ($scope.cutNode && $scope.cutNode.id == node.id) {
+                    return 'cut';
+                }
+                return undefined;
+            }
+
+            $scope.selectGroup = function(group) {
+                for (i = 0; i < $scope.selectedGroups.length; i++) {
+                    if ($scope.selectedGroups[i].id == group.id) {
+                        return
+                    }
+                }
+                $scope.selectedGroups.push({id: group.id, path: group.path});
+                $scope.changed = true;
+            }
+
+            $scope.extendChildren = function(group) {
+                $scope.changed = true;
+            }
+
+            $scope.removeFromList = function(group) {
+                var index = $scope.selectedGroups.indexOf(group);
+                if (index != -1) {
+                    $scope.selectedGroups.splice(index, 1);
+                    $scope.changed = true;
+                }
+            }
+        },
+
+        onInitCreate : function(policy) {
+            var selectedGroups = [];
+
+            $scope.selectedGroups = angular.copy(selectedGroups);
+
+            $scope.$watch('selectedGroups', function() {
+                if (!angular.equals($scope.selectedGroups, selectedGroups)) {
+                    $scope.changed = true;
+                } else {
+                    $scope.changed = false;
+                }
+            }, true);
+        },
+
+        onInitUpdate : function(policy) {
+            $scope.selectedGroups = policy.groups;
+
+            angular.forEach($scope.selectedGroups, function(group, index){
+               Group.get({realm: $route.current.params.realm, groupId: group.id}, function (existing) {
+                   group.path = existing.path;
+               });
+            });
+
+            $scope.$watch('selectedGroups', function() {
+                if (!$scope.changed) {
+                    return;
+                }
+                if (!angular.equals($scope.selectedGroups, selectedGroups)) {
+                    $scope.changed = true;
+                } else {
+                    $scope.changed = false;
+                }
+            }, true);
+        },
+
+        onUpdate : function() {
+            $scope.policy.groups = $scope.selectedGroups;
+            delete $scope.policy.config;
+        },
+
+        onCreate : function() {
+            $scope.policy.groups = $scope.selectedGroups;
+            delete $scope.policy.config;
+        }
+    }, realm, client, $scope);
+});
+
 module.controller('ResourceServerPolicyJSDetailCtrl', function($scope, $route, $location, realm, PolicyController, client) {
     PolicyController.onInit({
         getPolicyType : function() {
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-group-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-group-detail.html
new file mode 100644
index 0000000..61af0f1
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-group-detail.html
@@ -0,0 +1,124 @@
+<!--
+  ~ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+  ~ * and other contributors as indicated by the @author tags.
+  ~ *
+  ~ * Licensed under the Apache License, Version 2.0 (the "License");
+  ~ * you may not use this file except in compliance with the License.
+  ~ * You may obtain a copy of the License at
+  ~ *
+  ~ * http://www.apache.org/licenses/LICENSE-2.0
+  ~ *
+  ~ * Unless required by applicable law or agreed to in writing, software
+  ~ * distributed under the License is distributed on an "AS IS" BASIS,
+  ~ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ * See the License for the specific language governing permissions and
+  ~ * limitations under the License.
+  -->
+
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+    <ol class="breadcrumb">
+        <li><a href="#/realms/{{realm.realm}}/clients">{{:: 'clients' | translate}}</a></li>
+        <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
+        <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">{{:: 'authz-authorization' | translate}}</a></li>
+        <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/policy">{{:: 'authz-policies' | translate}}</a></li>
+        <li data-ng-show="create">{{:: 'authz-add-group-policy' | translate}}</li>
+        <li data-ng-hide="create">{{:: 'groups' | translate}}</li>
+        <li data-ng-hide="create">{{originalPolicy.name}}</li>
+    </ol>
+
+    <h1 data-ng-show="create">{{:: 'authz-add-group-policy' | translate}}</h1>
+    <h1 data-ng-hide="create">{{originalPolicy.name|capitalize}}<i class="pficon pficon-delete clickable" data-ng-show="!create"
+                                                         data-ng-click="remove()"></i></h1>
+
+    <form class="form-horizontal" name="groupPolicyForm" novalidate>
+        <fieldset class="border-top">
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="name">{{:: 'name' | translate}} <span class="required">*</span></label>
+                <div class="col-sm-6">
+                    <input class="form-control" type="text" id="name" name="name" data-ng-model="policy.name" autofocus required data-ng-blur="checkNewNameAvailability()">
+                </div>
+                <kc-tooltip>{{:: 'authz-policy-name.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="description">{{:: 'description' | translate}} </label>
+                <div class="col-sm-6">
+                    <input class="form-control" type="text" id="description" name="description" data-ng-model="policy.description">
+                </div>
+                <kc-tooltip>{{:: 'authz-policy-description.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="groupsClaim">{{:: 'authz-policy-group-claim' | translate}} <span class="required">*</span></label>
+                <div class="col-sm-6">
+                    <input class="form-control" type="text" id="groupsClaim" name="groupsClaim" data-ng-model="policy.groupsClaim" required>
+                </div>
+                <kc-tooltip>{{:: 'authz-policy-group-claim.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="selectedGroups">{{:: 'groups' | translate}} <span class="required">*</span></label>
+                <div class="col-md-6">
+                    <div tree-id="tree"
+                        angular-treeview="true"
+                        tree-model="groupList"
+                        node-id="id"
+                        node-label="name"
+                        node-children="subGroups" >
+                    </div>
+                    <button data-ng-click="selectGroup(tree.currentNode)" id="selectGroup" class="btn btn-primary" data-ng-disabled="tree.currentNode == null">Select</button>
+                    <input class="form-control" type="text" id="selectedGroups" name="selectedGroups" data-ng-model="noop" data-ng-required="selectedGroups.length <= 0" autofocus required data-ng-show="false">
+                </div>
+                <kc-tooltip>{{:: 'authz-policy-user-users.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group" data-ng-if="selectedGroups.length > 0">
+                <label class="col-md-2 control-label"></label>
+                <div class="col-md-5">
+                    <table class="table table-striped table-bordered" id="selected-groups">
+                        <thead>
+                            <tr>
+                                <th>{{:: 'path' | translate}}</th>
+                                <th class="col-sm-3">Extend to Children</th>
+                                <th>{{:: 'actions' | translate}}</th>
+                            </tr>
+                        </thead>
+                        <tbody>
+                            <tr ng-repeat="group in selectedGroups | orderBy:'name' track by $index">
+                                <td>{{group.path}}</td>
+                                <td>
+                                    <input type="checkbox" ng-model="group.extendChildren" id="{{role.id}}" data-ng-click="extendChildren()">
+                                </td>
+                                <td class="kc-action-cell">
+                                    <button class="btn btn-default btn-block btn-sm" ng-click="removeFromList(group);">{{:: 'remove' | translate}}</button>
+                                </td>
+                            </tr>
+                            <tr data-ng-show="!selectedGroups.length">
+                                <td class="text-muted" colspan="3">{{:: 'authz-no-groups-assigned' | translate}}</td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="logic">{{:: 'authz-policy-logic' | translate}}</label>
+
+                <div class="col-sm-1">
+                    <select class="form-control" id="logic"
+                            data-ng-model="policy.logic">
+                        <option value="POSITIVE">{{:: 'authz-policy-logic-positive' | translate}}</option>
+                        <option value="NEGATIVE">{{:: 'authz-policy-logic-negative' | translate}}</option>
+                    </select>
+                </div>
+
+                <kc-tooltip>{{:: 'authz-policy-logic.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <input type="hidden" data-ng-model="policy.type"/>
+        </fieldset>
+        <div class="form-group" data-ng-show="access.manageAuthorization">
+            <div class="col-md-10 col-md-offset-2">
+                <button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
+                <button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
+            </div>
+        </div>
+    </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file