keycloak-memoizeit

Changes

Details

diff --git a/examples/demo-template/angular2-product-app/src/main/webapp/app/keycloak.http.ts b/examples/demo-template/angular2-product-app/src/main/webapp/app/keycloak.http.ts
index a7af71a..028f256 100644
--- a/examples/demo-template/angular2-product-app/src/main/webapp/app/keycloak.http.ts
+++ b/examples/demo-template/angular2-product-app/src/main/webapp/app/keycloak.http.ts
@@ -47,7 +47,7 @@ export class KeycloakHttp extends Http {
             result.subscribe((response) => {
                 observer.next(response);
                 observer.complete();
-            });
+            }, (err) => observer.error(err));
         });
 
         return <Observable<Response>>Observable
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractRequestCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractRequestCmd.java
index 84d9579..81d893c 100644
--- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractRequestCmd.java
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractRequestCmd.java
@@ -73,6 +73,8 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd {
 
     String file;
 
+    String body;
+
     String fields;
 
     boolean printHeaders;
@@ -210,6 +212,10 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd {
             throw new IllegalArgumentException("Options --merge and --no-merge are mutually exclusive");
         }
 
+        if (body != null && file != null) {
+            throw new IllegalArgumentException("Options --body and --file are mutually exclusive");
+        }
+
         if (file == null && attrs.size() > 0 && !noMerge) {
             mergeMode = true;
         }
@@ -222,17 +228,17 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd {
         // see if Content-Type header is explicitly set to non-json value
         Header ctype = headers.get("content-type");
 
-        InputStream body = null;
+        InputStream content = null;
 
         CmdStdinContext<JsonNode> ctx = new CmdStdinContext<>();
 
         if (file != null) {
             if (ctype != null && !"application/json".equals(ctype.getValue())) {
                 if ("-".equals(file)) {
-                    body = System.in;
+                    content = System.in;
                 } else {
                     try {
-                        body = new BufferedInputStream(new FileInputStream(file));
+                        content = new BufferedInputStream(new FileInputStream(file));
                     } catch (FileNotFoundException e) {
                         throw new RuntimeException("File not found: " + file);
                     }
@@ -240,6 +246,8 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd {
             } else {
                 ctx = parseFileOrStdin(file);
             }
+        } else if (body != null) {
+            content = new ByteArrayInputStream(body.getBytes(Charset.forName("utf-8")));
         }
 
         ConfigData config = loadConfig();
@@ -304,15 +312,15 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd {
         }
 
         if (attrs.size() > 0) {
-            if (body != null) {
+            if (content != null) {
                 throw new RuntimeException("Can't set attributes on content of type other than application/json");
             }
 
             ctx = mergeAttributes(ctx, MAPPER.createObjectNode(), attrs);
         }
 
-        if (body == null && ctx.getContent() != null) {
-            body = new ByteArrayInputStream(ctx.getContent().getBytes(Charset.forName("utf-8")));
+        if (content == null && ctx.getContent() != null) {
+            content = new ByteArrayInputStream(ctx.getContent().getBytes(Charset.forName("utf-8")));
         }
 
         ReturnFields returnFields = null;
@@ -322,7 +330,7 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd {
         }
 
         // make sure content type is set
-        if (body != null) {
+        if (content != null) {
             headers.addIfMissing("Content-Type", "application/json");
         }
 
@@ -339,7 +347,7 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd {
 
         HeadersBodyStatus response;
         try {
-            response = HttpUtil.doRequest(httpVerb, resourceUrl, new HeadersBody(headers, body));
+            response = HttpUtil.doRequest(httpVerb, resourceUrl, new HeadersBody(headers, content));
         } catch (IOException e) {
             throw new RuntimeException("HTTP request error: " + e.getMessage(), e);
         }
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AddRolesCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AddRolesCmd.java
index fecd042..2ca6cd3 100644
--- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AddRolesCmd.java
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AddRolesCmd.java
@@ -67,6 +67,12 @@ public class AddRolesCmd extends AbstractAuthOptionsCmd {
     @Option(name = "gid", description = "Target group's 'id'")
     String gid;
 
+    @Option(name = "rname", description = "Composite role's 'name'")
+    String rname;
+
+    @Option(name = "rid", description = "Composite role's 'id'")
+    String rid;
+
     @Option(name = "cclientid", description = "Target client's 'clientId'")
     String cclientid;
 
@@ -116,19 +122,31 @@ public class AddRolesCmd extends AbstractAuthOptionsCmd {
             }
 
             if (roleNames.isEmpty() && roleIds.isEmpty()) {
-                throw new IllegalArgumentException("No role specified. Use --rolename or --roleid to specify roles");
+                throw new IllegalArgumentException("No role to add specified. Use --rolename or --roleid to specify roles to add");
             }
 
             if (cid != null && cclientid != null) {
                 throw new IllegalArgumentException("Incompatible options: --cid and --cclientid are mutually exclusive");
             }
 
+            if (rid != null && rname != null) {
+                throw new IllegalArgumentException("Incompatible options: --rid and --rname are mutually exclusive");
+            }
+
             if (isUserSpecified() && isGroupSpecified()) {
                 throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --gname / --gid / --gpath");
             }
 
-            if (!isUserSpecified() && !isGroupSpecified()) {
-                throw new IllegalArgumentException("No user nor group specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group");
+            if (isUserSpecified() && isCompositeRoleSpecified()) {
+                throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --rname / --rid");
+            }
+
+            if (isGroupSpecified() && isCompositeRoleSpecified()) {
+                throw new IllegalArgumentException("Incompatible options: --rname / --rid can't be used at the same time as --gname / --gid / --gpath");
+            }
+
+            if (!isUserSpecified() && !isGroupSpecified() && !isCompositeRoleSpecified()) {
+                throw new IllegalArgumentException("No user nor group nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role");
             }
 
 
@@ -204,9 +222,32 @@ public class AddRolesCmd extends AbstractAuthOptionsCmd {
                     GroupOperations.addRealmRoles(adminRoot, realm, auth, gid, new ArrayList<>(rolesToAdd));
                 }
 
-            } else {
+            } else if (isCompositeRoleSpecified()) {
+                if (rid == null) {
+                    rid = RoleOperations.getIdFromRoleName(adminRoot, realm, auth, rname);
+                }
+                if (isClientSpecified()) {
+                    // list client roles for a composite role
+                    if (cid == null) {
+                        cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
+                    }
 
-                throw new IllegalArgumentException("No user nor group specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group");
+                    List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
+                    Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
+
+                    // now add all the roles
+                    RoleOperations.addClientRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd));
+
+                } else {
+                    Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
+                            new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
+
+                    // now add all the roles
+                    RoleOperations.addRealmRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd));
+                }
+
+            } else {
+                throw new IllegalArgumentException("No user nor group, nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role");
             }
 
             return CommandResult.SUCCESS;
@@ -257,6 +298,9 @@ public class AddRolesCmd extends AbstractAuthOptionsCmd {
         return uid != null || uusername != null;
     }
 
+    private boolean isCompositeRoleSpecified() {
+        return rid != null || rname != null;
+    }
 
     @Override
     protected boolean nothingToDo() {
@@ -275,9 +319,10 @@ public class AddRolesCmd extends AbstractAuthOptionsCmd {
         StringWriter sb = new StringWriter();
         PrintWriter out = new PrintWriter(sb);
         out.println("Usage: " + CMD + " add-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]");
-        out.println("Usage: " + CMD + " add-roles (--gname NAME | --gpath PATH | --gid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]");
+        out.println("       " + CMD + " add-roles (--gname NAME | --gpath PATH | --gid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]");
+        out.println("       " + CMD + " add-roles (--rname ROLE_NAME | --rid ROLE_ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]");
         out.println();
-        out.println("Command to add realm or client roles to a user or group.");
+        out.println("Command to add realm or client roles to a user, a group or a composite role.");
         out.println();
         out.println("Use `" + CMD + " config credentials` to establish an authenticated session, or use CREDENTIALS OPTIONS");
         out.println("to perform one time authentication.");
@@ -285,7 +330,8 @@ public class AddRolesCmd extends AbstractAuthOptionsCmd {
         out.println("If client is specified using --cclientid or --cid then roles to add are client roles, otherwise they are realm roles.");
         out.println("Either a user, or a group needs to be specified. If user is specified using --uusername or --uid then roles are added");
         out.println("to a specific user. If group is specified using --gname, --gpath or --gid then roles are added to a specific group.");
-        out.println("One or more roles have to be specified using --rolename or --roleid so that they are added to a group or a user.");
+        out.println("If composite role is specified using --rname or --rid then roles are added to a specific composite role.");
+        out.println("One or more roles have to be specified using --rolename or --roleid so that they are added to a group, a user or a composite role.");
         out.println();
         out.println("Arguments:");
         out.println();
@@ -306,6 +352,8 @@ public class AddRolesCmd extends AbstractAuthOptionsCmd {
         out.println("                          to use --gid, or --gpath to specify the target group");
         out.println("    --gpath               Group's 'path' attribute");
         out.println("    --gid                 Group's 'id' attribute");
+        out.println("    --rname               Composite role's 'name' attribute");
+        out.println("    --rid                 Composite role's 'id' attribute");
         out.println("    --cclientid           Client's 'clientId' attribute");
         out.println("    --cid                 Client's 'id' attribute");
         out.println("    --rolename            Role's 'name' attribute");
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/CreateCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/CreateCmd.java
index 63591f1..86ca3f1 100644
--- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/CreateCmd.java
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/CreateCmd.java
@@ -34,9 +34,12 @@ import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
 @CommandDefinition(name = "create", description = "Command to create new resources")
 public class CreateCmd extends AbstractRequestCmd {
 
-    @Option(shortName = 'f', name = "file", description = "Read object from file or standard input if FILENAME is set to '-'", hasValue = true)
+    @Option(shortName = 'f', name = "file", description = "Read object from file or standard input if FILENAME is set to '-'")
     String file;
 
+    @Option(shortName = 'b', name = "body", description = "JSON object to be sent as-is or used as a template")
+    String body;
+
     @Option(shortName = 'F', name = "fields", description = "A pattern specifying which attributes of JSON response body to actually display as result - causes mismatch with Content-Length header", hasValue = true)
     String fields;
 
@@ -59,6 +62,7 @@ public class CreateCmd extends AbstractRequestCmd {
     void initOptions() {
         // set options on parent
         super.file = file;
+        super.body = body;
         super.fields = fields;
         super.printHeaders = printHeaders;
         super.returnId = returnId;
@@ -69,7 +73,7 @@ public class CreateCmd extends AbstractRequestCmd {
 
     @Override
     protected boolean nothingToDo() {
-        return noOptions() && file == null && (args == null || args.size() == 0);
+        return noOptions() && file == null && body == null && (args == null || args.size() == 0);
     }
 
     protected String suggestHelp() {
@@ -109,6 +113,7 @@ public class CreateCmd extends AbstractRequestCmd {
         out.println("    -s, --set NAME=VALUE      Set a specific attribute NAME to a specified value VALUE");
         out.println("    -d, --delete NAME         Remove a specific attribute NAME from JSON request body");
         out.println("    -f, --file FILENAME       Read object from file or standard input if FILENAME is set to '-'");
+        out.println("    -b, --body CONTENT        Content to be sent as-is or used as a JSON object template");
         out.println("    -q, --query NAME=VALUE    Add to request URI a NAME query parameter with value VALUE");
         out.println("    -h, --header NAME=VALUE   Set request header NAME to VALUE");
         out.println();
@@ -153,8 +158,19 @@ public class CreateCmd extends AbstractRequestCmd {
             out.println("  EOF");
         }
         out.println();
+        out.println("Create a new group using configuration JSON passed as 'body' argument:");
+        if (OS_ARCH.isWindows()) {
+            out.println("  " + PROMPT + " " + CMD + " create groups -r demorealm -b \"{ \\\"name\\\": \\\"Admins\\\" }\"");
+        } else {
+            out.println("  " + PROMPT + " " + CMD + " create groups -r demorealm -b '{ \"name\": \"Admins\" }'");
+        }
+        out.println();
         out.println("Create a client using file as a template, and override some attributes - return an 'id' of new client:");
-        out.println("  " + PROMPT + " " + CMD + " create clients -r demorealm -f my_client.json -s clientId=my_client2 -s 'redirectUris=[\"http://localhost:8980/myapp/*\"]' -i");
+        if (OS_ARCH.isWindows()) {
+            out.println("  " + PROMPT + " " + CMD + " create clients -r demorealm -f my_client.json -s clientId=my_client2 -s \"redirectUris=[\\\"http://localhost:8980/myapp/*\\\"]\" -i");
+        } else {
+            out.println("  " + PROMPT + " " + CMD + " create clients -r demorealm -f my_client.json -s clientId=my_client2 -s 'redirectUris=[\"http://localhost:8980/myapp/*\"]' -i");
+        }
         out.println();
         out.println("Create a new client role for client my_client in realm 'demorealm' (replace ID with output of previous example command):");
         out.println("  " + PROMPT + " " + CMD + " create clients/ID/roles -r demorealm -s name=client_role");
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/DeleteCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/DeleteCmd.java
index 7ef31e1..d84833d 100644
--- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/DeleteCmd.java
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/DeleteCmd.java
@@ -80,6 +80,7 @@ public class DeleteCmd extends CreateCmd {
         out.println("    -s, --set NAME=VALUE      Send a body with request - set a specific attribute NAME to a specified value VALUE");
         out.println("    -d, --delete NAME         Remove a specific attribute NAME from JSON request body");
         out.println("    -f, --file FILENAME       Send a body with request - read object from file or standard input if FILENAME is set to '-'");
+        out.println("    -b, --body CONTENT        Content to be sent as-is or used as a JSON object template");
         out.println("    -q, --query NAME=VALUE    Add to request URI a NAME query parameter with value VALUE");
         out.println("    -h, --header NAME=VALUE   Set request header NAME to VALUE");
         out.println();
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetRolesCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetRolesCmd.java
index 7b0fd5f..9a4fb65 100644
--- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetRolesCmd.java
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetRolesCmd.java
@@ -57,10 +57,10 @@ public class GetRolesCmd extends GetCmd {
     @Option(name = "cid", description = "Target client's 'id'")
     String cid;
 
-    @Option(name = "rolename", description = "Target role's 'name'")
+    @Option(name = "rname", description = "Composite role's 'name'")
     String rname;
 
-    @Option(name = "roleid", description = "Target role's 'id'")
+    @Option(name = "rid", description = "Composite role's 'id'")
     String rid;
 
     @Option(name = "gname", description = "Target group's 'name'")
@@ -72,6 +72,12 @@ public class GetRolesCmd extends GetCmd {
     @Option(name = "gid", description = "Target group's 'id'")
     String gid;
 
+    @Option(name = "rolename", description = "Target role's 'name'")
+    String rolename;
+
+    @Option(name = "roleid", description = "Target role's 'id'")
+    String roleid;
+
     @Option(name = "available", description = "List only available roles", hasValue = false)
     boolean available;
 
@@ -108,10 +114,14 @@ public class GetRolesCmd extends GetCmd {
             throw new IllegalArgumentException("Incompatible options: --gid, --gname and --gpath are mutually exclusive");
         }
 
-        if (rid != null && rname != null) {
+        if (roleid != null && rolename != null) {
             throw new IllegalArgumentException("Incompatible options: --roleid and --rolename are mutually exclusive");
         }
 
+        if (rid != null && rname != null) {
+            throw new IllegalArgumentException("Incompatible options: --rid and --rname are mutually exclusive");
+        }
+
         if (cid != null && cclientid != null) {
             throw new IllegalArgumentException("Incompatible options: --cid and --cclientid are mutually exclusive");
         }
@@ -120,6 +130,22 @@ public class GetRolesCmd extends GetCmd {
             throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --gname / --gid / --gpath");
         }
 
+        if (isUserSpecified() && isCompositeRoleSpecified()) {
+            throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --rname / --rid");
+        }
+
+        if (isGroupSpecified() && isCompositeRoleSpecified()) {
+            throw new IllegalArgumentException("Incompatible options: --rname / --rid can't be used at the same time as --gname / --gid / --gpath");
+        }
+
+        if (all && effective) {
+            throw new IllegalArgumentException("Incompatible options: --all can't be used at the same time as --effective");
+        }
+
+        if (all && available) {
+            throw new IllegalArgumentException("Incompatible options: --all can't be used at the same time as --available");
+        }
+
         super.processOptions(commandInvocation);
     }
 
@@ -167,7 +193,7 @@ public class GetRolesCmd extends GetCmd {
                 } else if (effective) {
                     super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/realm/composite");
                 } else {
-                    super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/realm");
+                    super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + (all ? "/role-mappings" : "/role-mappings/realm"));
                 }
             }
         } else if (isGroupSpecified()) {
@@ -195,9 +221,35 @@ public class GetRolesCmd extends GetCmd {
                 } else if (effective) {
                     super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/realm/composite");
                 } else {
-                    super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/realm");
+                    super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + (all ? "/role-mappings" : "/role-mappings/realm"));
                 }
             }
+        } else if (isCompositeRoleSpecified()) {
+            String uri = rname != null ? "roles/" + rname : "roles-by-id/" + rid;
+
+            if (isClientSpecified()) {
+                if (cid == null) {
+                    cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
+                }
+                if (available) {
+                    throw new IllegalArgumentException("Option --available not supported with composite roles. Try '" + CMD + " get-roles --cid " + cid + "' for full list of client roles for that client");
+                }
+                if (effective) {
+                    throw new IllegalArgumentException("Option --effective not supported with composite roles.");
+                }
+                uri += "/composites/clients/" + cid;
+            } else {
+                if (available) {
+                    throw new IllegalArgumentException("Option --available not supported with composite roles. Try '" + CMD + " get-roles' for full list of realm roles");
+                }
+                if (effective) {
+                    throw new IllegalArgumentException("Option --effective not supported with composite roles.");
+                }
+
+                uri += all ? "/composites" : "/composites/realm";
+            }
+            super.url = composeResourceUrl(adminRoot, realm, uri);
+
         } else if (isClientSpecified()) {
             if (cid == null) {
                 cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
@@ -205,10 +257,10 @@ public class GetRolesCmd extends GetCmd {
 
             if (isRoleSpecified()) {
                 // get specific client role
-                if (rname == null) {
-                    rname = RoleOperations.getClientRoleNameFromId(adminRoot, realm, auth, cid, rid);
+                if (rolename == null) {
+                    rolename = RoleOperations.getClientRoleNameFromId(adminRoot, realm, auth, cid, roleid);
                 }
-                super.url = composeResourceUrl(adminRoot, realm, "clients/" + cid + "/roles/" + rname);
+                super.url = composeResourceUrl(adminRoot, realm, "clients/" + cid + "/roles/" + rolename);
             } else {
                 // list defined client roles
                 super.url = composeResourceUrl(adminRoot, realm, "clients/" + cid + "/roles");
@@ -216,10 +268,10 @@ public class GetRolesCmd extends GetCmd {
         } else {
             if (isRoleSpecified()) {
                 // get specific realm role
-                if (rname == null) {
-                    rname = RoleOperations.getClientRoleNameFromId(adminRoot, realm, auth, cid, rid);
+                if (rolename == null) {
+                    rolename = RoleOperations.getClientRoleNameFromId(adminRoot, realm, auth, cid, roleid);
                 }
-                super.url = composeResourceUrl(adminRoot, realm, "roles/" + rname);
+                super.url = composeResourceUrl(adminRoot, realm, "roles/" + rolename);
             } else {
                 // list defined realm roles
                 super.url = composeResourceUrl(adminRoot, realm, "roles");
@@ -230,7 +282,7 @@ public class GetRolesCmd extends GetCmd {
     }
 
     private boolean isRoleSpecified() {
-        return rid != null || rname != null;
+        return roleid != null || rolename != null;
     }
 
     private boolean isClientSpecified() {
@@ -241,6 +293,10 @@ public class GetRolesCmd extends GetCmd {
         return gid != null || gname != null || gpath != null;
     }
 
+    private boolean isCompositeRoleSpecified() {
+        return rid != null || rname != null;
+    }
+
     private boolean isUserSpecified() {
         return uid != null || uusername != null;
     }
@@ -261,10 +317,11 @@ public class GetRolesCmd extends GetCmd {
         StringWriter sb = new StringWriter();
         PrintWriter out = new PrintWriter(sb);
         out.println("Usage: " + CMD + " get-roles [--cclientid CLIENT_ID | --cid ID] [ARGUMENTS]");
-        out.println("Usage: " + CMD + " get-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] [--available | --effective] (ARGUMENTS)");
-        out.println("Usage: " + CMD + " get-roles (--gname NAME | --gpath PATH | --gid ID) [--cclientid CLIENT_ID | --cid ID] [--available | --effective] [ARGUMENTS]");
+        out.println("       " + CMD + " get-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] [--available | --effective | --all] (ARGUMENTS)");
+        out.println("       " + CMD + " get-roles (--gname NAME | --gpath PATH | --gid ID) [--cclientid CLIENT_ID | --cid ID] [--available | --effective | --all] [ARGUMENTS]");
+        out.println("       " + CMD + " get-roles (--rname ROLE_NAME | --rid ROLE_ID) [--cclientid CLIENT_ID | --cid ID] [--available | --effective | --all] [ARGUMENTS]");
         out.println();
-        out.println("Command to list realm or client roles on a realm, user or group.");
+        out.println("Command to list realm or client roles of a realm, a user, a group or a composite role.");
         out.println();
         out.println("Use `" + CMD + " config credentials` to establish an authenticated session, or use CREDENTIALS OPTIONS");
         out.println("to perform one time authentication.");
@@ -272,11 +329,13 @@ public class GetRolesCmd extends GetCmd {
         out.println("If client is specified using --cclientid or --cid then client roles are listed, otherwise realm roles are listed.");
         out.println("If user is specified using --uusername or --uid then roles are listed for a specific user.");
         out.println("If group is specified using --gname, --gpath or --gid then roles are listed for a specific group.");
-        out.println("If neither user nor group is specified then defined roles are listed for a realm or specific client");
+        out.println("If composite role is specified --rname or --rid then roles are listed for a specific composite role.");
+        out.println("If neither user nor group, nor composite role is specified then defined roles are listed for a realm or specific client.");
         out.println("If role is specified using --rolename or --roleid then only that specific role is returned.");
         out.println("If --available is specified, then only roles not yet added to the target user or group are returned.");
         out.println("If --effective is specified, then roles added to the target user or group are transitively resolved and a full");
-        out.println("set of roles in effect for that user or group is returned.");
+        out.println("set of roles in effect for that user, group or composite role is returned.");
+        out.println("If --all is specified, then client roles for all clients are returned in addition to realm roles.");
         out.println();
         out.println("Arguments:");
         out.println();
@@ -297,10 +356,15 @@ public class GetRolesCmd extends GetCmd {
         out.println("                              to use --gid, or --gpath to specify the target group");
         out.println("    --gpath                   Group's 'path' attribute");
         out.println("    --gid                     Group's 'id' attribute");
+        out.println("    --rname                   Composite role's 'name' attribute");
+        out.println("    --rid                     Composite role's 'id' attribute");
         out.println("    --cclientid               Client's 'clientId' attribute");
         out.println("    --cid                     Client's 'id' attribute");
         out.println("    --rolename                Role's 'name' attribute");
         out.println("    --roleid                  Role's 'id' attribute");
+        out.println("    --available               Return available roles - those that can still be added");
+        out.println("    --effective               Return effective roles - transitively taking composite roles into account");
+        out.println("    --all                     Return all client roles in addition to realm roles");
         out.println("    -a, --admin-root URL      URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin");
         out.println("    -r, --target-realm REALM  Target realm to issue requests against if not the one authenticated against");
         out.println();
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/RemoveRolesCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/RemoveRolesCmd.java
index 84698c3..5338d23 100644
--- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/RemoveRolesCmd.java
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/RemoveRolesCmd.java
@@ -67,6 +67,12 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd {
     @Option(name = "gid", description = "Target group's 'id'")
     String gid;
 
+    @Option(name = "rname", description = "Composite role's 'name'")
+    String rname;
+
+    @Option(name = "rid", description = "Composite role's 'id'")
+    String rid;
+
     @Option(name = "cclientid", description = "Target client's 'clientId'")
     String cclientid;
 
@@ -116,19 +122,31 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd {
             }
 
             if (roleNames.isEmpty() && roleIds.isEmpty()) {
-                throw new IllegalArgumentException("No role specified. Use --rolename or --roleid to specify roles");
+                throw new IllegalArgumentException("No role to remove specified. Use --rolename or --roleid to specify roles to remove");
             }
 
             if (cid != null && cclientid != null) {
                 throw new IllegalArgumentException("Incompatible options: --cid and --cclientid are mutually exclusive");
             }
 
+            if (rid != null && rname != null) {
+                throw new IllegalArgumentException("Incompatible options: --rid and --rname are mutually exclusive");
+            }
+
             if (isUserSpecified() && isGroupSpecified()) {
                 throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --gname / --gid / --gpath");
             }
 
-            if (!isUserSpecified() && !isGroupSpecified()) {
-                throw new IllegalArgumentException("No user nor group specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group");
+            if (isUserSpecified() && isCompositeRoleSpecified()) {
+                throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --rname / --rid");
+            }
+
+            if (isGroupSpecified() && isCompositeRoleSpecified()) {
+                throw new IllegalArgumentException("Incompatible options: --rname / --rid can't be used at the same time as --gname / --gid / --gpath");
+            }
+
+            if (!isUserSpecified() && !isGroupSpecified() && !isCompositeRoleSpecified()) {
+                throw new IllegalArgumentException("No user nor group nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role");
             }
 
 
@@ -204,9 +222,32 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd {
                     GroupOperations.removeRealmRoles(adminRoot, realm, auth, gid, new ArrayList<>(rolesToAdd));
                 }
 
-            } else {
+            } else if (isCompositeRoleSpecified()) {
+                if (rid == null) {
+                    rid = RoleOperations.getIdFromRoleName(adminRoot, realm, auth, rname);
+                }
+                if (isClientSpecified()) {
+                    // remove client roles from a role
+                    if (cid == null) {
+                        cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
+                    }
 
-                throw new IllegalArgumentException("No user nor group specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group");
+                    List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
+                    Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
+
+                    // now remove the roles
+                    RoleOperations.removeClientRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd));
+
+                } else {
+                    Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
+                            new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
+
+                    // now remove the roles
+                    RoleOperations.removeRealmRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd));
+                }
+
+            } else {
+                throw new IllegalArgumentException("No user nor group, nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role");
             }
 
             return CommandResult.SUCCESS;
@@ -257,6 +298,9 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd {
         return uid != null || uusername != null;
     }
 
+    private boolean isCompositeRoleSpecified() {
+        return rid != null || rname != null;
+    }
 
     @Override
     protected boolean nothingToDo() {
@@ -275,9 +319,10 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd {
         StringWriter sb = new StringWriter();
         PrintWriter out = new PrintWriter(sb);
         out.println("Usage: " + CMD + " remove-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]");
-        out.println("Usage: " + CMD + " remove-roles (--gname NAME | --gpath PATH | --gid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]");
+        out.println("       " + CMD + " remove-roles (--gname NAME | --gpath PATH | --gid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]");
+        out.println("       " + CMD + " remove-roles (--rname ROLE_NAME | --rid ROLE_ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]");
         out.println();
-        out.println("Command to remove realm or client roles from a user or group.");
+        out.println("Command to remove realm or client roles from a user, a group or a composite role.");
         out.println();
         out.println("Use `" + CMD + " config credentials` to establish an authenticated session, or use CREDENTIALS OPTIONS");
         out.println("to perform one time authentication.");
@@ -285,7 +330,8 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd {
         out.println("If client is specified using --cclientid or --cid then roles to remove are client roles, otherwise they are realm roles.");
         out.println("Either a user, or a group needs to be specified. If user is specified using --uusername or --uid then roles are removed");
         out.println("from a specific user. If group is specified using --gname, --gpath or --gid then roles are removed from a specific group.");
-        out.println("One or more roles have to be specified using --rolename or --roleid to be removed from a group or a user.");
+        out.println("If composite role is specified using --rname or --rid then roles are removed from a specific composite role.");
+        out.println("One or more roles have to be specified using --rolename or --roleid to be removed from a group, a user or a composite role.");
         out.println();
         out.println("Arguments:");
         out.println();
@@ -306,6 +352,8 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd {
         out.println("                          to use --gid, or --gpath to specify the target group");
         out.println("    --gpath               Group's 'path' attribute");
         out.println("    --gid                 Group's 'id' attribute");
+        out.println("    --rname               Composite role's 'name' attribute");
+        out.println("    --rid                 Composite role's 'id' attribute");
         out.println("    --cclientid           Client's 'clientId' attribute");
         out.println("    --cid                 Client's 'id' attribute");
         out.println("    --rolename            Role's 'name' attribute");
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/UpdateCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/UpdateCmd.java
index 3ce91b3..6c194e9 100644
--- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/UpdateCmd.java
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/UpdateCmd.java
@@ -26,6 +26,7 @@ import java.io.StringWriter;
 import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
 import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
 import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
+import static org.keycloak.client.admin.cli.util.OsUtil.OS_ARCH;
 import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
 
 /**
@@ -37,6 +38,9 @@ public class UpdateCmd extends AbstractRequestCmd {
     @Option(shortName = 'f', name = "file", description = "Read object from file or standard input if FILENAME is set to '-'")
     String file;
 
+    @Option(shortName = 'b', name = "body", description = "JSON object to be sent as-is or used as a template")
+    String body;
+
     @Option(shortName = 'F', name = "fields", description = "A pattern specifying which attributes of JSON response body to actually display as result - causes mismatch with Content-Length header")
     String fields;
 
@@ -63,6 +67,7 @@ public class UpdateCmd extends AbstractRequestCmd {
     void initOptions() {
         // set options on parent
         super.file = file;
+        super.body = body;
         super.fields = fields;
         super.printHeaders = printHeaders;
         super.returnId = false;
@@ -76,7 +81,7 @@ public class UpdateCmd extends AbstractRequestCmd {
 
     @Override
     protected boolean nothingToDo() {
-        return noOptions() && file == null && (args == null || args.size() == 0);
+        return noOptions() && file == null && body == null && (args == null || args.size() == 0);
     }
 
     protected String suggestHelp() {
@@ -117,6 +122,7 @@ public class UpdateCmd extends AbstractRequestCmd {
         out.println("              NAME+=VALUE     Add item VALUE to list attribute NAME");
         out.println("    -d, --delete NAME         Remove a specific attribute NAME from JSON request body");
         out.println("    -f, --file FILENAME       Read object from file or standard input if FILENAME is set to '-'");
+        out.println("    -b, --body CONTENT        Content to be sent as-is or used as a JSON object template");
         out.println("    -q, --query NAME=VALUE    Add to request URI a NAME query parameter with value VALUE");
         out.println("    -h, --header NAME=VALUE   Set request header NAME to VALUE");
         out.println("    -m, --merge               Merge new values with existing configuration on the server");
@@ -150,7 +156,11 @@ public class UpdateCmd extends AbstractRequestCmd {
         out.println("  " + PROMPT + " " + CMD + " update realms/demorealm -s registrationAllowed=true");
         out.println();
         out.println("Update a client by overwriting existing configuration using local file as a template (replace ID with client's 'id'):");
-        out.println("  " + PROMPT + " " + CMD + " update clients/ID -f new_my_client.json -s 'redirectUris=[\"http://localhost:8080/myapp/*\"]'");
+        if (OS_ARCH.isWindows()) {
+            out.println("  " + PROMPT + " " + CMD + " update clients/ID -f new_my_client.json -s \"redirectUris=[\\\"http://localhost:8080/myapp/*\\\"]\"");
+        } else {
+            out.println("  " + PROMPT + " " + CMD + " update clients/ID -f new_my_client.json -s 'redirectUris=[\"http://localhost:8080/myapp/*\"]'");
+        }
         out.println();
         out.println("Update client by fetching current configuration from server and merging with specified changes (replace ID with client's 'id'):");
         out.println("  " + PROMPT + " " + CMD + " update clients/ID -f new_my_client.json -s enabled=true --merge");
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/RoleOperations.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/RoleOperations.java
index d3274fd..c7e3108 100644
--- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/RoleOperations.java
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/RoleOperations.java
@@ -23,8 +23,11 @@ import java.util.ArrayList;
 import java.util.List;
 
 import static org.keycloak.client.admin.cli.util.HttpUtil.composeResourceUrl;
+import static org.keycloak.client.admin.cli.util.HttpUtil.doDeleteJSON;
 import static org.keycloak.client.admin.cli.util.HttpUtil.doGetJSON;
+import static org.keycloak.client.admin.cli.util.HttpUtil.doPostJSON;
 import static org.keycloak.client.admin.cli.util.HttpUtil.getAttrForType;
+import static org.keycloak.client.admin.cli.util.HttpUtil.getIdForType;
 
 /**
  * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@@ -34,6 +37,28 @@ public class RoleOperations {
     public static class LIST_OF_ROLES extends ArrayList<RoleRepresentation>{};
     public static class LIST_OF_NODES extends ArrayList<ObjectNode>{};
 
+    public static String getIdFromRoleName(String adminRoot, String realm, String auth, String rname) {
+        return getIdForType(adminRoot, realm, auth, "roles", "name", rname);
+    }
+
+    public static void addRealmRoles(String rootUrl, String realm, String auth, String roleid, List<?> roles) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "roles-by-id/" + roleid + "/composites");
+        doPostJSON(resourceUrl, auth, roles);
+    }
+
+    public static void addClientRoles(String rootUrl, String realm, String auth, String roleid, List<?> roles) {
+        addRealmRoles(rootUrl, realm, auth, roleid, roles);
+    }
+
+    public static void removeRealmRoles(String rootUrl, String realm, String auth, String roleid, List<?> roles) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "roles-by-id/" + roleid + "/composites");
+        doDeleteJSON(resourceUrl, auth, roles);
+    }
+
+    public static void removeClientRoles(String rootUrl, String realm, String auth, String roleid, List<?> roles) {
+        removeRealmRoles(rootUrl, realm, auth, roleid, roles);
+    }
+
     public static String getRoleNameFromId(String adminRoot, String realm, String auth, String rid) {
         return getAttrForType(adminRoot, realm, auth, "roles", "id", rid, "name");
     }
diff --git a/model/jpa/src/main/java/org/keycloak/events/jpa/JpaAdminEventQuery.java b/model/jpa/src/main/java/org/keycloak/events/jpa/JpaAdminEventQuery.java
index 9c3e534..15c58c2 100755
--- a/model/jpa/src/main/java/org/keycloak/events/jpa/JpaAdminEventQuery.java
+++ b/model/jpa/src/main/java/org/keycloak/events/jpa/JpaAdminEventQuery.java
@@ -111,7 +111,7 @@ public class JpaAdminEventQuery implements AdminEventQuery {
     @Override
     public AdminEventQuery resourcePath(String resourcePath) {
         Expression<String> rPath = root.get("resourcePath");
-        predicates.add(cb.like(rPath, "%"+resourcePath+"%"));
+        predicates.add(cb.like(rPath, resourcePath.replace('*', '%')));
         return this;
     }
 
diff --git a/saml-core/src/main/java/org/keycloak/saml/common/util/StaxParserUtil.java b/saml-core/src/main/java/org/keycloak/saml/common/util/StaxParserUtil.java
index 6e37558..1b06e9d 100755
--- a/saml-core/src/main/java/org/keycloak/saml/common/util/StaxParserUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/common/util/StaxParserUtil.java
@@ -239,15 +239,10 @@ public class StaxParserUtil {
      * @return
      */
     public static XMLEventReader getXMLEventReader(InputStream is) {
-        XMLInputFactory xmlInputFactory = null;
+        XMLInputFactory xmlInputFactory;
         XMLEventReader xmlEventReader = null;
         try {
-            xmlInputFactory = getXMLInputFactory();
-            xmlInputFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, Boolean.TRUE);
-            xmlInputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE);
-            xmlInputFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.TRUE);
-            xmlInputFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE);
-
+            xmlInputFactory = XML_INPUT_FACTORY.get();
             xmlEventReader = xmlInputFactory.createXMLEventReader(is);
         } catch (Exception ex) {
             throw new RuntimeException(ex);
@@ -518,6 +513,13 @@ public class StaxParserUtil {
             throw new RuntimeException(logger.parserExpectedEndTag("</" + tag + ">.  Found </" + elementTag + ">"));
     }
 
+    private static final ThreadLocal<XMLInputFactory> XML_INPUT_FACTORY = new ThreadLocal<XMLInputFactory>() {
+        @Override
+        protected XMLInputFactory initialValue() {
+            return getXMLInputFactory();
+        }
+    };
+
     private static XMLInputFactory getXMLInputFactory() {
         boolean tccl_jaxp = SystemPropertiesUtil.getSystemProperty(GeneralConstants.TCCL_JAXP, "false")
                 .equalsIgnoreCase("true");
@@ -526,7 +528,14 @@ public class StaxParserUtil {
             if (tccl_jaxp) {
                 SecurityActions.setTCCL(StaxParserUtil.class.getClassLoader());
             }
-            return XMLInputFactory.newInstance();
+            XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
+
+            xmlInputFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, Boolean.TRUE);
+            xmlInputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE);
+            xmlInputFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.TRUE);
+            xmlInputFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE);
+
+            return xmlInputFactory;
         } finally {
             if (tccl_jaxp) {
                 SecurityActions.setTCCL(prevTCCL);
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java b/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java
index 3b4b5db..9830482 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java
@@ -243,7 +243,7 @@ public class SAML2Request {
      *
      * @throws ConfigurationException
      */
-    public LogoutRequestType createLogoutRequest(String issuer) throws ConfigurationException {
+    public static LogoutRequestType createLogoutRequest(String issuer) throws ConfigurationException {
         LogoutRequestType lrt = new LogoutRequestType(IDGenerator.create("ID_"), XMLTimeUtil.getIssueInstant());
 
         // Create an issuer
@@ -266,7 +266,7 @@ public class SAML2Request {
      * @throws ParsingException
      * @throws ConfigurationException
      */
-    public Document convert(RequestAbstractType rat) throws ProcessingException, ConfigurationException, ParsingException {
+    public static Document convert(RequestAbstractType rat) throws ProcessingException, ConfigurationException, ParsingException {
         ByteArrayOutputStream bos = new ByteArrayOutputStream();
 
         SAMLRequestWriter writer = new SAMLRequestWriter(StaxUtil.getXMLStreamWriter(bos));
@@ -290,7 +290,7 @@ public class SAML2Request {
      * @throws ParsingException
      * @throws ConfigurationException
      */
-    public Document convert(ResponseType responseType) throws ProcessingException, ParsingException, ConfigurationException {
+    public static Document convert(ResponseType responseType) throws ProcessingException, ParsingException, ConfigurationException {
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         SAMLResponseWriter writer = new SAMLResponseWriter(StaxUtil.getXMLStreamWriter(baos));
         writer.write(responseType);
@@ -307,7 +307,7 @@ public class SAML2Request {
      *
      * @throws ProcessingException
      */
-    public void marshall(RequestAbstractType requestType, OutputStream os) throws ProcessingException {
+    public static void marshall(RequestAbstractType requestType, OutputStream os) throws ProcessingException {
         SAMLRequestWriter samlRequestWriter = new SAMLRequestWriter(StaxUtil.getXMLStreamWriter(os));
         if (requestType instanceof AuthnRequestType) {
             samlRequestWriter.write((AuthnRequestType) requestType);
@@ -325,7 +325,7 @@ public class SAML2Request {
      *
      * @throws ProcessingException
      */
-    public void marshall(RequestAbstractType requestType, Writer writer) throws ProcessingException {
+    public static void marshall(RequestAbstractType requestType, Writer writer) throws ProcessingException {
         SAMLRequestWriter samlRequestWriter = new SAMLRequestWriter(StaxUtil.getXMLStreamWriter(writer));
         if (requestType instanceof AuthnRequestType) {
             samlRequestWriter.write((AuthnRequestType) requestType);
diff --git a/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/saml20-encrypted-signed-redirect-response-two-extensions.xml b/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/saml20-encrypted-signed-redirect-response-two-extensions.xml
index 94a6fdb..20a7997 100644
--- a/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/saml20-encrypted-signed-redirect-response-two-extensions.xml
+++ b/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/saml20-encrypted-signed-redirect-response-two-extensions.xml
@@ -2,7 +2,9 @@
     <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://localhost:8081/auth/realms/saml-demo</saml:Issuer>
     <samlp:Extensions>
         <kckey:KeyInfo xmlns:kckey="urn:keycloak:ext:key:1.0" MessageSigningKeyId="FJ86GcF3jTbNLOco4NvZkUCIUmfYCqoqtOQeMfbhNlE"/>
-        <what:ever xmlns:what="urn:keycloak:ext:what:1.0" what="ever"/>
+        <what:ever xmlns:what="urn:keycloak:ext:what:1.0" what="ever">
+            <nested><element>text contents</element></nested>
+        </what:ever>
     </samlp:Extensions>
     <samlp:Status>
         <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
diff --git a/server-spi-private/src/main/java/org/keycloak/events/admin/AdminEventQuery.java b/server-spi-private/src/main/java/org/keycloak/events/admin/AdminEventQuery.java
index 8035a14..5702298 100644
--- a/server-spi-private/src/main/java/org/keycloak/events/admin/AdminEventQuery.java
+++ b/server-spi-private/src/main/java/org/keycloak/events/admin/AdminEventQuery.java
@@ -81,11 +81,11 @@ public interface AdminEventQuery {
     AdminEventQuery resourceType(ResourceType ... resourceTypes);
 
     /**
-     * Search by resource path. Supports wildcards <code>*</code> and <code>**</code>. For example:
+     * Search by resource path. Supports wildcard <code>*</code>. For example:
      * <ul>
      * <li><b>*&#47;master</b> - matches 'realms/master'</li>
-     * <li><b>**&#47;00d4b16f</b> - matches 'realms/master/clients/00d4b16f'</li>
-     * <li><b>realms&#47;master&#47;**</b> - matches anything under 'realms/master'</li>
+     * <li><b>realms/master&#47;*&#47;00d4b16f</b> - matches 'realms/master/clients/00d4b16f'</li>
+     * <li><b>realms&#47;master&#47;*</b> - matches anything under 'realms/master'</li>
      * </ul>
      *
      * @param resourcePath
diff --git a/server-spi-private/src/main/java/org/keycloak/events/EventType.java b/server-spi-private/src/main/java/org/keycloak/events/EventType.java
index 67c94a1..f97b713 100755
--- a/server-spi-private/src/main/java/org/keycloak/events/EventType.java
+++ b/server-spi-private/src/main/java/org/keycloak/events/EventType.java
@@ -68,6 +68,7 @@ public enum EventType {
     REMOVE_TOTP_ERROR(true),
 
     REVOKE_GRANT(true),
+    REVOKE_GRANT_ERROR(true),
 
     SEND_VERIFY_EMAIL(true),
     SEND_VERIFY_EMAIL_ERROR(true),
@@ -78,9 +79,12 @@ public enum EventType {
     RESET_PASSWORD(true),
     RESET_PASSWORD_ERROR(true),
 
+    INVALID_SIGNATURE(false),
     INVALID_SIGNATURE_ERROR(false),
     REGISTER_NODE(false),
+    REGISTER_NODE_ERROR(false),
     UNREGISTER_NODE(false),
+    UNREGISTER_NODE_ERROR(false),
 
     USER_INFO_REQUEST(false),
     USER_INFO_REQUEST_ERROR(false),
@@ -96,6 +100,7 @@ public enum EventType {
     IDENTITY_PROVIDER_RETRIEVE_TOKEN(false),
     IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR(false),
     IMPERSONATE(true),
+    IMPERSONATE_ERROR(true),
     CUSTOM_REQUIRED_ACTION(true),
     CUSTOM_REQUIRED_ACTION_ERROR(true),
     EXECUTE_ACTIONS(true),
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java
index 19edea6..46097a0 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java
@@ -80,6 +80,8 @@ public class ResetCredentialChooseUser implements Authenticator, AuthenticatorFa
             context.failureChallenge(AuthenticationFlowError.INVALID_USER, challenge);
             return;
         }
+
+        username = username.trim();
         
         RealmModel realm = context.getRealm();
         UserModel user = context.getSession().users().getUserByUsername(username, realm);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java
index c2f39f1..6917953 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java
@@ -37,6 +37,7 @@ import org.keycloak.models.AuthenticatorConfigModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RequiredActionProviderModel;
+import org.keycloak.models.utils.DefaultAuthenticationFlows;
 import org.keycloak.models.utils.ModelToRepresentation;
 import org.keycloak.models.utils.RepresentationToModel;
 import org.keycloak.provider.ProviderConfigProperty;
@@ -186,7 +187,8 @@ public class AuthenticationManagementResource {
 
         List<AuthenticationFlowRepresentation> flows = new LinkedList<>();
         for (AuthenticationFlowModel flow : realm.getAuthenticationFlows()) {
-            if (flow.isTopLevel()) {
+            // KEYCLOAK-3517, we need a better way to filter non-configurable internal flows
+            if (flow.isTopLevel() && !flow.getAlias().equals(DefaultAuthenticationFlows.SAML_ECP_FLOW)) {
                 flows.add(ModelToRepresentation.toRepresentation(realm, flow));
             }
         }
diff --git a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java
index 81afc03..5d29edb 100755
--- a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java
+++ b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java
@@ -195,7 +195,10 @@ public class SendUsernameServlet {
         output += "<br /> friendlyAttribute email: " + principal.getFriendlyAttribute("email");
         output += "<br /> phone: " + principal.getAttribute("phone");
         output += "<br /> friendlyAttribute phone: " + principal.getFriendlyAttribute("phone");
-        output += "<br /> hardcoded-attribute: " + principal.getAttribute("hardcoded-attribute");
+        output += "<br /> hardcoded-attribute: ";
+        for (String attr : principal.getAttributes("hardcoded-attribute")) {
+            output += attr + ",";
+        }
 
         return output;
     }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java
index 0ec5c8c..0481518 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java
@@ -196,13 +196,6 @@ public class InitialFlowsTest extends AbstractAuthenticationTest {
         addExecInfo(execs, "Reset OTP", "reset-otp", false, 0, 3, OPTIONAL, null, new String[]{REQUIRED, OPTIONAL, DISABLED});
         expected.add(new FlowExecutions(flow, execs));
 
-        flow = newFlow("saml ecp", "SAML ECP Profile Authentication Flow", "basic-flow", true, true);
-        addExecExport(flow, null, false, "http-basic-authenticator", false, null, REQUIRED, 10);
-
-        execs = new LinkedList<>();
-        addExecInfo(execs, "HTTP Basic Authentication", "http-basic-authenticator", false, 0, 0, REQUIRED, null, new String[]{});
-        expected.add(new FlowExecutions(flow, execs));
-
         return expected;
     }
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/event/AdminEventTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/event/AdminEventTest.java
index 3d33fdc..96e2920 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/event/AdminEventTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/event/AdminEventTest.java
@@ -57,9 +57,9 @@ public class AdminEventTest extends AbstractEventTest {
         return testRealmResource().getAdminEvents();
     }
 
-    private void createUser(String username) {
+    private String createUser(String username) {
         UserRepresentation user = createUserRepresentation(username, username + "@foo.com", "foo", "bar", true);
-        ApiUtil.createUserWithAdminClient(testRealmResource(), user);
+        return ApiUtil.createUserWithAdminClient(testRealmResource(), user);
     }
 
     private void updateRealm() {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/partialimport/PartialImportTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/partialimport/PartialImportTest.java
index e6f7348..6125ab3 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/partialimport/PartialImportTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/partialimport/PartialImportTest.java
@@ -133,7 +133,7 @@ public class PartialImportTest extends AbstractAuthTest {
     public void removeClients() {
         List<ClientRepresentation> toRemove = testRealmResource().clients().findAll();
         for (ClientRepresentation client : toRemove) {
-            if (client.getName().startsWith(CLIENT_PREFIX)) {
+            if (client.getName() != null && client.getName().startsWith(CLIENT_PREFIX)) {
                 testRealmResource().clients().get(client.getId()).remove();
             }
         }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmSessionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmSessionTest.java
new file mode 100644
index 0000000..6cbe4f5
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmSessionTest.java
@@ -0,0 +1,197 @@
+package org.keycloak.testsuite.cli.admin;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.client.admin.cli.config.FileConfigHandler;
+import org.keycloak.testsuite.cli.KcAdmExec;
+import org.keycloak.testsuite.util.TempFileResource;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
+import static org.keycloak.testsuite.cli.KcAdmExec.execute;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KcAdmSessionTest extends AbstractAdmCliTest {
+
+    static Class<? extends List<ObjectNode>> LIST_OF_JSON = new ArrayList<ObjectNode>() {}.getClass();
+
+    @Test
+    public void test() throws IOException {
+
+        FileConfigHandler handler = initCustomConfigFile();
+
+        try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
+
+            // login as admin
+            loginAsUser(configFile.getFile(), serverUrl, "master", "admin", "admin");
+
+            // create realm
+            KcAdmExec exe = execute("create realms --config '" + configFile.getName() + "' -s realm=demorealm -s enabled=true");
+
+            assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+            Assert.assertTrue(exe.stderrLines().get(0).startsWith("Created "));
+
+            // create user
+            exe = execute("create users --config '" + configFile.getName() + "' -r demorealm -s username=testuser -s enabled=true -i");
+
+            assertExitCodeAndStreamSizes(exe, 0, 1, 0);
+            String userId = exe.stdoutLines().get(0);
+
+            // add realm admin capabilities to user
+            exe = execute("add-roles --config '" + configFile.getName() + "' -r demorealm --uusername testuser --cclientid realm-management --rolename realm-admin");
+
+            assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+            // set password for the user
+            exe = execute("set-password --config '" + configFile.getName() + "' -r demorealm --username testuser -p password");
+
+            assertExitCodeAndStdErrSize(exe, 0, 0);
+
+
+            // login as testuser
+            loginAsUser(configFile.getFile(), serverUrl, "demorealm", "testuser", "password");
+
+
+            // get realm roles
+            exe = execute("get-roles --config '" + configFile.getName() + "'");
+
+            assertExitCodeAndStdErrSize(exe, 0, 0);
+            List<ObjectNode> roles = loadJson(exe.stdout(), LIST_OF_JSON);
+            Assert.assertTrue("expect two realm roles available", roles.size() == 2);
+
+            // create realm role
+            exe = execute("create roles --config '" + configFile.getName() + "' -s name=testrole -s 'description=Test role' -o");
+
+            assertExitCodeAndStdErrSize(exe, 0, 0);
+            ObjectNode role = loadJson(exe.stdout(), ObjectNode.class);
+            Assert.assertEquals("testrole", role.get("name").asText());
+            String roleId = role.get("id").asText();
+
+            // get realm roles again
+            exe = execute("get-roles --config '" + configFile.getName() + "'");
+
+            assertExitCodeAndStdErrSize(exe, 0, 0);
+            roles = loadJson(exe.stdout(), LIST_OF_JSON);
+            Assert.assertTrue("expect three realm roles available", roles.size() == 3);
+
+            // create client
+            exe = execute("create clients --config '" + configFile.getName() + "' -s clientId=testclient -i");
+
+            assertExitCodeAndStreamSizes(exe, 0, 1, 0);
+            String idOfClient = exe.stdoutLines().get(0);
+
+
+            // create client role
+            exe = execute("create clients/" + idOfClient + "/roles --config '" + configFile.getName() + "' -s name=clientrole  -s 'description=Test client role'");
+
+            assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+            Assert.assertTrue(exe.stderrLines().get(0).startsWith("Created "));
+
+            // make sure client role has been created
+            exe = execute("get-roles --config '" + configFile.getName() + "' --cclientid testclient");
+
+            assertExitCodeAndStdErrSize(exe, 0, 0);
+            roles = loadJson(exe.stdout(), LIST_OF_JSON);
+            Assert.assertTrue("expect one role", roles.size() == 1);
+            Assert.assertEquals("clientrole", roles.get(0).get("name").asText());
+
+            // add created role to user - we are realm admin so we can add role to ourself
+            exe = execute("add-roles --config '" + configFile.getName() + "' --uusername testuser --cclientid testclient --rolename clientrole");
+
+            assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+
+            // make sure the roles have been added
+            exe = execute("get-roles --config '" + configFile.getName() + "' --uusername testuser --all");
+
+            assertExitCodeAndStdErrSize(exe, 0, 0);
+            ObjectNode node = loadJson(exe.stdout(), ObjectNode.class);
+            Assert.assertNotNull(node.get("realmMappings"));
+
+            List<String> realmMappings = StreamSupport.stream(node.get("realmMappings").spliterator(), false)
+                    .map(o -> o.get("name").asText()).sorted().collect(Collectors.toList());
+            Assert.assertEquals(Arrays.asList("offline_access", "uma_authorization"), realmMappings);
+
+            ObjectNode clientRoles = (ObjectNode) node.get("clientMappings");
+            //List<String> fields = asSortedList(clientRoles.fieldNames());
+            List<String> fields = StreamSupport.stream(clientRoles.spliterator(), false)
+                    .map(o -> o.get("client").asText()).sorted().collect(Collectors.toList());
+            Assert.assertEquals(Arrays.asList("account", "realm-management", "testclient"), fields);
+
+            realmMappings = StreamSupport.stream(clientRoles.get("account").get("mappings").spliterator(), false)
+                    .map(o -> o.get("name").asText()).sorted().collect(Collectors.toList());
+            Assert.assertEquals(Arrays.asList("manage-account", "view-profile"), realmMappings);
+
+            realmMappings = StreamSupport.stream(clientRoles.get("realm-management").get("mappings").spliterator(), false)
+                    .map(o -> o.get("name").asText()).sorted().collect(Collectors.toList());
+            Assert.assertEquals(Arrays.asList("realm-admin"), realmMappings);
+
+            realmMappings = StreamSupport.stream(clientRoles.get("testclient").get("mappings").spliterator(), false)
+                    .map(o -> o.get("name").asText()).sorted().collect(Collectors.toList());
+            Assert.assertEquals(Arrays.asList("clientrole"), realmMappings);
+
+
+
+            // add a realm role to the user
+            exe = execute("add-roles --config '" + configFile.getName() + "' --uusername testuser --rolename testrole");
+
+            assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+
+            // get all roles for the user again
+            exe = execute("get-roles --config '" + configFile.getName() + "' --uusername testuser --all");
+            assertExitCodeAndStdErrSize(exe, 0, 0);
+
+            node = loadJson(exe.stdout(), ObjectNode.class);
+            Assert.assertNotNull(node.get("realmMappings"));
+
+            realmMappings = StreamSupport.stream(node.get("realmMappings").spliterator(), false)
+                    .map(o -> o.get("name").asText()).sorted().collect(Collectors.toList());
+            Assert.assertEquals(Arrays.asList("offline_access", "testrole", "uma_authorization"), realmMappings);
+
+            // create a group
+            exe = execute("create groups --config '" + configFile.getName() + "' -s name=TestUsers -i");
+            assertExitCodeAndStdErrSize(exe, 0, 0);
+            String groupId = exe.stdoutLines().get(0);
+
+            // create a sub-group
+            exe = execute("create groups/" + groupId + "/children --config '" + configFile.getName() + "' -s name=TestPowerUsers -i");
+            assertExitCodeAndStdErrSize(exe, 0, 0);
+            String subGroupId = exe.stdoutLines().get(0);
+
+            // add testuser to TestPowerUsers
+            exe = execute("update users/" + userId + "/groups/" + subGroupId + " --config '" + configFile.getName()
+                    + "' -s realm=demorealm -s userId=" + userId +  " -s groupId=" + subGroupId + " -n");
+            assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+            // delete everything
+            exe = execute("delete groups/" + subGroupId + " --config '" + configFile.getName() + "'");
+            assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+            exe = execute("delete groups/" + groupId + " --config '" + configFile.getName() + "'");
+            assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+            exe = execute("delete clients/" + idOfClient + " --config '" + configFile.getName() + "'");
+            assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+            exe = execute("delete roles/testrole --config '" + configFile.getName() + "'");
+            assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+            exe = execute("delete users/" + userId + " --config '" + configFile.getName() + "'");
+            assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+            // delete realm as well - using initial master realm session still saved in config file
+            exe = execute("delete realms/demorealm --config '" + configFile.getName() + "' --realm master");
+            assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+        }
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/SAMLClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/SAMLClientRegistrationTest.java
index 6efbe9a..970780e 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/SAMLClientRegistrationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/SAMLClientRegistrationTest.java
@@ -57,6 +57,8 @@ public class SAMLClientRegistrationTest extends AbstractClientRegistrationTest {
           "https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/paos",
           "https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/redirect"
         ));  // No redirect URI for ARTIFACT binding which is unsupported
+
+        assertThat(response.getAttributes().get("saml_single_logout_service_url_redirect"), is("https://LoadBalancer-9.siroe.com:3443/federation/SPSloRedirect/metaAlias/sp"));
     }
 
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/events/AdminEventStoreProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/events/AdminEventStoreProviderTest.java
index 055ed5d..0db44be 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/events/AdminEventStoreProviderTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/events/AdminEventStoreProviderTest.java
@@ -152,12 +152,13 @@ public class AdminEventStoreProviderTest extends AbstractEventsTest {
         testing().onAdminEvent(create(oldest, "realmId", OperationType.CREATE, "realmId", "clientId2", "userId", "127.0.0.1", "/admin/realms/master", "error"), false);
         testing().onAdminEvent(create("realmId", OperationType.CREATE, "realmId", "clientId", "userId2", "127.0.0.1", "/admin/realms/master", "error"), false);
 
-        Assert.assertEquals(6, testing().getAdminEvents(null, null, null, null, null, null, "/admin", null, null, null, null).size());
-        Assert.assertEquals(6, testing().getAdminEvents(null, null, null, null, null, null, "/realms", null, null, null, null).size());
-        Assert.assertEquals(6, testing().getAdminEvents(null, null, null, null, null, null, "/master", null, null, null, null).size());
-        Assert.assertEquals(6, testing().getAdminEvents(null, null, null, null, null, null, "/admin/realms", null, null, null, null).size());
-        Assert.assertEquals(6, testing().getAdminEvents(null, null, null, null, null, null, "/realms/master", null, null, null, null).size());
-        Assert.assertEquals(6, testing().getAdminEvents(null, null, null, null, null, null, "/admin/realms/master", null, null, null, null).size());
+        Assert.assertEquals(6, testing().getAdminEvents(null, null, null, null, null, null, "/admin/*", null, null, null, null).size());
+        Assert.assertEquals(6, testing().getAdminEvents(null, null, null, null, null, null, "*/realms/*", null, null, null, null).size());
+        Assert.assertEquals(6, testing().getAdminEvents(null, null, null, null, null, null, "*/master", null, null, null, null).size());
+        Assert.assertEquals(6, testing().getAdminEvents(null, null, null, null, null, null, "/admin/realms/*", null, null, null, null).size());
+        Assert.assertEquals(6, testing().getAdminEvents(null, null, null, null, null, null, "*/realms/master", null, null, null, null).size());
+        Assert.assertEquals(6, testing().getAdminEvents(null, null, null, null, null, null, "/admin/*/master", null, null, null, null).size());
+        Assert.assertEquals(6, testing().getAdminEvents(null, null, null, null, null, null, "/ad*/*/master", null, null, null, null).size());
     }
 
     @Test
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
index e23d51e..8ecfa13 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
@@ -178,6 +178,11 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
     }
 
     @Test
+    public void resetPasswordWithSpacesInUsername() throws IOException, MessagingException {
+        resetPassword(" login-test ");
+    }
+
+    @Test
     public void resetPasswordCancelChangeUser() throws IOException, MessagingException {
         loginPage.open();
         loginPage.resetPassword();
@@ -224,7 +229,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
 
         events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
                 .user(userId)
-                .detail(Details.USERNAME, username)
+                .detail(Details.USERNAME, username.trim())
                 .detail(Details.EMAIL, "login@test.com")
                 .session((String)null)
                 .assertEvent();
@@ -241,11 +246,11 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
 
         updatePasswordPage.changePassword("resetPassword", "resetPassword");
 
-        String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).detail(Details.USERNAME, username).assertEvent().getSessionId();
+        String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).detail(Details.USERNAME, username.trim()).assertEvent().getSessionId();
 
         assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
-        events.expectLogin().user(userId).detail(Details.USERNAME, username).session(sessionId).assertEvent();
+        events.expectLogin().user(userId).detail(Details.USERNAME, username.trim()).session(sessionId).assertEvent();
 
         oauth.openLogout();
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AuthnRequestNameIdFormatTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AuthnRequestNameIdFormatTest.java
new file mode 100644
index 0000000..e9b4ac6
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AuthnRequestNameIdFormatTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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.saml;
+
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.dom.saml.v2.assertion.NameIDType;
+import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
+import org.keycloak.dom.saml.v2.protocol.NameIDPolicyType;
+import org.keycloak.dom.saml.v2.protocol.ResponseType;
+import org.keycloak.protocol.saml.SamlConfigAttributes;
+import org.keycloak.protocol.saml.SamlProtocol;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
+import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
+import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
+import org.keycloak.services.resources.RealmsResource;
+import org.keycloak.testsuite.AbstractAuthTest;
+import org.keycloak.testsuite.util.SamlClient;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.List;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriBuilderException;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.util.EntityUtils;
+import org.hamcrest.Matcher;
+import org.junit.Test;
+import org.w3c.dom.Document;
+
+import static org.hamcrest.Matchers.*;
+import static org.keycloak.testsuite.util.SamlClient.*;
+import static org.junit.Assert.assertThat;
+import static org.keycloak.testsuite.util.IOUtil.loadRealm;
+import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class AuthnRequestNameIdFormatTest extends AbstractAuthTest {
+
+    private static final String REALM_NAME = "demo";
+
+    private static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST = "http://localhost:8080/sales-post/";
+    private static final String SAML_CLIENT_ID_SALES_POST = "http://localhost:8081/sales-post/";
+
+    public static SAMLDocumentHolder login(UserRepresentation user, URI samlEndpoint,
+      Document samlRequest, String relayState, Binding requestBinding, Binding expectedResponseBinding) {
+        CloseableHttpResponse response = null;
+        SamlClient.RedirectStrategyWithSwitchableFollowRedirect strategy = new SamlClient.RedirectStrategyWithSwitchableFollowRedirect();
+        try (CloseableHttpClient client = HttpClientBuilder.create().setRedirectStrategy(strategy).build()) {
+            HttpClientContext context = HttpClientContext.create();
+
+            HttpUriRequest post = requestBinding.createSamlRequest(samlEndpoint, relayState, samlRequest);
+            response = client.execute(post, context);
+
+            assertThat(response, statusCodeIsHC(Response.Status.OK));
+            String loginPageText = EntityUtils.toString(response.getEntity(), "UTF-8");
+            response.close();
+
+            assertThat(loginPageText, containsString("login"));
+
+            HttpUriRequest loginRequest = handleLoginPage(user, loginPageText);
+
+            strategy.setRedirectable(false);
+            response = client.execute(loginRequest, context);
+
+            return expectedResponseBinding.extractResponse(response);
+        } catch (Exception ex) {
+            throw new RuntimeException(ex);
+        } finally {
+            if (response != null) {
+                EntityUtils.consumeQuietly(response.getEntity());
+                try { response.close(); } catch (IOException ex) { }
+            }
+        }
+    }
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+        testRealms.add(loadRealm("/adapter-test/keycloak-saml/testsaml.json"));
+    }
+
+    public AuthnRequestType createLoginRequestDocument(String issuer, String assertionConsumerURL, String realmName) {
+        return SamlClient.createLoginRequestDocument(issuer, assertionConsumerURL, getAuthServerSamlEndpoint(realmName));
+    }
+
+    private URI getAuthServerSamlEndpoint(String realm) throws IllegalArgumentException, UriBuilderException {
+        return RealmsResource
+          .protocolUrl(UriBuilder.fromUri(getAuthServerRoot()))
+          .build(realm, SamlProtocol.LOGIN_PROTOCOL);
+    }
+
+    private void testLoginWithNameIdPolicy(Binding requestBinding, Binding responseBinding, NameIDPolicyType nameIDPolicy, Matcher<String> nameIdMatcher) throws Exception {
+        AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME);
+        loginRep.setProtocolBinding(requestBinding.getBindingUri());
+        loginRep.setNameIDPolicy(nameIDPolicy);
+
+        Document samlRequest = SAML2Request.convert(loginRep);
+        SAMLDocumentHolder res = login(bburkeUser, getAuthServerSamlEndpoint(REALM_NAME), samlRequest, null, requestBinding, responseBinding);
+
+        assertThat(res.getSamlObject(), notNullValue());
+        assertThat(res.getSamlObject(), instanceOf(ResponseType.class));
+
+        ResponseType rt = (ResponseType) res.getSamlObject();
+        assertThat(rt.getAssertions(), not(empty()));
+        assertThat(rt.getAssertions().get(0).getAssertion().getSubject().getSubType().getBaseID(), instanceOf(NameIDType.class));
+        NameIDType nameId = (NameIDType) rt.getAssertions().get(0).getAssertion().getSubject().getSubType().getBaseID();
+        assertThat(nameId.getValue(), nameIdMatcher);
+    }
+
+    @Test
+    public void testPostLoginNameIdPolicyUnspecified() throws Exception {
+        NameIDPolicyType nameIdPolicy = new NameIDPolicyType();
+        nameIdPolicy.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get()));
+        testLoginWithNameIdPolicy(Binding.POST, Binding.POST, nameIdPolicy, is("bburke"));
+    }
+
+    @Test
+    public void testPostLoginNameIdPolicyEmail() throws Exception {
+        NameIDPolicyType nameIdPolicy = new NameIDPolicyType();
+        nameIdPolicy.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get()));
+        testLoginWithNameIdPolicy(Binding.POST, Binding.POST, nameIdPolicy, is("bburke@redhat.com"));
+    }
+
+    @Test
+    public void testPostLoginNameIdPolicyPersistent() throws Exception {
+        NameIDPolicyType nameIdPolicy = new NameIDPolicyType();
+        nameIdPolicy.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get()));
+        testLoginWithNameIdPolicy(Binding.POST, Binding.POST, nameIdPolicy, startsWith("G-"));
+    }
+
+    @Test
+    public void testPostLoginNoNameIdPolicyUnset() throws Exception {
+        testLoginWithNameIdPolicy(Binding.POST, Binding.POST, null, is("bburke"));
+    }
+
+    @Test
+    public void testRedirectLoginNameIdPolicyUnspecified() throws Exception {
+        NameIDPolicyType nameIdPolicy = new NameIDPolicyType();
+        nameIdPolicy.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get()));
+        testLoginWithNameIdPolicy(Binding.REDIRECT, Binding.REDIRECT, nameIdPolicy, is("bburke"));
+    }
+
+    @Test
+    public void testRedirectLoginNameIdPolicyEmail() throws Exception {
+        NameIDPolicyType nameIdPolicy = new NameIDPolicyType();
+        nameIdPolicy.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get()));
+        testLoginWithNameIdPolicy(Binding.REDIRECT, Binding.REDIRECT, nameIdPolicy, is("bburke@redhat.com"));
+    }
+
+    @Test
+    public void testRedirectLoginNameIdPolicyPersistent() throws Exception {
+        NameIDPolicyType nameIdPolicy = new NameIDPolicyType();
+        nameIdPolicy.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get()));
+        testLoginWithNameIdPolicy(Binding.REDIRECT, Binding.REDIRECT, nameIdPolicy, startsWith("G-"));
+    }
+
+    @Test
+    public void testRedirectLoginNoNameIdPolicyUnset() throws Exception {
+        testLoginWithNameIdPolicy(Binding.REDIRECT, Binding.REDIRECT, null, is("bburke"));
+    }
+
+    @Test
+    public void testRedirectLoginNoNameIdPolicyForcePostBinding() throws Exception {
+        ClientsResource clients = adminClient.realm(REALM_NAME).clients();
+        List<ClientRepresentation> foundClients = clients.findByClientId("http://localhost:8081/sales-post/");
+        assertThat(foundClients, hasSize(1));
+        ClientResource clientRes = clients.get(foundClients.get(0).getId());
+        ClientRepresentation client = clientRes.toRepresentation();
+        client.getAttributes().put(SamlConfigAttributes.SAML_FORCE_POST_BINDING, "true");
+        clientRes.update(client);
+
+        testLoginWithNameIdPolicy(Binding.REDIRECT, Binding.POST, null, is("bburke"));
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ConcurrentAuthnRequestTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ConcurrentAuthnRequestTest.java
new file mode 100644
index 0000000..b207c04
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ConcurrentAuthnRequestTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.saml;
+
+import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
+import org.keycloak.protocol.saml.SamlProtocol;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
+import org.keycloak.services.resources.RealmsResource;
+import org.keycloak.testsuite.AbstractAuthTest;
+import org.keycloak.testsuite.util.SamlClient;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriBuilderException;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.util.EntityUtils;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.w3c.dom.Document;
+
+import static org.keycloak.testsuite.util.SamlClient.*;
+import static org.keycloak.testsuite.util.IOUtil.loadRealm;
+
+/**
+ *
+ * @author hmlnarik
+ */
+@Ignore
+public class ConcurrentAuthnRequestTest extends AbstractAuthTest {
+
+    private static final String REALM_NAME = "demo";
+
+    private static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST = "http://localhost:8080/sales-post/";
+    private static final String SAML_CLIENT_ID_SALES_POST = "http://localhost:8081/sales-post/";
+
+    public static final int ITERATIONS = 10000;
+    public static final int CONCURRENT_THREADS = 5;
+
+    private static void loginRepeatedly(UserRepresentation user, URI samlEndpoint,
+      Document samlRequest, String relayState, Binding requestBinding) {
+        CloseableHttpResponse response = null;
+        SamlClient.RedirectStrategyWithSwitchableFollowRedirect strategy = new SamlClient.RedirectStrategyWithSwitchableFollowRedirect();
+        ExecutorService threadPool = Executors.newFixedThreadPool(CONCURRENT_THREADS);
+
+        try (CloseableHttpClient client = HttpClientBuilder.create().setRedirectStrategy(strategy).build()) {
+            HttpUriRequest post = requestBinding.createSamlRequest(samlEndpoint, relayState, samlRequest);
+            
+            Collection<Callable<Void>> futures = new LinkedList<>();
+            for (int i = 0; i < ITERATIONS; i ++) {
+                final int j = i;
+                Callable<Void> f = () -> {
+                    performLogin(post, samlEndpoint, relayState, samlRequest, response, client, user, strategy);
+                    return null;
+                };
+                futures.add(f);
+            }
+
+            threadPool.invokeAll(futures);
+        } catch (Exception ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    public static void performLogin(HttpUriRequest post, URI samlEndpoint, String relayState,
+      Document samlRequest, CloseableHttpResponse response, final CloseableHttpClient client,
+      UserRepresentation user,
+      RedirectStrategyWithSwitchableFollowRedirect strategy) {
+        try {
+            HttpClientContext context = HttpClientContext.create();
+            response = client.execute(post, context);
+
+            String loginPageText = EntityUtils.toString(response.getEntity(), "UTF-8");
+            response.close();
+
+            HttpUriRequest loginRequest = handleLoginPage(user, loginPageText);
+
+            strategy.setRedirectable(false);
+            response = client.execute(loginRequest, context);
+            response.close();
+        } catch (Exception ex) {
+            throw new RuntimeException(ex);
+        } finally {
+            if (response != null) {
+                EntityUtils.consumeQuietly(response.getEntity());
+                try { response.close(); } catch (IOException ex) { }
+            }
+        }
+    }
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+        testRealms.add(loadRealm("/adapter-test/keycloak-saml/testsaml.json"));
+    }
+
+    public AuthnRequestType createLoginRequestDocument(String issuer, String assertionConsumerURL, String realmName) {
+        return SamlClient.createLoginRequestDocument(issuer, assertionConsumerURL, getAuthServerSamlEndpoint(realmName));
+    }
+
+    private URI getAuthServerSamlEndpoint(String realm) throws IllegalArgumentException, UriBuilderException {
+        return RealmsResource
+          .protocolUrl(UriBuilder.fromUri(getAuthServerRoot()))
+          .build(realm, SamlProtocol.LOGIN_PROTOCOL);
+    }
+
+    private void testLogin(Binding requestBinding) throws Exception {
+        AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME);
+        Document samlRequest = SAML2Request.convert(loginRep);
+        loginRepeatedly(bburkeUser, getAuthServerSamlEndpoint(REALM_NAME), samlRequest, null, requestBinding);
+    }
+
+    @Test
+    public void testConcurrentPostLogins() throws Exception {
+        testLogin(Binding.POST);
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/Matchers.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/Matchers.java
index b66e728..7ff72a5 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/Matchers.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/Matchers.java
@@ -16,12 +16,11 @@
  */
 package org.keycloak.testsuite.util;
 
-import org.keycloak.testsuite.util.matchers.ResponseBodyMatcher;
-import org.keycloak.testsuite.util.matchers.ResponseHeaderMatcher;
-import org.keycloak.testsuite.util.matchers.ResponseStatusCodeMatcher;
+import org.keycloak.testsuite.util.matchers.*;
 
 import java.util.Map;
 import javax.ws.rs.core.Response;
+import org.apache.http.HttpResponse;
 import org.hamcrest.Matcher;
 
 /**
@@ -49,6 +48,15 @@ public class Matchers {
     }
 
     /**
+     * Matcher on HTTP status code of a {@link Response} instance (HttpClient variant).
+     * @param matcher
+     * @return
+     */
+    public static Matcher<HttpResponse> statusCodeHC(Matcher<? extends Number> matcher) {
+        return new HttpResponseStatusCodeMatcher(matcher);
+    }
+
+    /**
      * Matches when the HTTP status code of a {@link Response} instance is equal to the given code.
      * @param expectedStatusCode
      * @return
@@ -58,6 +66,15 @@ public class Matchers {
     }
 
     /**
+     * Matches when the HTTP status code of a {@link Response} instance is equal to the given code (HttpClient variant).
+     * @param expectedStatusCode
+     * @return
+     */
+    public static Matcher<HttpResponse> statusCodeIsHC(Response.Status expectedStatusCode) {
+        return new HttpResponseStatusCodeMatcher(org.hamcrest.Matchers.is(expectedStatusCode.getStatusCode()));
+    }
+
+    /**
      * Matches when the HTTP status code of a {@link Response} instance is equal to the given code.
      * @param expectedStatusCode
      * @return
@@ -67,6 +84,15 @@ public class Matchers {
     }
 
     /**
+     * Matches when the HTTP status code of a {@link Response} instance is equal to the given code (HttpClient variant).
+     * @param expectedStatusCode
+     * @return
+     */
+    public static Matcher<HttpResponse> statusCodeIsHC(int expectedStatusCode) {
+        return new HttpResponseStatusCodeMatcher(org.hamcrest.Matchers.is(expectedStatusCode));
+    }
+
+    /**
      * Matches when the HTTP status code of a {@link Response} instance is equal to the given code.
      * @param expectedStatusCode
      * @return
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/HttpResponseStatusCodeMatcher.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/HttpResponseStatusCodeMatcher.java
new file mode 100644
index 0000000..78d5b3f
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/HttpResponseStatusCodeMatcher.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.util.matchers;
+
+import javax.ws.rs.core.Response;
+import org.apache.http.HttpResponse;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+
+/**
+ * Matcher for matching status code of {@link Response} instance.
+ * @author hmlnarik
+ */
+public class HttpResponseStatusCodeMatcher extends BaseMatcher<HttpResponse> {
+
+    private final Matcher<? extends Number> matcher;
+
+    public HttpResponseStatusCodeMatcher(Matcher<? extends Number> matcher) {
+        this.matcher = matcher;
+    }
+
+    @Override
+    public boolean matches(Object item) {
+        return (item instanceof HttpResponse) && this.matcher.matches(((HttpResponse) item).getStatusLine().getStatusCode());
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText("response status code matches ").appendDescriptionOf(this.matcher);
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/SamlClient.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/SamlClient.java
new file mode 100644
index 0000000..7af34f8
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/SamlClient.java
@@ -0,0 +1,266 @@
+/*
+ * 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.util;
+
+import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.saml.BaseSAML2BindingBuilder;
+import org.keycloak.saml.SAMLRequestParser;
+import org.keycloak.saml.common.constants.GeneralConstants;
+import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
+import org.keycloak.saml.common.exceptions.ConfigurationException;
+import org.keycloak.saml.common.exceptions.ProcessingException;
+import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
+import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.utils.URLEncodedUtils;
+import org.apache.http.impl.client.LaxRedirectStrategy;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import org.w3c.dom.Document;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.assertThat;
+import static org.keycloak.testsuite.admin.Users.getPasswordOf;
+import static org.keycloak.testsuite.util.Matchers.*;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class SamlClient {
+
+    /**
+     * SAML bindings and related HttpClient methods.
+     */
+    public enum Binding {
+        POST {
+            @Override
+            public SAMLDocumentHolder extractResponse(CloseableHttpResponse response) throws IOException {
+                assertThat(response, statusCodeIsHC(Response.Status.OK));
+                String responsePage = EntityUtils.toString(response.getEntity(), "UTF-8");
+                response.close();
+                return extractSamlResponseFromForm(responsePage);
+            }
+
+            @Override
+            public HttpPost createSamlRequest(URI samlEndpoint, String relayState, Document samlRequest) {
+                HttpPost post = new HttpPost(samlEndpoint);
+
+                List<NameValuePair> parameters = new LinkedList<>();
+                try {
+                    parameters.add(
+                      new BasicNameValuePair(GeneralConstants.SAML_REQUEST_KEY,
+                      new BaseSAML2BindingBuilder()
+                        .postBinding(samlRequest)
+                        .encoded())
+                    );
+                } catch (ProcessingException | ConfigurationException | IOException ex) {
+                    throw new RuntimeException(ex);
+                }
+                if (relayState != null) {
+                    parameters.add(new BasicNameValuePair(GeneralConstants.RELAY_STATE, relayState));
+                }
+
+                UrlEncodedFormEntity formEntity;
+                try {
+                    formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
+                } catch (UnsupportedEncodingException e) {
+                    throw new RuntimeException(e);
+                }
+                post.setEntity(formEntity);
+
+                return post;
+            }
+
+            @Override
+            public URI getBindingUri() {
+                return URI.create(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get());
+            }
+        },
+
+        REDIRECT {
+            @Override
+            public SAMLDocumentHolder extractResponse(CloseableHttpResponse response) throws IOException {
+                assertThat(response, statusCodeIsHC(Response.Status.FOUND));
+                String location = response.getFirstHeader("Location").getValue();
+                response.close();
+                return extractSamlResponseFromRedirect(location);
+            }
+
+            @Override
+            public HttpGet createSamlRequest(URI samlEndpoint, String relayState, Document samlRequest) {
+                try {
+                    URI requestURI = new BaseSAML2BindingBuilder()
+                      .relayState(relayState)
+                      .redirectBinding(samlRequest)
+                      .requestURI(samlEndpoint.toString());
+                    return new HttpGet(requestURI);
+                } catch (ProcessingException | ConfigurationException | IOException ex) {
+                    throw new RuntimeException(ex);
+                }
+            }
+
+            @Override
+            public URI getBindingUri() {
+                return URI.create(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get());
+            }
+        };
+
+        public abstract SAMLDocumentHolder extractResponse(CloseableHttpResponse response) throws IOException;
+        public abstract HttpUriRequest createSamlRequest(URI samlEndpoint, String relayState, Document samlRequest);
+        public abstract URI getBindingUri();
+    }
+
+    public static class RedirectStrategyWithSwitchableFollowRedirect extends LaxRedirectStrategy {
+
+        public boolean redirectable = true;
+
+        @Override
+        protected boolean isRedirectable(String method) {
+            return redirectable && super.isRedirectable(method);
+        }
+
+        public void setRedirectable(boolean redirectable) {
+            this.redirectable = redirectable;
+        }
+    }
+
+    /**
+     * Extracts and parses value of SAMLResponse input field of a form present in the given page.
+     * @param responsePage HTML code of the page
+     * @return
+     */
+    public static SAMLDocumentHolder extractSamlResponseFromForm(String responsePage) {
+        org.jsoup.nodes.Document theResponsePage = Jsoup.parse(responsePage);
+        Elements samlResponses = theResponsePage.select("input[name=SAMLResponse]");
+        assertThat("Checking uniqueness of SAMLResponse input field in the page", samlResponses, hasSize(1));
+
+        Element respElement = samlResponses.first();
+
+        return SAMLRequestParser.parseResponsePostBinding(respElement.val());
+    }
+
+    /**
+     * Extracts and parses value of SAMLResponse query parameter from the given URI.
+     * @param responseUri
+     * @return
+     */
+    public static SAMLDocumentHolder extractSamlResponseFromRedirect(String responseUri) {
+        List<NameValuePair> params = URLEncodedUtils.parse(URI.create(responseUri), "UTF-8");
+
+        String samlResponse = null;
+        for (NameValuePair param : params) {
+            if ("SAMLResponse".equals(param.getName())) {
+                assertThat(samlResponse, nullValue());
+                samlResponse = param.getValue();
+            }
+        }
+
+        return SAMLRequestParser.parseResponseRedirectBinding(samlResponse);
+    }
+
+    /**
+     * Prepares a GET/POST request for logging the given user into the given login page. The login page is expected
+     * to have at least input fields with id "username" and "password".
+     * @param user
+     * @param loginPage
+     * @return
+     */
+    public static HttpUriRequest handleLoginPage(UserRepresentation user, String loginPage) {
+        String username = user.getUsername();
+        String password = getPasswordOf(user);
+        org.jsoup.nodes.Document theLoginPage = Jsoup.parse(loginPage);
+
+        List<NameValuePair> parameters = new LinkedList<>();
+        for (Element form : theLoginPage.getElementsByTag("form")) {
+            String method = form.attr("method");
+            String action = form.attr("action");
+            boolean isPost = method != null && "post".equalsIgnoreCase(method);
+
+            for (Element input : form.getElementsByTag("input")) {
+                if (Objects.equals(input.id(), "username")) {
+                    parameters.add(new BasicNameValuePair(input.attr("name"), username));
+                } else if (Objects.equals(input.id(), "password")) {
+                    parameters.add(new BasicNameValuePair(input.attr("name"), password));
+                } else {
+                    parameters.add(new BasicNameValuePair(input.attr("name"), input.val()));
+                }
+            }
+
+            if (isPost) {
+                HttpPost res = new HttpPost(action);
+
+                UrlEncodedFormEntity formEntity;
+                try {
+                    formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
+                } catch (UnsupportedEncodingException e) {
+                    throw new RuntimeException(e);
+                }
+                res.setEntity(formEntity);
+
+                return res;
+            } else {
+                UriBuilder b = UriBuilder.fromPath(action);
+                for (NameValuePair parameter : parameters) {
+                    b.queryParam(parameter.getName(), parameter.getValue());
+                }
+                return new HttpGet(b.build());
+            }
+        }
+
+        throw new IllegalArgumentException("Invalid login form: " + loginPage);
+    }
+
+    /**
+     * Creates a SAML login request document with the given parameters. See SAML &lt;AuthnRequest&gt; description for more details.
+     * @param issuer
+     * @param assertionConsumerURL
+     * @param destination
+     * @return
+     */
+    public static AuthnRequestType createLoginRequestDocument(String issuer, String assertionConsumerURL, URI destination) {
+        try {
+            SAML2Request samlReq = new SAML2Request();
+            AuthnRequestType loginReq = samlReq.createAuthnRequestType(UUID.randomUUID().toString(), assertionConsumerURL, destination.toString(), issuer);
+
+            return loginReq;
+        } catch (ConfigurationException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/authentication/PasswordPolicy.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/authentication/PasswordPolicy.java
index fc26648..8f8073c 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/authentication/PasswordPolicy.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/authentication/PasswordPolicy.java
@@ -50,7 +50,9 @@ public class PasswordPolicy extends Authentication {
 
     public void removePolicy(Type policy) {
         getPolicyRow(policy).findElement(By.cssSelector("td.kc-action-cell")).click();
-        primaryButton.click();
+        if (primaryButton.isEnabled()) {
+            primaryButton.click();
+        }
         waitForPageToLoad(driver);
     }
 
@@ -60,7 +62,9 @@ public class PasswordPolicy extends Authentication {
 
     public void editPolicy(Type policy, String value) {
         setPolicyValue(policy, value);
-        primaryButton.click();
+        if (primaryButton.isEnabled()) {
+            primaryButton.click();
+        }
         waitForPageToLoad(driver);
     }
 
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 0106590..7480ef8 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
@@ -821,7 +821,7 @@ resource-types=Resource Types
 select-operations.placeholder=Select operations...
 select-resource-types.placeholder=Select resource types...
 resource-path=Resource Path
-resource-path.tooltip=Filter by resource path. Supports wildcards '*' to match a single part of the path and '**' matches multiple parts. For example 'realms/*/clients/asbc' matches client with id asbc in any realm, while or 'realms/master/**' matches anything in the master realm.
+resource-path.tooltip=Filter by resource path. Supports wildcard '*' (for example 'users/*').
 date-(from)=Date (From)
 date-(to)=Date (To)
 authentication-details=Authentication Details
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
index 27be677..e99b03e 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -1228,7 +1228,7 @@ module.controller('LDAPUserStorageCtrl', function($scope, $location, Notificatio
 
     $scope.cancel = function() {
         if ($scope.create) {
-            $location.url("/realms/" + realm.realm + "/user-storage");
+            $location.url("/realms/" + realm.realm + "/user-federation");
         } else {
             $route.reload();
         }