keycloak-uncached
Changes
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/BearerAuthFilter.java 6(+4 -2)
integration/client-cli/admin-cli/pom.xml 177(+177 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshConsoleCallbackImpl.java 117(+117 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshEnhancer.java 41(+41 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/Globals.java 31(+31 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/ValveInputStream.java 89(+89 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractAuthOptionsCmd.java 267(+267 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractGlobalOptionsCmd.java 113(+113 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractRequestCmd.java 435(+435 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AddRolesCmd.java 334(+334 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCmd.java 95(+95 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCredentialsCmd.java 275(+275 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigTruststoreCmd.java 200(+200 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/CreateCmd.java 167(+167 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/DeleteCmd.java 104(+104 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetCmd.java 168(+168 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetRolesCmd.java 325(+325 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/HelpCmd.java 107(+107 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/KcAdmCmd.java 101(+101 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/NewObjectCmd.java 204(+204 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/RemoveRolesCmd.java 334(+334 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/SetPasswordCmd.java 177(+177 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/UpdateCmd.java 165(+165 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/AttributeKey.java 170(+170 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/AttributeOperation.java 58(+58 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/CmdStdinContext.java 44(+44 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigData.java 176(+176 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigHandler.java 28(+28 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigUpdateOperation.java 26(+26 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/FileConfigHandler.java 135(+135 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/InMemoryConfigHandler.java 39(+39 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/RealmConfigData.java 172(+172 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/httpcomponents/HttpDelete.java 39(+39 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/KcAdmMain.java 94(+94 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/ClientOperations.java 29(+29 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/GroupOperations.java 58(+58 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/LocalSearch.java 60(+60 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/RoleOperations.java 129(+129 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/UserOperations.java 96(+96 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AccessibleBufferOutputStream.java 66(+66 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AttributeException.java 39(+39 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AuthUtil.java 202(+202 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ConfigUtil.java 116(+116 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/FilterUtil.java 59(+59 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/Header.java 39(+39 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/Headers.java 55(+55 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HeadersBody.java 72(+72 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HeadersBodyStatus.java 68(+68 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HttpResponseException.java 34(+34 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HttpUtil.java 450(+450 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/IoUtil.java 255(+255 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OsArch.java 71(+71 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OsUtil.java 64(+64 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OutputFormat.java 25(+25 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OutputUtil.java 107(+107 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ParseUtil.java 111(+111 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ReflectionUtil.java 228(+228 -0)
integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ReturnFields.java 333(+333 -0)
integration/client-cli/admin-cli/src/main/resources/META-INF/services/javax.ws.rs.ext.Providers 23(+23 -0)
integration/client-cli/admin-cli/src/test/java/org/keycloak/client/admin/cli/util/MergeAttributesTest.java 101(+101 -0)
integration/client-cli/admin-cli/src/test/java/org/keycloak/client/admin/cli/util/ReturnFieldsTest.java 142(+142 -0)
integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigTruststoreCmd.java 20(+18 -2)
integration/client-cli/pom.xml 11(+11 -0)
pom.xml 5(+5 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExec.java 245(+245 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExecBuilder.java 44(+44 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/ExecutionException.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/InteractiveInputStream.java 120(+120 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/LoggingOutputStream.java 53(+53 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/NullInputStream.java 12(+12 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/StreamReaderThread.java 33(+33 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcAdmExec.java 58(+58 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcRegExec.java 443(+14 -429)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/AbstractCliTest.java 54(+54 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/AbstractAdmCliTest.java 387(+387 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmCreateTest.java 131(+131 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTest.java 561(+561 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTruststoreTest.java 115(+115 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmUpdateTest.java 130(+130 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/AbstractRegCliTest.java 43(+2 -41)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegConfigTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegCreateTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTest.java 6(+3 -3)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTruststoreTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTest.java 2(+1 -1)
Details
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/Config.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/Config.java
index 4e628e5..183f486 100644
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/Config.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/Config.java
@@ -110,7 +110,7 @@ public class Config {
     }
 
     public static void checkGrantType(String grantType) {
-        if (!PASSWORD.equals(grantType) && !CLIENT_CREDENTIALS.equals(grantType)) {
+        if (grantType != null && !PASSWORD.equals(grantType) && !CLIENT_CREDENTIALS.equals(grantType)) {
             throw new IllegalArgumentException("Unsupported grantType: " + grantType +
                     " (only " + PASSWORD + " and " + CLIENT_CREDENTIALS + " are supported)");
         }
                diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java
index d267d17..8c4235c 100755
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java
@@ -43,18 +43,22 @@ import static org.keycloak.OAuth2Constants.PASSWORD;
 public class Keycloak {
     private final Config config;
     private final TokenManager tokenManager;
+    private String authToken;
     private final ResteasyWebTarget target;
     private final ResteasyClient client;
 
-    Keycloak(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, String grantType, ResteasyClient resteasyClient) {
+    Keycloak(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, String grantType, ResteasyClient resteasyClient, String authtoken) {
         config = new Config(serverUrl, realm, username, password, clientId, clientSecret, grantType);
         client = resteasyClient != null ? resteasyClient : new ResteasyClientBuilder().connectionPoolSize(10).build();
-
-        tokenManager = new TokenManager(config, client);
+        authToken = authtoken;
+        tokenManager = authtoken == null ? new TokenManager(config, client) : null;
 
         target = client.target(config.getServerUrl());
+        target.register(newAuthFilter());
+    }
 
-        target.register(new BearerAuthFilter(tokenManager));
+    private BearerAuthFilter newAuthFilter() {
+        return authToken != null ? new BearerAuthFilter(authToken) : new BearerAuthFilter(tokenManager);
     }
 
     public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, SSLContext sslContext) {
@@ -63,15 +67,19 @@ public class Keycloak {
                 .hostnameVerification(ResteasyClientBuilder.HostnameVerificationPolicy.WILDCARD)
                 .connectionPoolSize(10).build();
 
-        return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, client);
+        return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, client, null);
     }
 
     public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret) {
-        return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, null);
+        return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, null, null);
     }
 
     public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId) {
-        return new Keycloak(serverUrl, realm, username, password, clientId, null, PASSWORD, null);
+        return new Keycloak(serverUrl, realm, username, password, clientId, null, PASSWORD, null, null);
+    }
+
+    public static Keycloak getInstance(String serverUrl, String realm, String clientId, String authtoken) {
+        return new Keycloak(serverUrl, realm, null, null, clientId, null, PASSWORD, null, null);
     }
 
     public RealmsResource realms() {
@@ -100,7 +108,7 @@ public class Keycloak {
      * @return
      */
     public <T> T proxy(Class<T> proxyClass, URI absoluteURI) {
-        return client.target(absoluteURI).register(new BearerAuthFilter(tokenManager)).proxy(proxyClass);
+        return client.target(absoluteURI).register(newAuthFilter()).proxy(proxyClass);
     }
 
     /**
                diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/KeycloakBuilder.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/KeycloakBuilder.java
index e192d9a..0c003d3 100644
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/KeycloakBuilder.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/KeycloakBuilder.java
@@ -60,8 +60,9 @@ public class KeycloakBuilder {
     private String password;
     private String clientId;
     private String clientSecret;
-    private String grantType = PASSWORD;
+    private String grantType;
     private ResteasyClient resteasyClient;
+    private String authorization;
 
     public KeycloakBuilder serverUrl(String serverUrl) {
         this.serverUrl = serverUrl;
@@ -104,6 +105,11 @@ public class KeycloakBuilder {
         return this;
     }
 
+    public KeycloakBuilder authorization(String auth) {
+        this.authorization = auth;
+        return this;
+    }
+
     /**
      * Builds a new Keycloak client from this builder.
      */
@@ -116,6 +122,10 @@ public class KeycloakBuilder {
             throw new IllegalStateException("realm required");
         }
 
+        if (authorization == null && grantType == null) {
+            grantType = PASSWORD;
+        }
+
         if (PASSWORD.equals(grantType)) {
             if (username == null) {
                 throw new IllegalStateException("username required");
@@ -130,11 +140,11 @@ public class KeycloakBuilder {
             }
         }
 
-        if (clientId == null) {
+        if (authorization == null && clientId == null) {
             throw new IllegalStateException("clientId required");
         }
 
-        return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, grantType, resteasyClient);
+        return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, grantType, resteasyClient, authorization);
     }
 
     private KeycloakBuilder() {
                diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/BearerAuthFilter.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/BearerAuthFilter.java
index 514aeea..0d4992f 100644
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/BearerAuthFilter.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/BearerAuthFilter.java
@@ -49,8 +49,10 @@ public class BearerAuthFilter implements ClientRequestFilter, ClientResponseFilt
 
     @Override
     public void filter(ClientRequestContext requestContext) throws IOException {
-        String authHeader = AUTH_HEADER_PREFIX + (tokenManager != null ? tokenManager.getAccessTokenString() : tokenString);
-
+        String authHeader = (tokenManager != null ? tokenManager.getAccessTokenString() : tokenString);
+        if (!authHeader.startsWith(AUTH_HEADER_PREFIX)) {
+            authHeader = AUTH_HEADER_PREFIX + authHeader;
+        }
         requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader);
     }
 
                integration/client-cli/admin-cli/pom.xml 177(+177 -0)
diff --git a/integration/client-cli/admin-cli/pom.xml b/integration/client-cli/admin-cli/pom.xml
new file mode 100755
index 0000000..90676bf
--- /dev/null
+++ b/integration/client-cli/admin-cli/pom.xml
@@ -0,0 +1,177 @@
+<?xml version="1.0"?>
+<!--
+  ~ 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.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <parent>
+        <artifactId>keycloak-client-cli-parent</artifactId>
+        <groupId>org.keycloak</groupId>
+        <version>2.5.0.Final-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>keycloak-admin-cli</artifactId>
+    <name>Keycloak Admin CLI</name>
+    <description/>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.jboss.aesh</groupId>
+            <artifactId>aesh</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <!--version>2.4.3</version-->
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>shade</goal>
+                        </goals>
+                        <configuration>
+                            <filters>
+                                <filter>
+                                    <artifact>org.keycloak:keycloak-core</artifact>
+                                    <includes>
+                                        <include>org/keycloak/util/**</include>
+                                        <include>org/keycloak/json/**</include>
+                                        <include>org/keycloak/jose/jws/**</include>
+                                        <include>org/keycloak/jose/jwk/**</include>
+                                        <include>org/keycloak/representations/adapters/config/**</include>
+                                        <include>org/keycloak/representations/adapters/action/**</include>
+                                        <include>org/keycloak/representations/AccessTokenResponse.class</include>
+                                        <!--
+                                        <include>org/keycloak/representations/idm/ClientRepresentation.class</include>
+                                        <include>org/keycloak/representations/idm/RealmRepresentation.class</include>
+                                        <include>org/keycloak/representations/idm/UserRepresentation.class</include>
+                                        <include>org/keycloak/representations/idm/RoleRepresentation.class</include>
+                                        <include>org/keycloak/representations/idm/RoleRepresentation.class</include>
+                                        <include>org/keycloak/representations/idm/RolesRepresentation.class</include>
+                                        <include>org/keycloak/representations/idm/ScopeMappingRepresentation.class</include>
+                                        <include>org/keycloak/representations/idm/UserFederationMapperRepresentation.class</include>
+                                        <include>org/keycloak/representations/idm/ProtocolMapperRepresentation.class</include>
+                                        <include>org/keycloak/representations/idm/IdentityProviderRepresentation.class</include>
+                                        <include>org/keycloak/representations/idm/authorization/**</include>
+                                        -->
+                                        <include>org/keycloak/representations/idm/**</include>
+                                        <include>org/keycloak/representations/JsonWebToken.class</include>
+                                    </includes>
+                                </filter>
+                                <filter>
+                                    <artifact>org.keycloak:keycloak-common</artifact>
+                                    <includes>
+                                        <include>org/keycloak/common/util/**</include>
+                                    </includes>
+                                </filter>
+                                <filter>
+                                    <artifact>org.bouncycastle:bcprov-jdk15on</artifact>
+                                    <includes>
+                                        <include>**/**</include>
+                                    </includes>
+                                </filter>
+                                <filter>
+                                    <artifact>org.bouncycastle:bcpkix-jdk15on</artifact>
+                                    <excludes>
+                                        <exclude>**/**</exclude>
+                                    </excludes>
+                                </filter>
+                                <filter>
+                                    <artifact>com.fasterxml.jackson.core:jackson-core</artifact>
+                                    <includes>
+                                        <include>**/**</include>
+                                    </includes>
+                                </filter>
+                                <filter>
+                                    <artifact>com.fasterxml.jackson.core:jackson-databind</artifact>
+                                    <includes>
+                                        <include>**/**</include>
+                                    </includes>
+                                </filter>
+                                <filter>
+                                    <artifact>com.fasterxml.jackson.core:jackson-annotations</artifact>
+                                    <includes>
+                                        <include>com/fasterxml/jackson/annotation/**</include>
+                                    </includes>
+                                </filter>
+                                <filter>
+                                    <artifact>org.jboss.resteasy:resteasy-client</artifact>
+                                    <includes>
+                                        <include>**/**</include>
+                                    </includes>
+                                </filter>
+                                <filter>
+                                    <artifact>org.jboss.resteasy:resteasy-jaxrs</artifact>
+                                    <includes>
+                                        <include>**/**</include>
+                                    </includes>
+                                </filter>
+                                <filter>
+                                    <artifact>org.jboss.resteasy:resteasy-jackson2-provider</artifact>
+                                    <includes>
+                                        <include>**/**</include>
+                                    </includes>
+                                </filter>
+                                <filter>
+                                    <artifact>org.jboss.spec.javax.ws.rs:jboss-jaxrs-api_2.0_spec</artifact>
+                                    <includes>
+                                        <include>**/**</include>
+                                    </includes>
+                                </filter>
+                                <filter>
+                                    <artifact>*:*</artifact>
+                                    <excludes>
+                                        <exclude>META-INF/*.SF</exclude>
+                                        <exclude>META-INF/*.DSA</exclude>
+                                        <exclude>META-INF/*.RSA</exclude>
+                                    </excludes>
+                                </filter>
+                            </filters>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>1.8</source>
+                    <target>1.8</target>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
                diff --git a/integration/client-cli/admin-cli/src/main/bin/kcadm.bat b/integration/client-cli/admin-cli/src/main/bin/kcadm.bat
new file mode 100644
index 0000000..46dc256
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/bin/kcadm.bat
@@ -0,0 +1,8 @@
+@echo off
+
+if "%OS%" == "Windows_NT" (
+  set "DIRNAME=%~dp0%"
+) else (
+  set DIRNAME=.\
+)
+java %KC_OPTS% -cp %DIRNAME%\client\keycloak-admin-cli-${project.version}.jar org.keycloak.client.admin.cli.KcAdmMain %*
                diff --git a/integration/client-cli/admin-cli/src/main/bin/kcadm.sh b/integration/client-cli/admin-cli/src/main/bin/kcadm.sh
new file mode 100755
index 0000000..26df7e1
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/bin/kcadm.sh
@@ -0,0 +1,23 @@
+#!/bin/sh
+case "`uname`" in
+    CYGWIN*)
+        CFILE = `cygpath "$0"`
+        RESOLVED_NAME=`readlink -f "$CFILE"`
+        ;;
+    Darwin*)
+        RESOLVED_NAME=`readlink "$0"`
+        ;;
+    FreeBSD)
+        RESOLVED_NAME=`readlink -f "$0"`
+        ;;
+    Linux)
+        RESOLVED_NAME=`readlink -f "$0"`
+        ;;
+esac
+
+if [ "x$RESOLVED_NAME" = "x" ]; then
+    RESOLVED_NAME="$0"
+fi
+
+DIRNAME=`dirname "$RESOLVED_NAME"`
+java $KC_OPTS -cp $DIRNAME/client/keycloak-admin-cli-${project.version}.jar org.keycloak.client.admin.cli.KcAdmMain "$@"
\ No newline at end of file
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshConsoleCallbackImpl.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshConsoleCallbackImpl.java
new file mode 100644
index 0000000..fedaaa0
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshConsoleCallbackImpl.java
@@ -0,0 +1,117 @@
+/*
+ * 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.client.admin.cli.aesh;
+
+import org.jboss.aesh.cl.parser.OptionParserException;
+import org.jboss.aesh.cl.result.ResultHandler;
+import org.jboss.aesh.console.AeshConsoleCallback;
+import org.jboss.aesh.console.AeshConsoleImpl;
+import org.jboss.aesh.console.ConsoleOperation;
+import org.jboss.aesh.console.command.CommandNotFoundException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.container.CommandContainer;
+import org.jboss.aesh.console.command.container.CommandContainerResult;
+import org.jboss.aesh.console.command.invocation.AeshCommandInvocation;
+import org.jboss.aesh.console.command.invocation.AeshCommandInvocationProvider;
+import org.jboss.aesh.parser.AeshLine;
+import org.jboss.aesh.parser.ParserStatus;
+
+import java.lang.reflect.Method;
+
+class AeshConsoleCallbackImpl extends AeshConsoleCallback {
+
+    private final AeshConsoleImpl console;
+    private CommandResult result;
+
+    AeshConsoleCallbackImpl(AeshConsoleImpl aeshConsole) {
+        this.console = aeshConsole;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public int execute(ConsoleOperation output) throws InterruptedException {
+        if (output != null && output.getBuffer().trim().length() > 0) {
+            ResultHandler resultHandler = null;
+            //AeshLine aeshLine = Parser.findAllWords(output.getBuffer());
+            AeshLine aeshLine = new AeshLine(output.getBuffer(), Globals.args, ParserStatus.OK, "");
+            try (CommandContainer commandContainer = getCommand(output, aeshLine)) {
+                resultHandler = commandContainer.getParser().getProcessedCommand().getResultHandler();
+                CommandContainerResult ccResult =
+                        commandContainer.executeCommand(aeshLine, console.getInvocationProviders(), console.getAeshContext(),
+                                new AeshCommandInvocationProvider().enhanceCommandInvocation(
+                                new AeshCommandInvocation(console,
+                                    output.getControlOperator(), output.getPid(), this)));
+
+                result = ccResult.getCommandResult();
+
+                if(result == CommandResult.SUCCESS && resultHandler != null)
+                    resultHandler.onSuccess();
+                else if(resultHandler != null)
+                    resultHandler.onFailure(result);
+
+                if (result == CommandResult.FAILURE) {
+                    // we assume the command has already output any error messages
+                    System.exit(1);
+                }
+            } catch (Exception e) {
+                console.stop();
+
+                if (e instanceof OptionParserException) {
+                    System.err.println("Unknown command: " + aeshLine.getWords().get(0));
+                } else {
+                    System.err.println(e.getMessage());
+                }
+                if (Globals.dumpTrace) {
+                    e.printStackTrace();
+                }
+
+                System.exit(1);
+            }
+        }
+        // empty line
+        else if (output != null) {
+            result = CommandResult.FAILURE;
+        }
+        else {
+            //stop();
+            result = CommandResult.FAILURE;
+        }
+
+        if (result == CommandResult.SUCCESS) {
+            return 0;
+        } else {
+            return 1;
+        }
+    }
+
+    private CommandContainer getCommand(ConsoleOperation output, AeshLine aeshLine) throws CommandNotFoundException {
+        Method m;
+        try {
+            m = console.getClass().getDeclaredMethod("getCommand", AeshLine.class, String.class);
+        } catch (NoSuchMethodException e) {
+            throw new RuntimeException("Unexpected error: ", e);
+        }
+
+        m.setAccessible(true);
+
+        try {
+            return (CommandContainer) m.invoke(console, aeshLine, output.getBuffer());
+        } catch (Exception e) {
+            throw new RuntimeException("Unexpected error: ", e);
+        }
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshEnhancer.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshEnhancer.java
new file mode 100644
index 0000000..9e21b18
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshEnhancer.java
@@ -0,0 +1,41 @@
+/*
+ * 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.client.admin.cli.aesh;
+
+import org.jboss.aesh.console.AeshConsoleImpl;
+import org.jboss.aesh.console.Console;
+
+import java.lang.reflect.Field;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class AeshEnhancer {
+
+    public static void enhance(AeshConsoleImpl console) {
+        try {
+            Globals.stdin.setConsole(console);
+
+            Field field = AeshConsoleImpl.class.getDeclaredField("console");
+            field.setAccessible(true);
+            Console internalConsole = (Console) field.get(console);
+            internalConsole.setConsoleCallback(new AeshConsoleCallbackImpl(console));
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to install Aesh enhancement", e);
+        }
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/Globals.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/Globals.java
new file mode 100644
index 0000000..e16a43c
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/Globals.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.aesh;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class Globals {
+
+    public static boolean dumpTrace = false;
+
+    public static ValveInputStream stdin;
+
+    public static List<String> args;
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/ValveInputStream.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/ValveInputStream.java
new file mode 100644
index 0000000..ec2cf4f
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/ValveInputStream.java
@@ -0,0 +1,89 @@
+/*
+ * 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.client.admin.cli.aesh;
+
+import org.jboss.aesh.console.AeshConsoleImpl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * This stream blocks and waits, until there is a stream in the queue.
+ * It reads the stream to the end, then stops Aesh console.
+ *
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class ValveInputStream extends InputStream {
+
+    private BlockingQueue<InputStream> queue = new LinkedBlockingQueue<>(10);
+
+    private InputStream current;
+
+    private AeshConsoleImpl console;
+
+    @Override
+    public int read() throws IOException {
+        if (current == null) {
+            try {
+                current = queue.take();
+            } catch (InterruptedException e) {
+                throw new InterruptedIOException("Signalled to exit");
+            }
+        }
+        int c = current.read();
+        if (c == -1) {
+            //current = null;
+            if (console != null) {
+                console.stop();
+            }
+        }
+
+        return c;
+    }
+
+    /**
+     * For some reason AeshInputStream wants to do blocking read of whole buffers, which for stdin
+     * results in blocked input.
+     */
+    @Override
+    public int read(byte b[], int off, int len) throws IOException {
+        int c = read();
+        if (c == -1) {
+            return c;
+        }
+        b[off] = (byte) c;
+        return 1;
+    }
+
+    public void setInputStream(InputStream is) {
+        if (queue.contains(is)) {
+            return;
+        }
+        queue.add(is);
+    }
+
+    public void setConsole(AeshConsoleImpl console) {
+        this.console = console;
+    }
+
+    public boolean isStdinAvailable() {
+        return console.isRunning();
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractAuthOptionsCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractAuthOptionsCmd.java
new file mode 100644
index 0000000..1473d7f
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractAuthOptionsCmd.java
@@ -0,0 +1,267 @@
+/*
+ * 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.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.admin.cli.config.ConfigData;
+import org.keycloak.client.admin.cli.config.ConfigHandler;
+import org.keycloak.client.admin.cli.config.FileConfigHandler;
+import org.keycloak.client.admin.cli.config.InMemoryConfigHandler;
+import org.keycloak.client.admin.cli.config.RealmConfigData;
+import org.keycloak.client.admin.cli.util.ConfigUtil;
+import org.keycloak.client.admin.cli.util.HttpUtil;
+import org.keycloak.client.admin.cli.util.IoUtil;
+
+import java.io.File;
+
+import static org.keycloak.client.admin.cli.config.FileConfigHandler.setConfigFile;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CLIENT;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.checkAuthInfo;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.checkServerInfo;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
+
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
+
+    @Option(shortName = 'a', name = "admin-root", description = "URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin")
+    String adminRestRoot;
+
+    @Option(name = "config", description = "Path to the config file (~/.keycloak/kcadm.config by default)")
+    String config;
+
+    @Option(name = "no-config", description = "No configuration file should be used, no authentication info should be saved", hasValue = false)
+    boolean noconfig;
+
+    @Option(name = "server", description = "Server endpoint url (e.g. 'http://localhost:8080/auth')")
+    String server;
+
+    @Option(shortName = 'r', name = "target-realm", description = "Realm to target - when it's different than the realm we authenticate against")
+    String targetRealm;
+
+    @Option(name = "realm", description = "Realm name to authenticate against")
+    String realm;
+
+    @Option(name = "client", description = "Realm name to authenticate against")
+    String clientId;
+
+    @Option(name = "user", description = "Username to login with")
+    String user;
+
+    @Option(name = "password", description = "Password to login with (prompted for if not specified and --user is used)")
+    String password;
+
+    @Option(name = "secret", description = "Secret to authenticate the client (prompted for if no --user or --keystore is specified)")
+    String secret;
+
+    @Option(name = "keystore", description = "Path to a keystore containing private key")
+    String keystore;
+
+    @Option(name = "storepass", description = "Keystore password (prompted for if not specified and --keystore is used)")
+    String storePass;
+
+    @Option(name = "keypass", description = "Key password (prompted for if not specified and --keystore is used without --storepass, \n                             otherwise defaults to keystore password)")
+    String keyPass;
+
+    @Option(name = "alias", description = "Alias of the key inside a keystore (defaults to the value of ClientId)")
+    String alias;
+
+    @Option(name = "truststore", description = "Path to a truststore")
+    String trustStore;
+
+    @Option(name = "trustpass", description = "Truststore password (prompted for if not specified and --truststore is used)")
+    String trustPass;
+
+
+    protected void initFromParent(AbstractAuthOptionsCmd parent) {
+
+        super.initFromParent(parent);
+
+        noconfig = parent.noconfig;
+        config = parent.config;
+        server = parent.server;
+        realm = parent.realm;
+        clientId = parent.clientId;
+        user = parent.user;
+        password = parent.password;
+        secret = parent.secret;
+        keystore = parent.keystore;
+        storePass = parent.storePass;
+        keyPass = parent.keyPass;
+        alias = parent.alias;
+        trustStore = parent.trustStore;
+        trustPass = parent.trustPass;
+    }
+
+    protected void applyDefaultOptionValues() {
+        if (clientId == null) {
+            clientId = DEFAULT_CLIENT;
+        }
+    }
+
+    protected boolean noOptions() {
+        return server == null && realm == null && clientId == null && secret == null &&
+                user == null && password == null &&
+                keystore == null && storePass == null && keyPass == null && alias == null &&
+                trustStore == null && trustPass == null && config == null && (args == null || args.size() == 0);
+    }
+
+
+    protected String getTargetRealm(ConfigData config) {
+        return targetRealm != null ? targetRealm : config.getRealm();
+    }
+
+    protected void processGlobalOptions() {
+
+        super.processGlobalOptions();
+
+        if (config != null && noconfig) {
+            throw new RuntimeException("Options --config and --no-config are mutually exclusive");
+        }
+
+        if (!noconfig) {
+            setConfigFile(config != null ? config : ConfigUtil.DEFAULT_CONFIG_FILE_PATH);
+            ConfigUtil.setHandler(new FileConfigHandler());
+        } else {
+            InMemoryConfigHandler handler = new InMemoryConfigHandler();
+            ConfigData data = new ConfigData();
+            initConfigData(data);
+            handler.setConfigData(data);
+            ConfigUtil.setHandler(handler);
+        }
+    }
+
+    protected void setupTruststore(ConfigData configData, CommandInvocation invocation ) {
+
+        if (!configData.getServerUrl().startsWith("https:")) {
+            return;
+        }
+
+        String truststore = trustStore;
+        if (truststore == null) {
+            truststore = configData.getTruststore();
+        }
+
+        if (truststore != null) {
+            String pass = trustPass;
+            if (pass == null) {
+                pass = configData.getTrustpass();
+            }
+            if (pass == null) {
+                pass = IoUtil.readSecret("Enter truststore password: ", invocation);
+            }
+
+            try {
+                HttpUtil.setTruststore(new File(truststore), pass);
+            } catch (Exception e) {
+                throw new RuntimeException("Failed to load truststore: " + truststore, e);
+            }
+        }
+    }
+
+    protected ConfigData ensureAuthInfo(ConfigData config, CommandInvocation commandInvocation) {
+
+        if (requiresLogin()) {
+            // make sure current handler is in-memory handler
+            // restore it at the end
+            ConfigHandler old = ConfigUtil.getHandler();
+            try {
+                // make sure all defaults are initialized after this point
+                applyDefaultOptionValues();
+
+                initConfigData(config);
+                ConfigUtil.setupInMemoryHandler(config);
+
+                ConfigCredentialsCmd login = new ConfigCredentialsCmd();
+                login.initFromParent(this);
+                login.init(config);
+                login.process(commandInvocation);
+
+                // this must be executed before finally block which restores config handler
+                return loadConfig();
+
+            } catch (RuntimeException e) {
+                throw e;
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            } finally {
+                ConfigUtil.setHandler(old);
+            }
+
+        } else {
+            checkAuthInfo(config);
+
+            // make sure all defaults are initialized after this point
+            applyDefaultOptionValues();
+            return loadConfig();
+        }
+    }
+
+    protected boolean requiresLogin() {
+        return user != null || password != null || secret != null || keystore != null
+                || keyPass != null || storePass != null || alias != null;
+    }
+
+    protected ConfigData copyWithServerInfo(ConfigData config) {
+
+        ConfigData result = config.deepcopy();
+
+        if (server != null) {
+            result.setServerUrl(server);
+        }
+        if (realm != null) {
+            result.setRealm(realm);
+        }
+
+        checkServerInfo(result);
+        return result;
+    }
+
+    private void initConfigData(ConfigData data) {
+        if (server != null)
+            data.setServerUrl(server);
+        if (realm != null)
+            data.setRealm(realm);
+        if (trustStore != null)
+            data.setTruststore(trustStore);
+
+        RealmConfigData rdata = data.sessionRealmConfigData();
+        if (clientId != null)
+            rdata.setClientId(clientId);
+        if (secret != null)
+            rdata.setSecret(secret);
+    }
+
+    protected void checkUnsupportedOptions(String ... options) {
+        if (options.length % 2 != 0) {
+            throw new IllegalArgumentException("Even number of argument required");
+        }
+
+        for (int i = 0; i < options.length; i++) {
+            String name = options[i];
+            String value = options[++i];
+
+            if (value != null) {
+                throw new RuntimeException("Unsupported option: " + name);
+            }
+        }
+    }
+
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractGlobalOptionsCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractGlobalOptionsCmd.java
new file mode 100644
index 0000000..c49c83c
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractGlobalOptionsCmd.java
@@ -0,0 +1,113 @@
+/*
+ * 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.client.admin.cli.commands;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.jboss.aesh.cl.Arguments;
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.Command;
+import org.keycloak.client.admin.cli.aesh.Globals;
+import org.keycloak.client.admin.cli.util.FilterUtil;
+import org.keycloak.client.admin.cli.util.ReturnFields;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.List;
+
+import static org.keycloak.client.admin.cli.util.HttpUtil.normalize;
+import static org.keycloak.client.admin.cli.util.IoUtil.printOut;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public abstract class AbstractGlobalOptionsCmd implements Command {
+
+    @Option(shortName = 'x', description = "Print full stack trace when exiting with error", hasValue = false)
+    boolean dumpTrace;
+
+    @Option(name = "help", description = "Print command specific help", hasValue = false)
+    boolean help;
+
+
+    // we don't want Aesh to handle illegal options
+    @Arguments
+    List<String> args;
+
+
+    protected void initFromParent(AbstractGlobalOptionsCmd parent) {
+        dumpTrace = parent.dumpTrace;
+        help = parent.help;
+        args = parent.args;
+    }
+
+    protected void processGlobalOptions() {
+        Globals.dumpTrace = dumpTrace;
+    }
+
+    protected boolean printHelp() {
+        if (help || nothingToDo()) {
+            printOut(help());
+            return true;
+        }
+
+        return false;
+    }
+
+    protected boolean nothingToDo() {
+        return false;
+    }
+
+    protected String help() {
+        return KcAdmCmd.usage();
+    }
+
+    protected String composeAdminRoot(String server) {
+        return normalize(server) + "admin";
+    }
+
+
+    protected void requireValue(Iterator<String> it, String option) {
+        if (!it.hasNext()) {
+            throw new IllegalArgumentException("Option " + option + " requires a value");
+        }
+    }
+
+    protected String extractTypeNameFromUri(String resourceUrl) {
+        String type = extractLastComponentOfUri(resourceUrl);
+        if (type.endsWith("s")) {
+            type = type.substring(0, type.length()-1);
+        }
+        return type;
+    }
+
+    protected String extractLastComponentOfUri(String resourceUrl) {
+        int endPos = resourceUrl.endsWith("/") ? resourceUrl.length()-2 : resourceUrl.length()-1;
+        int pos = resourceUrl.lastIndexOf("/", endPos);
+        pos = pos == -1 ? 0 : pos;
+        return resourceUrl.substring(pos+1, endPos+1);
+    }
+
+    protected JsonNode applyFieldFilter(ObjectMapper mapper, JsonNode rootNode, ReturnFields returnFields) {
+        // construct new JsonNode that satisfies filtering specified by returnFields
+        try {
+            return FilterUtil.copyFilteredObject(rootNode, returnFields);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to apply fields filter", e);
+        }
+    }
+}
                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
new file mode 100644
index 0000000..84d9579
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractRequestCmd.java
@@ -0,0 +1,435 @@
+/*
+ * 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.client.admin.cli.commands;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.admin.cli.common.AttributeOperation;
+import org.keycloak.client.admin.cli.common.CmdStdinContext;
+import org.keycloak.client.admin.cli.config.ConfigData;
+import org.keycloak.client.admin.cli.util.AccessibleBufferOutputStream;
+import org.keycloak.client.admin.cli.util.Header;
+import org.keycloak.client.admin.cli.util.Headers;
+import org.keycloak.client.admin.cli.util.HeadersBody;
+import org.keycloak.client.admin.cli.util.HeadersBodyStatus;
+import org.keycloak.client.admin.cli.util.HttpUtil;
+import org.keycloak.client.admin.cli.util.OutputFormat;
+import org.keycloak.client.admin.cli.util.ReflectionUtil;
+import org.keycloak.client.admin.cli.util.ReturnFields;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.DELETE;
+import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.SET;
+import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
+import static org.keycloak.client.admin.cli.util.HttpUtil.checkSuccess;
+import static org.keycloak.client.admin.cli.util.HttpUtil.composeResourceUrl;
+import static org.keycloak.client.admin.cli.util.HttpUtil.doGet;
+import static org.keycloak.client.admin.cli.util.IoUtil.copyStream;
+import static org.keycloak.client.admin.cli.util.IoUtil.printErr;
+import static org.keycloak.client.admin.cli.util.IoUtil.printOut;
+import static org.keycloak.client.admin.cli.util.OutputUtil.MAPPER;
+import static org.keycloak.client.admin.cli.util.OutputUtil.printAsCsv;
+import static org.keycloak.client.admin.cli.util.ParseUtil.mergeAttributes;
+import static org.keycloak.client.admin.cli.util.ParseUtil.parseFileOrStdin;
+import static org.keycloak.client.admin.cli.util.ParseUtil.parseKeyVal;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd {
+
+    String file;
+
+    String fields;
+
+    boolean printHeaders;
+
+    boolean returnId;
+
+    boolean outputResult;
+
+    boolean compressed;
+
+    boolean unquoted;
+
+    boolean mergeMode;
+
+    boolean noMerge;
+
+    Integer offset;
+
+    Integer limit;
+
+    String format = "json";
+
+    OutputFormat outputFormat;
+
+    String httpVerb;
+
+    Headers headers = new Headers();
+
+    List<AttributeOperation> attrs = new LinkedList<>();
+
+    Map<String, String> filter = new HashMap<>();
+
+    String url = null;
+
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+        try {
+            initOptions();
+
+            if (printHelp()) {
+                return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
+            }
+
+            processGlobalOptions();
+
+            processOptions(commandInvocation);
+
+            return process(commandInvocation);
+        } catch (IllegalArgumentException e) {
+            throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
+        } finally {
+            commandInvocation.stop();
+        }
+    }
+
+    abstract void initOptions();
+
+    abstract String suggestHelp();
+
+
+    void processOptions(CommandInvocation commandInvocation) {
+
+        if (args == null || args.isEmpty()) {
+            throw new IllegalArgumentException("URI not specified");
+        }
+
+        Iterator<String> it = args.iterator();
+
+        while (it.hasNext()) {
+            String option = it.next();
+            switch (option) {
+                case "-s":
+                case "--set": {
+                    if (!it.hasNext()) {
+                        throw new IllegalArgumentException("Option " + option + " requires a value");
+                    }
+                    String[] keyVal = parseKeyVal(it.next());
+                    attrs.add(new AttributeOperation(SET, keyVal[0], keyVal[1]));
+                    break;
+                }
+                case "-d":
+                case "--delete": {
+                    attrs.add(new AttributeOperation(DELETE, it.next()));
+                    break;
+                }
+                case "-h":
+                case "--header": {
+                    requireValue(it, option);
+                    String[] keyVal = parseKeyVal(it.next());
+                    headers.add(keyVal[0], keyVal[1]);
+                    break;
+                }
+                case "-q":
+                case "--query": {
+                    if (!it.hasNext()) {
+                        throw new IllegalArgumentException("Option " + option + " requires a value");
+                    }
+                    String arg = it.next();
+                    String[] keyVal;
+                    if (arg.indexOf("=") == -1) {
+                        keyVal = new String[] {"", arg};
+                    } else {
+                        keyVal = parseKeyVal(arg);
+                    }
+                    filter.put(keyVal[0], keyVal[1]);
+                    break;
+                }
+                default: {
+                    if (url == null) {
+                        url = option;
+                    } else {
+                        throw new IllegalArgumentException("Invalid option: " + option);
+                    }
+                }
+            }
+        }
+
+
+        if (url == null) {
+            throw new IllegalArgumentException("Resource URI not specified");
+        }
+
+        if (outputResult && returnId) {
+            throw new IllegalArgumentException("Options -o and -i are mutually exclusive");
+        }
+
+        try {
+            outputFormat = OutputFormat.valueOf(format.toUpperCase());
+        } catch (Exception e) {
+            throw new RuntimeException("Unsupported output format: " + format);
+        }
+
+        if (mergeMode && noMerge) {
+            throw new IllegalArgumentException("Options --merge and --no-merge are mutually exclusive");
+        }
+
+        if (file == null && attrs.size() > 0 && !noMerge) {
+            mergeMode = true;
+        }
+    }
+
+
+
+    public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+        // see if Content-Type header is explicitly set to non-json value
+        Header ctype = headers.get("content-type");
+
+        InputStream body = null;
+
+        CmdStdinContext<JsonNode> ctx = new CmdStdinContext<>();
+
+        if (file != null) {
+            if (ctype != null && !"application/json".equals(ctype.getValue())) {
+                if ("-".equals(file)) {
+                    body = System.in;
+                } else {
+                    try {
+                        body = new BufferedInputStream(new FileInputStream(file));
+                    } catch (FileNotFoundException e) {
+                        throw new RuntimeException("File not found: " + file);
+                    }
+                }
+            } else {
+                ctx = parseFileOrStdin(file);
+            }
+        }
+
+        ConfigData config = loadConfig();
+        config = copyWithServerInfo(config);
+
+        setupTruststore(config, commandInvocation);
+
+        String auth = null;
+
+        config = ensureAuthInfo(config, commandInvocation);
+        config = copyWithServerInfo(config);
+        if (credentialsAvailable(config)) {
+            auth = ensureToken(config);
+        }
+
+        auth = auth != null ? "Bearer " + auth : null;
+
+        if (auth != null) {
+            headers.addIfMissing("Authorization", auth);
+        }
+
+
+        final String server = config.getServerUrl();
+        final String realm = getTargetRealm(config);
+        final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server);
+
+
+        String resourceUrl = composeResourceUrl(adminRoot, realm, url);
+        String typeName = extractTypeNameFromUri(resourceUrl);
+
+
+        if (filter.size() > 0) {
+            resourceUrl = HttpUtil.addQueryParamsToUri(resourceUrl, filter);
+        }
+
+        headers.addIfMissing("Accept", "application/json");
+
+        if (isUpdate() && mergeMode) {
+            ObjectNode result;
+            HeadersBodyStatus response;
+            try {
+                response = HttpUtil.doGet(resourceUrl, new HeadersBody(headers));
+                checkSuccess(resourceUrl, response);
+
+                ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+                copyStream(response.getBody(), buffer);
+
+                result = MAPPER.readValue(buffer.toByteArray(), ObjectNode.class);
+
+            } catch (IOException e) {
+                throw new RuntimeException("HTTP request error: " + e.getMessage(), e);
+            }
+
+            CmdStdinContext<JsonNode> ctxremote = new CmdStdinContext<>();
+            ctxremote.setResult(result);
+
+            // merge local representation over remote one
+            if (ctx.getResult() != null) {
+                ReflectionUtil.merge(ctx.getResult(), (ObjectNode) ctxremote.getResult());
+            }
+            ctx = ctxremote;
+        }
+
+        if (attrs.size() > 0) {
+            if (body != 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")));
+        }
+
+        ReturnFields returnFields = null;
+
+        if (fields != null) {
+            returnFields = new ReturnFields(fields);
+        }
+
+        // make sure content type is set
+        if (body != null) {
+            headers.addIfMissing("Content-Type", "application/json");
+        }
+
+        LinkedHashMap<String, String> queryParams = new LinkedHashMap<>();
+        if (offset != null) {
+            queryParams.put("first", String.valueOf(offset));
+        }
+        if (limit != null) {
+            queryParams.put("max", String.valueOf(limit));
+        }
+        if (queryParams.size() > 0) {
+            resourceUrl = HttpUtil.addQueryParamsToUri(resourceUrl, queryParams);
+        }
+
+        HeadersBodyStatus response;
+        try {
+            response = HttpUtil.doRequest(httpVerb, resourceUrl, new HeadersBody(headers, body));
+        } catch (IOException e) {
+            throw new RuntimeException("HTTP request error: " + e.getMessage(), e);
+        }
+
+        // output response
+        if (printHeaders) {
+            printOut(response.getStatus());
+            for (Header header : response.getHeaders()) {
+                printOut(header.getName() + ": " + header.getValue());
+            }
+        }
+
+        checkSuccess(resourceUrl, response);
+
+        AccessibleBufferOutputStream abos = new AccessibleBufferOutputStream(System.out);
+        if (response.getBody() == null) {
+            throw new RuntimeException("Internal error - response body should never be null");
+        }
+
+        if (printHeaders) {
+            printOut("");
+        }
+
+
+        Header location = response.getHeaders().get("Location");
+        String id = location != null ? extractLastComponentOfUri(location.getValue()) : null;
+        if (id != null) {
+            if (returnId) {
+                printOut(id);
+            } else if (!outputResult) {
+                printErr("Created new " + typeName + " with id '" + id + "'");
+            }
+        }
+
+        if (outputResult) {
+
+            if (isCreateOrUpdate() && (response.getStatusCode() == 204 || id != null)) {
+                // get object for id
+                headers = new Headers();
+                if (auth != null) {
+                    headers.add("Authorization", auth);
+                }
+                try {
+                    String fetchUrl = id != null ? (resourceUrl + "/" + id) : resourceUrl;
+                    response = doGet(fetchUrl, new HeadersBody(headers));
+                } catch (IOException e) {
+                    throw new RuntimeException("HTTP request error: " + e.getMessage(), e);
+                }
+            }
+
+            Header contentType = response.getHeaders().get("content-type");
+            boolean canPrettyPrint = contentType != null && contentType.getValue().equals("application/json");
+            boolean pretty = !compressed;
+
+            if (canPrettyPrint && (pretty || returnFields != null)) {
+                ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+                copyStream(response.getBody(), buffer);
+
+                try {
+                    JsonNode rootNode = MAPPER.readValue(buffer.toByteArray(), JsonNode.class);
+                    if (returnFields != null) {
+                        rootNode = applyFieldFilter(MAPPER, rootNode, returnFields);
+                    }
+                    if (outputFormat == OutputFormat.JSON) {
+                        // now pretty print it to output
+                        MAPPER.writeValue(abos, rootNode);
+                    } else {
+                        printAsCsv(rootNode, returnFields, unquoted);
+                    }
+                } catch (Exception ignored) {
+                    copyStream(new ByteArrayInputStream(buffer.toByteArray()), abos);
+                }
+            } else {
+                copyStream(response.getBody(), abos);
+            }
+        }
+
+        int lastByte = abos.getLastByte();
+        if (lastByte != -1 && lastByte != 13 && lastByte != 10) {
+            printErr("");
+        }
+
+        return CommandResult.SUCCESS;
+    }
+
+    private boolean isUpdate() {
+        return "put".equals(httpVerb);
+    }
+
+    private boolean isCreateOrUpdate() {
+        return "post".equals(httpVerb) || "put".equals(httpVerb);
+    }
+}
                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
new file mode 100644
index 0000000..fecd042
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AddRolesCmd.java
@@ -0,0 +1,334 @@
+/*
+ * 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.client.admin.cli.commands;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.admin.cli.config.ConfigData;
+import org.keycloak.client.admin.cli.operations.ClientOperations;
+import org.keycloak.client.admin.cli.operations.GroupOperations;
+import org.keycloak.client.admin.cli.operations.RoleOperations;
+import org.keycloak.client.admin.cli.operations.LocalSearch;
+import org.keycloak.client.admin.cli.operations.UserOperations;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
+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.PROMPT;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "add-roles", description = "[ARGUMENTS]")
+public class AddRolesCmd extends AbstractAuthOptionsCmd {
+
+    @Option(name = "uusername", description = "Target user's 'username'")
+    String uusername;
+
+    @Option(name = "uid", description = "Target user's 'id'")
+    String uid;
+
+    @Option(name = "gname", description = "Target group's 'name'")
+    String gname;
+
+    @Option(name = "gpath", description = "Target group's 'path'")
+    String gpath;
+
+    @Option(name = "gid", description = "Target group's 'id'")
+    String gid;
+
+    @Option(name = "cclientid", description = "Target client's 'clientId'")
+    String cclientid;
+
+    @Option(name = "cid", description = "Target client's 'id'")
+    String cid;
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+        List<String> roleNames = new LinkedList<>();
+        List<String> roleIds = new LinkedList<>();
+
+        try {
+            if (printHelp()) {
+                return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
+            }
+
+            processGlobalOptions();
+
+            Iterator<String> it = args.iterator();
+
+            while (it.hasNext()) {
+                String option = it.next();
+                switch (option) {
+                    case "--rolename": {
+                        optionRequiresValueCheck(it, option);
+                        roleNames.add(it.next());
+                        break;
+                    }
+                    case "--roleid": {
+                        optionRequiresValueCheck(it, option);
+                        roleIds.add(it.next());
+                        break;
+                    }
+                    default: {
+                        throw new IllegalArgumentException("Invalid option: " + option);
+                    }
+                }
+            }
+
+            if (uid != null && uusername != null) {
+                throw new IllegalArgumentException("Incompatible options: --uid and --uusername are mutually exclusive");
+            }
+
+            if ((gid != null && gname != null) || (gid != null && gpath != null) || (gname != null && gpath != null)) {
+                throw new IllegalArgumentException("Incompatible options: --gid, --gname and --gpath are mutually exclusive");
+            }
+
+            if (roleNames.isEmpty() && roleIds.isEmpty()) {
+                throw new IllegalArgumentException("No role specified. Use --rolename or --roleid to specify roles");
+            }
+
+            if (cid != null && cclientid != null) {
+                throw new IllegalArgumentException("Incompatible options: --cid and --cclientid 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");
+            }
+
+
+            ConfigData config = loadConfig();
+            config = copyWithServerInfo(config);
+
+            setupTruststore(config, commandInvocation);
+
+            String auth = null;
+
+            config = ensureAuthInfo(config, commandInvocation);
+            config = copyWithServerInfo(config);
+            if (credentialsAvailable(config)) {
+                auth = ensureToken(config);
+            }
+
+            auth = auth != null ? "Bearer " + auth : null;
+
+            final String server = config.getServerUrl();
+            final String realm = getTargetRealm(config);
+            final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server);
+
+
+            if (isUserSpecified()) {
+                if (uid == null) {
+                    uid = UserOperations.getIdFromUsername(adminRoot, realm, auth, uusername);
+                }
+                if (isClientSpecified()) {
+                    // list client roles for a user
+                    if (cid == null) {
+                        cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
+                    }
+
+                    List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
+                    Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
+
+                    // now add all the roles
+                    UserOperations.addClientRoles(adminRoot, realm, auth, uid, cid, new ArrayList<>(rolesToAdd));
+
+                } else {
+
+                    Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
+                            new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
+
+                    // now add all the roles
+                    UserOperations.addRealmRoles(adminRoot, realm, auth, uid, new ArrayList<>(rolesToAdd));
+                }
+
+            } else if (isGroupSpecified()) {
+                if (gname != null) {
+                    gid = GroupOperations.getIdFromName(adminRoot, realm, auth, gname);
+                } else if (gpath != null) {
+                    gid = GroupOperations.getIdFromPath(adminRoot, realm, auth, gpath);
+                }
+                if (isClientSpecified()) {
+                    // list client roles for a group
+                    if (cid == null) {
+                        cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
+                    }
+
+                    List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
+                    Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
+
+                    // now add all the roles
+                    GroupOperations.addClientRoles(adminRoot, realm, auth, gid, cid, new ArrayList<>(rolesToAdd));
+
+                } else {
+
+                    Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
+                            new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
+
+                    // now add all the roles
+                    GroupOperations.addRealmRoles(adminRoot, realm, auth, gid, new ArrayList<>(rolesToAdd));
+                }
+
+            } else {
+
+                throw new IllegalArgumentException("No user nor group specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group");
+            }
+
+            return CommandResult.SUCCESS;
+
+        } catch (IllegalArgumentException e) {
+            throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
+        } finally {
+            commandInvocation.stop();
+        }
+    }
+
+    private Set<ObjectNode> getRoleRepresentations(List<String> roleNames, List<String> roleIds, LocalSearch roleSearch) {
+        Set<ObjectNode> rolesToAdd = new HashSet<>();
+
+        // now we process roles
+        for (String name : roleNames) {
+            ObjectNode r = roleSearch.exactMatchOne(name, "name");
+            if (r == null) {
+                throw new RuntimeException("Role not found for name: " + name);
+            }
+            rolesToAdd.add(r);
+        }
+        for (String id : roleIds) {
+            ObjectNode r = roleSearch.exactMatchOne(id, "id");
+            if (r == null) {
+                throw new RuntimeException("Role not found for id: " + id);
+            }
+            rolesToAdd.add(r);
+        }
+        return rolesToAdd;
+    }
+
+    private void optionRequiresValueCheck(Iterator<String> it, String option) {
+        if (!it.hasNext()) {
+            throw new IllegalArgumentException("Option " + option + " requires a value");
+        }
+    }
+
+    private boolean isClientSpecified() {
+        return cid != null || cclientid != null;
+    }
+
+    private boolean isGroupSpecified() {
+        return gid != null || gname != null || gpath != null;
+    }
+
+    private boolean isUserSpecified() {
+        return uid != null || uusername != null;
+    }
+
+
+    @Override
+    protected boolean nothingToDo() {
+        return noOptions() && uusername == null && uid == null && cclientid == null && (args == null || args.size() == 0);
+    }
+
+    protected String suggestHelp() {
+        return EOL + "Try '" + CMD + " help add-roles' for more information";
+    }
+
+    protected String help() {
+        return usage();
+    }
+
+    public static String usage() {
+        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();
+        out.println("Command to add realm or client roles to a user or group.");
+        out.println();
+        out.println("Use `" + CMD + " config credentials` to establish an authenticated session, or use CREDENTIALS OPTIONS");
+        out.println("to perform one time authentication.");
+        out.println();
+        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();
+        out.println("Arguments:");
+        out.println();
+        out.println("  Global options:");
+        out.println("    -x                    Print full stack trace when exiting with error");
+        out.println("    --config              Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+        out.println("    --truststore PATH     Path to a truststore containing trusted certificates");
+        out.println("    --trustpass PASSWORD  Truststore password (prompted for if not specified and --truststore is used)");
+        out.println("    CREDENTIALS OPTIONS   Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
+        out.println("                          an authenticated sessions. This allows on-the-fly transient authentication that does");
+        out.println("                          not touch a config file.");
+        out.println();
+        out.println("  Command specific options:");
+        out.println("    --uusername           User's 'username'. If more than one user exists with the same username");
+        out.println("                          you'll have to use --uid to specify the target user");
+        out.println("    --uid                 User's 'id' attribute");
+        out.println("    --gname               Group's 'name'. If more than one group exists with the same name you'll have");
+        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("    --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("    -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();
+        out.println("Examples:");
+        out.println();
+        out.println("Add 'offline_access' realm role to a user:");
+        out.println("  " + PROMPT + " " + CMD + " add-roles -r demorealm --uusername testuser --rolename offline_access");
+        out.println();
+        out.println("Add 'realm-management' client roles 'view-users', 'view-clients' and 'view-realm' to a user:");
+        out.println("  " + PROMPT + " " + CMD + " add-roles -r demorealm --uusername testuser --cclientid realm-management --rolename view-users --rolename view-clients --rolename view-realm");
+        out.println();
+        out.println("Add 'uma_authorization' realm role to a group:");
+        out.println("  " + PROMPT + " " + CMD + " add-roles -r demorealm --gname PowerUsers --rolename uma_authorization");
+        out.println();
+        out.println("Add 'realm-management' client roles 'realm-admin' to a group:");
+        out.println("  " + PROMPT + " " + CMD + " add-roles -r demorealm --gname PowerUsers --cclientid realm-management --rolename realm-admin");
+        out.println();
+        out.println();
+        out.println("Use '" + CMD + " help' for general information and a list of commands");
+        return sb.toString();
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCmd.java
new file mode 100644
index 0000000..f297561
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCmd.java
@@ -0,0 +1,95 @@
+/*
+ * 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.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.GroupCommandDefinition;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
+import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
+
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+
+@GroupCommandDefinition(name = "config", description = "COMMAND [ARGUMENTS]", groupCommands = {ConfigCredentialsCmd.class} )
+public class ConfigCmd extends AbstractAuthOptionsCmd {
+
+    public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+        try {
+            if (args != null && args.size() > 0) {
+                String cmd = args.get(0);
+                switch (cmd) {
+                    case "credentials": {
+                        args.remove(0);
+                        ConfigCredentialsCmd command = new ConfigCredentialsCmd();
+                        command.initFromParent(this);
+                        return command.execute(commandInvocation);
+                    }
+                    case "truststore": {
+                        args.remove(0);
+                        ConfigTruststoreCmd command = new ConfigTruststoreCmd();
+                        command.initFromParent(this);
+                        return command.execute(commandInvocation);
+                    }
+                    default: {
+                        if (printHelp()) {
+                            return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
+                        }
+                        throw new IllegalArgumentException("Unknown sub-command: " + cmd + suggestHelp());
+                    }
+                }
+            }
+
+            if (printHelp()) {
+                return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
+            }
+
+            throw new IllegalArgumentException("Sub-command required by '" + CMD + " config' - one of: 'credentials', 'truststore'");
+
+        } finally {
+            commandInvocation.stop();
+        }
+    }
+
+    protected String suggestHelp() {
+        return EOL + "Try '" + CMD + " help config' for more information";
+    }
+
+    protected String help() {
+        return usage();
+    }
+
+    public static String usage() {
+        StringWriter sb = new StringWriter();
+        PrintWriter out = new PrintWriter(sb);
+        out.println("Usage: " + CMD + " config SUB_COMMAND [ARGUMENTS]");
+        out.println();
+        out.println("Where SUB_COMMAND is one of: 'credentials', 'truststore'");
+        out.println();
+        out.println();
+        out.println("Use '" + CMD + " help config SUB_COMMAND' for more info.");
+        out.println("Use '" + CMD + " help' for general information and a list of commands.");
+        return sb.toString();
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCredentialsCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCredentialsCmd.java
new file mode 100644
index 0000000..c84c52e
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCredentialsCmd.java
@@ -0,0 +1,275 @@
+/*
+ * 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.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.admin.cli.config.ConfigData;
+import org.keycloak.client.admin.cli.config.RealmConfigData;
+import org.keycloak.client.admin.cli.util.AuthUtil;
+import org.keycloak.representations.AccessTokenResponse;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.net.URL;
+
+import static org.keycloak.client.admin.cli.util.AuthUtil.getAuthTokens;
+import static org.keycloak.client.admin.cli.util.AuthUtil.getAuthTokensByJWT;
+import static org.keycloak.client.admin.cli.util.AuthUtil.getAuthTokensBySecret;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.getHandler;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.saveTokens;
+import static org.keycloak.client.admin.cli.util.IoUtil.printErr;
+import static org.keycloak.client.admin.cli.util.IoUtil.readSecret;
+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;
+
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "credentials", description = "--server SERVER_URL --realm REALM [ARGUMENTS]")
+public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd {
+
+    private int sigLifetime = 600;
+
+
+    public void init(ConfigData configData) {
+        if (server == null) {
+            server = configData.getServerUrl();
+        }
+        if (realm == null) {
+            realm = configData.getRealm();
+        }
+        if (trustStore == null) {
+            trustStore = configData.getTruststore();
+        }
+
+        RealmConfigData rdata = configData.getRealmConfigData(server, realm);
+        if (rdata == null) {
+            return;
+        }
+
+        if (clientId == null) {
+            clientId = rdata.getClientId();
+        }
+    }
+
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+        try {
+            if (printHelp()) {
+                return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
+            }
+
+            processGlobalOptions();
+
+            return process(commandInvocation);
+        } catch (IllegalArgumentException e) {
+            throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
+        } finally {
+            commandInvocation.stop();
+        }
+    }
+
+    @Override
+    protected boolean nothingToDo() {
+        return noOptions();
+    }
+
+    public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+        // check server
+        if (server == null) {
+            throw new IllegalArgumentException("Required option not specified: --server");
+        }
+
+        try {
+            new URL(server);
+        } catch (Exception e) {
+            throw new RuntimeException("Invalid server endpoint url: " + server, e);
+        }
+
+        if (realm == null)
+            throw new IllegalArgumentException("Required option not specified: --realm");
+
+        String signedRequestToken = null;
+        boolean clientSet = clientId != null;
+
+        applyDefaultOptionValues();
+
+        if (user != null) {
+            printErr("Logging into " + server + " as user " + user + " of realm " + realm);
+
+            // if user was set there needs to be a password so we can authenticate
+            if (password == null) {
+                password = readSecret("Enter password: ", commandInvocation);
+            }
+            // if secret was set to be read from stdin, then ask for it
+            if ("-".equals(secret) && keystore == null) {
+                secret = readSecret("Enter client secret: ", commandInvocation);
+            }
+        } else if (keystore != null || secret != null || clientSet) {
+            printErr("Logging into " + server + " as " + "service-account-" + clientId + " of realm " + realm);
+            if (keystore == null) {
+                if (secret == null) {
+                    secret = readSecret("Enter client secret: ", commandInvocation);
+                }
+            }
+        }
+
+        if (keystore != null) {
+            if (secret != null) {
+                throw new IllegalArgumentException("Can't use both --keystore and --secret");
+            }
+
+            if (!new File(keystore).isFile()) {
+                throw new RuntimeException("No such keystore file: " + keystore);
+            }
+
+            if (storePass == null) {
+                storePass = readSecret("Enter keystore password: ", commandInvocation);
+                keyPass = readSecret("Enter key password: ", commandInvocation);
+            }
+
+            if (keyPass == null) {
+                keyPass = storePass;
+            }
+
+            if (alias == null) {
+                alias = clientId;
+            }
+
+            String realmInfoUrl = server + "/realms/" + realm;
+
+            signedRequestToken = AuthUtil.getSignedRequestToken(keystore, storePass, keyPass,
+                    alias, sigLifetime, clientId, realmInfoUrl);
+        }
+
+        // if only server and realm are set, just save config and be done
+        if (user == null && secret == null && keystore == null) {
+            getHandler().saveMergeConfig(config -> {
+                config.setServerUrl(server);
+                config.setRealm(realm);
+            });
+            return CommandResult.SUCCESS;
+        }
+
+        setupTruststore(copyWithServerInfo(loadConfig()), commandInvocation);
+
+        // now use the token endpoint to retrieve access token, and refresh token
+        AccessTokenResponse tokens = signedRequestToken != null ?
+                getAuthTokensByJWT(server, realm, user, password, clientId, signedRequestToken) :
+                secret != null ?
+                        getAuthTokensBySecret(server, realm, user, password, clientId, secret) :
+                        getAuthTokens(server, realm, user, password, clientId);
+
+        Long sigExpiresAt = signedRequestToken == null ? null : System.currentTimeMillis() + sigLifetime * 1000;
+
+        // save tokens to config file
+        saveTokens(tokens, server, realm, clientId, signedRequestToken, sigExpiresAt, secret);
+
+        return CommandResult.SUCCESS;
+    }
+
+    protected String suggestHelp() {
+        return EOL + "Try '" + CMD + " help config credentials' for more information";
+    }
+
+    protected String help() {
+        return usage();
+    }
+
+    public static String usage() {
+        StringWriter sb = new StringWriter();
+        PrintWriter out = new PrintWriter(sb);
+        out.println("Usage: " + CMD + " config credentials --server SERVER_URL --realm REALM --user USER [--password PASSWORD] [ARGUMENTS]");
+        out.println("       " + CMD + " config credentials --server SERVER_URL --realm REALM --client CLIENT_ID [--secret SECRET] [ARGUMENTS]");
+        out.println("       " + CMD + " config credentials --server SERVER_URL --realm REALM --client CLIENT_ID [--keystore KEYSTORE] [ARGUMENTS]");
+        out.println();
+        out.println("Command to establish an authenticated client session with the server. There are many authentication");
+        out.println("options available, and it depends on server side client authentication configuration how client can or should authenticate.");
+        out.println("The information always required includes --server, and --realm. Then, --user and / or --client need to be used to authenticate.");
+        out.println("If --client is not provided it defaults to 'admin-cli'. The authentication options / requirements depend on how this client is configured.");
+        out.println();
+        out.println("If confidential client authentication is also configured, you may have to specify a client id, and client credentials in addition to");
+        out.println("user credentials. Client credentials are either a client secret, or a keystore information to use Signed JWT mechanism.");
+        out.println("If only client credentials are provided, and no user credentials, then the service account is used for login.");
+        out.println();
+        out.println("Arguments:");
+        out.println();
+        out.println("  Global options:");
+        out.println("    -x                      Print full stack trace when exiting with error");
+        out.println("    --config                Path to a config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+        out.println("    --truststore PATH       Path to a truststore containing trusted certificates");
+        out.println("    --trustpass PASSWORD    Truststore password (prompted for if not specified and --truststore is used)");
+        out.println();
+        out.println("  Command specific options:");
+        out.println("    --server SERVER_URL     Server endpoint url (e.g. 'http://localhost:8080/auth')");
+        out.println("    --realm REALM           Realm name to use");
+        out.println("    --user USER             Username to login with");
+        out.println("    --password PASSWORD     Password to login with (prompted for if not specified and --user is used)");
+        out.println("    --client CLIENT_ID      ClientId used by this client tool ('admin-cli' by default)");
+        out.println("    --secret SECRET         Secret to authenticate the client (prompted for if --client is specified, and no --keystore is specified)");
+        out.println("    --keystore PATH         Path to a keystore containing private key");
+        out.println("    --storepass PASSWORD    Keystore password (prompted for if not specified and --keystore is used)");
+        out.println("    --keypass PASSWORD      Key password (prompted for if not specified and --keystore is used without --storepass,");
+        out.println("                            otherwise defaults to keystore password)");
+        out.println("    --alias ALIAS           Alias of the key inside a keystore (defaults to the value of ClientId)");
+        out.println();
+        out.println();
+        out.println("Examples:");
+        out.println();
+        out.println("Login as 'admin' user of 'master' realm to a local Keycloak server running on default port.");
+        out.println("You will be prompted for a password:");
+        out.println("  " + PROMPT + " " + CMD + " config credentials --server http://localhost:8080/auth --realm master --user admin");
+        out.println();
+        out.println("Login to Keycloak server at non-default endpoint passing the password via standard input:");
+        if (OS_ARCH.isWindows()) {
+            out.println("  " + PROMPT + " echo mypassword | " + CMD + " config credentials --server http://localhost:9080/auth --realm master --user admin");
+        } else {
+            out.println("  " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --user admin << EOF");
+            out.println("  mypassword");
+            out.println("  EOF");
+        }
+        out.println();
+        out.println("Login specifying a password through command line:");
+        out.println("  " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --user admin --password " + OS_ARCH.envVar("PASSWORD"));
+        out.println();
+        out.println("Login using a client service account of a custom client. You will be prompted for a client secret:");
+        out.println("  " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --client reg-cli");
+        out.println();
+        out.println("Login using a client service account of a custom client, authenticating with signed JWT.");
+        out.println("You will be prompted for a keystore password, and a key password:");
+        out.println("  " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --client reg-cli --keystore " + OS_ARCH.path("~/.keycloak/keystore.jks"));
+        out.println();
+        out.println("Login as 'user' while also authenticating a custom client with signed JWT.");
+        out.println("You will be prompted for a user password, a keystore password, and a key password:");
+        out.println("  " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --user user --client reg-cli --keystore " + OS_ARCH.path("~/.keycloak/keystore.jks"));
+        out.println();
+        out.println();
+        out.println("Use '" + CMD + " help' for general information and a list of commands");
+        return sb.toString();
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigTruststoreCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigTruststoreCmd.java
new file mode 100644
index 0000000..38f7b9f
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigTruststoreCmd.java
@@ -0,0 +1,200 @@
+/*
+ * 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.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.saveMergeConfig;
+import static org.keycloak.client.admin.cli.util.IoUtil.readSecret;
+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;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "truststore", description = "PATH [ARGUMENTS]")
+public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd {
+
+    private ConfigCmd parent;
+
+    private boolean delete;
+
+
+    protected void initFromParent(ConfigCmd parent) {
+        this.parent = parent;
+        super.initFromParent(parent);
+    }
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+        try {
+            if (printHelp()) {
+                return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
+            }
+
+            return process(commandInvocation);
+
+        } catch (IllegalArgumentException e) {
+            throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
+        } finally {
+            commandInvocation.stop();
+        }
+    }
+
+    @Override
+    protected boolean nothingToDo() {
+        return noOptions();
+    }
+
+    public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+        List<String> args = new ArrayList<>();
+
+        Iterator<String> it = parent.args.iterator();
+
+        while (it.hasNext()) {
+            String arg = it.next();
+            switch (arg) {
+                case "-d":
+                case "--delete": {
+                    delete = true;
+                    break;
+                }
+                default: {
+                    args.add(arg);
+                }
+            }
+        }
+
+        if (args.size() > 1) {
+            throw new IllegalArgumentException("Invalid option: " + args.get(1));
+        }
+
+        String truststore = null;
+        if (args.size() > 0) {
+            truststore = args.get(0);
+        }
+
+        checkUnsupportedOptions("--server", server,
+                "--realm", realm,
+                "--client", clientId,
+                "--user", user,
+                "--password", password,
+                "--secret", secret,
+                "--truststore", trustStore,
+                "--keystore", keystore,
+                "--keypass", keyPass,
+                "--alias", alias);
+
+        // now update the config
+        processGlobalOptions();
+
+        String store;
+        String pass;
+
+        if (!delete) {
+
+            if (truststore == null) {
+                throw new IllegalArgumentException("No truststore specified");
+            }
+
+            if (!new File(truststore).isFile()) {
+                throw new RuntimeException("Truststore file not found: " + truststore);
+            }
+
+            if ("-".equals(trustPass)) {
+                trustPass = readSecret("Enter truststore password: ", commandInvocation);
+            }
+
+            store = truststore;
+            pass = trustPass;
+
+        } else {
+            if (truststore != null) {
+                throw new IllegalArgumentException("Option --delete is mutually exclusive with specifying a TRUSTSTORE");
+            }
+            if (trustPass != null) {
+                throw new IllegalArgumentException("Options --trustpass and --delete are mutually exclusive");
+            }
+            store = null;
+            pass = null;
+        }
+
+        saveMergeConfig(config -> {
+            config.setTruststore(store);
+            config.setTrustpass(pass);
+        });
+
+        return CommandResult.SUCCESS;
+    }
+
+    protected String suggestHelp() {
+        return EOL + "Try '" + CMD + " help config truststore' for more information";
+    }
+
+    protected String help() {
+        return usage();
+    }
+
+    public static String usage() {
+        StringWriter sb = new StringWriter();
+        PrintWriter out = new PrintWriter(sb);
+        out.println("Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWORD] [ARGUMENTS]");
+        out.println();
+        out.println("Command to configure a global truststore to use when using https to connect to Keycloak server.");
+        out.println();
+        out.println("Arguments:");
+        out.println();
+        out.println("  Global options:");
+        out.println("    -x                      Print full stack trace when exiting with error");
+        out.println("    --config                Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+        out.println();
+        out.println("  Command specific options:");
+        out.println("    TRUSTSTORE              Path to truststore file");
+        out.println("    --trustpass PASSWORD    Truststore password to unlock truststore (prompted for if set to '-')");
+        out.println("    -d, --delete            Remove truststore configuration");
+        out.println();
+        out.println();
+        out.println("Examples:");
+        out.println();
+        out.println("Specify a truststore - you will be prompted for truststore password every time it is used:");
+        out.println("  " + PROMPT + " " + CMD + " config truststore " + OS_ARCH.path("~/.keycloak/truststore.jks"));
+        out.println();
+        out.println("Specify a truststore, and password - truststore will automatically be used without prompting for password:");
+        out.println("  " + PROMPT + " " + CMD + " config truststore --storepass " + OS_ARCH.envVar("PASSWORD") + " " + OS_ARCH.path("~/.keycloak/truststore.jks"));
+        out.println();
+        out.println("Remove truststore configuration:");
+        out.println("  " + PROMPT + " " + CMD + " config truststore --delete");
+        out.println();
+        out.println();
+        out.println("Use '" + CMD + " help' for general information and a list of commands");
+        return sb.toString();
+    }
+}
                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
new file mode 100644
index 0000000..63591f1
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/CreateCmd.java
@@ -0,0 +1,167 @@
+/*
+ * 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.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+
+import java.io.PrintWriter;
+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;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@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)
+    String file;
+
+    @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;
+
+    @Option(shortName = 'H', name = "print-headers", description = "Print response headers", hasValue = false)
+    boolean printHeaders;
+
+    @Option(shortName = 'i', name = "id", description = "After creation only print id of created resource to standard output", hasValue = false)
+    boolean returnId = false;
+
+    @Option(shortName = 'o', name = "output", description = "After creation output the new resource to standard output", hasValue = false)
+    boolean outputResult = false;
+
+    @Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false)
+    boolean compressed = false;
+
+    //@OptionGroup(shortName = 's', name = "set", description = "Set attribute to the specified value")
+    //Map<String, String> attributes = new LinkedHashMap<>();
+
+    @Override
+    void initOptions() {
+        // set options on parent
+        super.file = file;
+        super.fields = fields;
+        super.printHeaders = printHeaders;
+        super.returnId = returnId;
+        super.outputResult = outputResult;
+        super.compressed = compressed;
+        super.httpVerb = "post";
+    }
+
+    @Override
+    protected boolean nothingToDo() {
+        return noOptions() && file == null && (args == null || args.size() == 0);
+    }
+
+    protected String suggestHelp() {
+        return EOL + "Try '" + CMD + " help create' for more information";
+    }
+
+    protected String help() {
+        return usage();
+    }
+
+    public static String usage() {
+        StringWriter sb = new StringWriter();
+        PrintWriter out = new PrintWriter(sb);
+        out.println("Usage: " + CMD + " create ENDPOINT_URI [ARGUMENTS]");
+        out.println();
+        out.println("Command to create new resources on the server.");
+        out.println();
+        out.println("Use `" + CMD + " config credentials` to establish an authenticated sessions, or use CREDENTIALS OPTIONS");
+        out.println("to perform one time authentication.");
+        out.println();
+        out.println("Arguments:");
+        out.println();
+        out.println("  Global options:");
+        out.println("    -x                    Print full stack trace when exiting with error");
+        out.println("    --config              Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+        out.println("    --truststore PATH     Path to a truststore containing trusted certificates");
+        out.println("    --trustpass PASSWORD  Truststore password (prompted for if not specified and --truststore is used)");
+        out.println("    CREDENTIALS OPTIONS   Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
+        out.println("                          an authenticated sessions. This allows on-the-fly transient authentication that does");
+        out.println("                          not touch a config file.");
+        out.println();
+        out.println("  Command specific options:");
+        out.println("    ENDPOINT_URI              URI used to compose a target resource url. Commonly used values are:");
+        out.println("                              realms, users, roles, groups, clients, keys, serverinfo, components ...");
+        out.println("                              If it starts with 'http://' then it will be used as target resource url");
+        out.println("    -r, --target-realm REALM  Target realm to issue requests against if not the one authenticated against");
+        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("    -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();
+        out.println("    -H, --print-headers       Print response headers");
+        out.println("    -o, --output              After creation output the new resource to standard output");
+        out.println("    -i, --id                  After creation only print id of the new resource to standard output");
+        out.println("    -F, --fields FILTER       A filter pattern to specify which fields of a JSON response to output");
+        out.println("    -c, --compressed          Don't pretty print the output");
+        out.println("    -a, --admin-root URL      URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin");
+        out.println();
+        out.println();
+        out.println("Nested attributes are supported by using '.' to separate components of a KEY. Optionaly, the KEY components ");
+        out.println("can be quoted with double quotes - e.g. my_client.attributes.\"external.user.id\". If VALUE starts with [ and ");
+        out.println("ends with ] the attribute will be set as a JSON array. If VALUE starts with { and ends with } the attribute ");
+        out.println("will be set as a JSON object. If KEY ends with an array index - e.g. clients[3]=VALUE - then the specified item");
+        out.println("of the array is updated. If KEY+=VALUE syntax is used, then KEY is assumed to be an array, and another item is");
+        out.println("added to it.");
+        out.println();
+        out.println("Attributes can also be deleted. If KEY ends with an array index, then the targeted item of an array is removed");
+        out.println("and the following items are shifted.");
+        out.println();
+        out.println();
+        out.println("Examples:");
+        out.println();
+        out.println("Create a new realm:");
+        out.println("  " + PROMPT + " " + CMD + " create realms -s realm=demorealm -s enabled=true");
+        out.println();
+        out.println("Create a new realm role in realm 'demorealm' returning newly created role:");
+        out.println("  " + PROMPT + " " + CMD + " create roles -r demorealm -s name=manage-all -o");
+        out.println();
+        out.println("Create a new user in realm 'demorealm' returning only 'id', and 'username' attributes:");
+        out.println("  " + PROMPT + " " + CMD + " create users -r demorealm -s username=testuser -s enabled=true -o --fields id,username");
+        out.println();
+        out.println("Create a new client using configuration read from standard input:");
+        if (OS_ARCH.isWindows()) {
+            out.println("  " + PROMPT + " echo { \"clientId\": \"my_client\" } | " + CMD + " create clients -r demorealm -f -");
+        } else {
+            out.println("  " + PROMPT + " " + CMD + " create clients -r demorealm  -f - << EOF");
+            out.println("  {");
+            out.println("    \"clientId\": \"my_client\"");
+            out.println("  }");
+            out.println("  EOF");
+        }
+        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");
+        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");
+        out.println();
+        out.println();
+        out.println("Use '" + CMD + " help' for general information and a list of commands");
+        return sb.toString();
+    }
+
+}
                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
new file mode 100644
index 0000000..7ef31e1
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/DeleteCmd.java
@@ -0,0 +1,104 @@
+/*
+ * 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.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+
+import java.io.PrintWriter;
+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.PROMPT;
+
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "delete", description = "CLIENT [GLOBAL_OPTIONS]")
+public class DeleteCmd extends CreateCmd {
+
+    void initOptions() {
+        super.initOptions();
+        httpVerb = "delete";
+    }
+
+    @Override
+    protected boolean nothingToDo() {
+        return noOptions() && (args == null || args.size() == 0);
+    }
+
+    protected String suggestHelp() {
+        return EOL + "Try '" + CMD + " help delete' for more information";
+    }
+
+    protected String help() {
+        return usage();
+    }
+
+    public static String usage() {
+        StringWriter sb = new StringWriter();
+        PrintWriter out = new PrintWriter(sb);
+        out.println("Usage: " + CMD + " delete ENDPOINT_URI [ARGUMENTS]");
+        out.println();
+        out.println("Command to delete resources on the server.");
+        out.println();
+        out.println("Use `" + CMD + " config credentials` to establish an authenticated sessions, or use CREDENTIALS OPTIONS");
+        out.println("to perform one time authentication.");
+        out.println();
+        out.println("Arguments:");
+        out.println();
+        out.println("  Global options:");
+        out.println("    -x                    Print full stack trace when exiting with error");
+        out.println("    --config              Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+        out.println("    --truststore PATH     Path to a truststore containing trusted certificates");
+        out.println("    --trustpass PASSWORD  Truststore password (prompted for if not specified and --truststore is used)");
+        out.println("    CREDENTIALS OPTIONS   Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
+        out.println("                          an authenticated sessions. This allows on-the-fly transient authentication that does");
+        out.println("                          not touch a config file.");
+        out.println();
+        out.println("  Command specific options:");
+        out.println("    ENDPOINT_URI              URI used to compose a target resource url. Commonly used values start with:");
+        out.println("                              realms/, users/, roles/, groups/, clients/, keys/, components/ ...");
+        out.println("                              If it starts with 'http://' then it will be used as target resource url");
+        out.println("    -r, --target-realm REALM  Target realm to issue requests against if not the one authenticated against");
+        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("    -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();
+        out.println("    -H, --print-headers       Print response headers");
+        out.println("    -o, --output              After delete output any response to standard output");
+        out.println("    -F, --fields FILTER       A filter pattern to specify which fields of a JSON response to output");
+        out.println("    -c, --compressed          Don't pretty print the output");
+        out.println("    -a, --admin-root URL      URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin");
+        out.println();
+        out.println("Examples:");
+        out.println();
+        out.println("Delete a realm role:");
+        out.println("  " + PROMPT + " " + CMD + " delete roles/manage-all -r demorealm");
+        out.println();
+        out.println("Delete a user (replace USER_ID with the value of user's 'id' attribute):");
+        out.println("  " + PROMPT + " " + CMD + " delete users/USER_ID -r demorealm");
+        out.println();
+        out.println();
+        out.println("Use '" + CMD + " help' for general information and a list of commands");
+        return sb.toString();
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetCmd.java
new file mode 100644
index 0000000..27005e4
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetCmd.java
@@ -0,0 +1,168 @@
+/*
+ * 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.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+
+import java.io.PrintWriter;
+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.PROMPT;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "get", description = "[ARGUMENTS]")
+public class GetCmd extends  AbstractRequestCmd {
+
+    @Option(name = "noquotes", description = "", hasValue = false)
+    boolean unquoted;
+
+    @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;
+
+    @Option(shortName = 'H', name = "print-headers", description = "Print response headers", hasValue = false)
+    boolean printHeaders;
+
+    @Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false)
+    boolean compressed;
+
+    @Option(shortName = 'o', name = "offset", description = "Number of results from beginning of resultset to skip")
+    Integer offset;
+
+    @Option(shortName = 'l', name = "limit", description = "Maksimum number of results to return")
+    Integer limit;
+
+    @Option(name = "format", description = "Output format - one of: json, csv", defaultValue = "json")
+    String format;
+
+
+    @Override
+    void initOptions() {
+        // set options on parent
+        super.fields = fields;
+        super.printHeaders = printHeaders;
+        super.returnId = false;
+        super.outputResult = true;
+        super.compressed = compressed;
+        super.offset = offset;
+        super.limit = limit;
+        super.format = format;
+        super.unquoted = unquoted;
+        super.httpVerb = "get";
+    }
+
+    @Override
+    protected boolean nothingToDo() {
+        return noOptions() && (args == null || args.size() == 0);
+    }
+
+    protected String suggestHelp() {
+        return EOL + "Try '" + CMD + " help get' for more information";
+    }
+
+    protected String help() {
+        return usage();
+    }
+
+    public static String usage() {
+        StringWriter sb = new StringWriter();
+        PrintWriter out = new PrintWriter(sb);
+        out.println("Usage: " + CMD + " get ENDPOINT_URI [ARGUMENTS]");
+        out.println();
+        out.println("Command to retrieve existing resources from the server.");
+        out.println();
+        out.println("Use `" + CMD + " config credentials` to establish an authenticated session, or use CREDENTIALS OPTIONS");
+        out.println("to perform one time authentication.");
+        out.println();
+        out.println("Arguments:");
+        out.println();
+        out.println("  Global options:");
+        out.println("    -x                    Print full stack trace when exiting with error");
+        out.println("    --config              Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+        out.println("    --truststore PATH     Path to a truststore containing trusted certificates");
+        out.println("    --trustpass PASSWORD  Truststore password (prompted for if not specified and --truststore is used)");
+        out.println("    CREDENTIALS OPTIONS   Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
+        out.println("                          an authenticated sessions. This allows on-the-fly transient authentication that does");
+        out.println("                          not touch a config file.");
+        out.println();
+        out.println("  Command specific options:");
+        out.println("    ENDPOINT_URI              URI used to compose a target resource url. Commonly used values are:");
+        out.println("                              realms, users, roles, groups, clients, keys, serverinfo, components ...");
+        out.println("                              If it starts with 'http://' then it will be used as target resource url");
+        out.println("    -r, --target-realm REALM  Target realm to issue requests against if not the one authenticated against");
+        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("    -o, --offset OFFSET       Set paging offset - adds a query parameter 'first' which some endpoints recognize");
+        out.println("    -l, --limit LIMIT         Set limit to number of items in result - adds a query parameter 'max' ");
+        out.println("                              which some endpoints recognize");
+        out.println();
+        out.println("    -H, --print-headers       Print response headers");
+        out.println("    -o, --output              After delete output any response to standard output");
+        out.println("    -F, --fields FILTER       A filter pattern to specify which fields of a JSON response to output");
+        out.println("    -c, --compressed          Don't pretty print the output");
+        out.println("    --format FORMAT           Set output format to comma-separated-values by using 'csv'. Default format is 'json'");
+        out.println("    --noquotes                Don't quote strings when output format is 'csv'");
+        out.println("    -a, --admin-root URL      URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin");
+        out.println();
+        out.println("Examples:");
+        out.println();
+        out.println("Get all realms, displaying only some of the attributes:");
+        out.println("  " + PROMPT + " " + CMD + " get realms --fields id,realm,enabled");
+        out.println();
+        out.println("Get 'demorealm':");
+        out.println("  " + PROMPT + " " + CMD + " get realms/demorealm");
+        out.println();
+        out.println("Get all configured identity providers in demorealm, displaying only some of the attributes:");
+        out.println("  " + PROMPT + " " + CMD + " get identity-provider/instances -r demorealm --fields alias,providerId,enabled");
+        out.println();
+        out.println("Get all clients in demorealm, displaying only some of the attributes:");
+        out.println("  " + PROMPT + " " + CMD + " get clients -r demorealm --fields 'id,clientId,protocolMappers(id,name,protocol,protocolMapper)'");
+        out.println();
+        out.println("Get specific client in demorealm, and remove 'id', and 'protocolMappers' attributes in order to use");
+        out.println("it as a template (replace ID with client's 'id'):");
+        out.println("  " + PROMPT + " " + CMD + " get clients/ID -r demorealm --fields '*(*),-id,-protocolMappers' > realm-template.json");
+        out.println();
+        out.println("Display first level attributes available on 'serverinfo' resource:");
+        out.println("  " + PROMPT + " " + CMD + " get serverinfo -r demorealm --fields '*'");
+        out.println();
+        out.println("Display system info and memory info:");
+        out.println("  " + PROMPT + " " + CMD + " get serverinfo -r demorealm --fields 'systemInfo(*),memoryInfo(*)'");
+        out.println();
+        out.println("Get adapter configuration for the client (replace ID with client's 'id'):");
+        out.println("  " + PROMPT + " " + CMD + " get clients/ID/installation/providers/keycloak-oidc-keycloak-json -r demorealm");
+        out.println();
+        out.println("Get first 100 users at the most:");
+        out.println("  " + PROMPT + " " + CMD + " get users -r demorealm --offset 0 --limit 100");
+        out.println();
+        out.println("Note: 'users' endpoint knows how to handle --offset and --limit. Most other endpoints don't.");
+        out.println();
+        out.println("Get all users whose 'username' matches '*test*' pattern, and 'email' matches '*@google.com*':");
+        out.println("  " + PROMPT + " " + CMD + " get users -r demorealm -q username=test -q email=@google.com");
+        out.println();
+        out.println("Note: it is the 'users' endpoint that interprets query parameters 'username', and 'email' in such a way that");
+        out.println("it results in the described semantics. Another endpoint may provide a different semantics.");
+        out.println();
+        out.println();
+        out.println("Use '" + CMD + " help' for general information and a list of commands");
+        return sb.toString();
+    }
+}
                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
new file mode 100644
index 0000000..7b0fd5f
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetRolesCmd.java
@@ -0,0 +1,325 @@
+/*
+ * 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.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.admin.cli.config.ConfigData;
+import org.keycloak.client.admin.cli.operations.ClientOperations;
+import org.keycloak.client.admin.cli.operations.GroupOperations;
+import org.keycloak.client.admin.cli.operations.RoleOperations;
+import org.keycloak.client.admin.cli.operations.UserOperations;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+
+import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
+import static org.keycloak.client.admin.cli.util.HttpUtil.composeResourceUrl;
+import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
+import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "get-roles", description = "[ARGUMENTS]")
+public class GetRolesCmd extends GetCmd {
+
+    @Option(name = "uusername", description = "Target user's 'username'")
+    String uusername;
+
+    @Option(name = "uid", description = "Target user's 'id'")
+    String uid;
+
+    @Option(name = "cclientid", description = "Target client's 'clientId'")
+    String cclientid;
+
+    @Option(name = "cid", description = "Target client's 'id'")
+    String cid;
+
+    @Option(name = "rolename", description = "Target role's 'name'")
+    String rname;
+
+    @Option(name = "roleid", description = "Target role's 'id'")
+    String rid;
+
+    @Option(name = "gname", description = "Target group's 'name'")
+    String gname;
+
+    @Option(name = "gpath", description = "Target group's 'path'")
+    String gpath;
+
+    @Option(name = "gid", description = "Target group's 'id'")
+    String gid;
+
+    @Option(name = "available", description = "List only available roles", hasValue = false)
+    boolean available;
+
+    @Option(name = "effective", description = "List assigned roles including transitively included roles", hasValue = false)
+    boolean effective;
+
+    @Option(name = "all", description = "List roles for all clients in addition to realm roles", hasValue = false)
+    boolean all;
+
+
+    void initOptions() {
+
+        super.initOptions();
+
+        // hack args so that GetCmd option check doesn't fail
+        // set a placeholder
+        if (args == null) {
+            args = new ArrayList();
+        }
+        if (args.size() == 0) {
+            args.add("uri");
+        } else {
+            args.add(0, "uri");
+        }
+    }
+
+    void processOptions(CommandInvocation commandInvocation) {
+
+        if (uid != null && uusername != null) {
+            throw new IllegalArgumentException("Incompatible options: --uid and --uusername are mutually exclusive");
+        }
+
+        if ((gid != null && gname != null) || (gid != null && gpath != null) || (gname != null && gpath != null)) {
+            throw new IllegalArgumentException("Incompatible options: --gid, --gname and --gpath are mutually exclusive");
+        }
+
+        if (rid != null && rname != null) {
+            throw new IllegalArgumentException("Incompatible options: --roleid and --rolename are mutually exclusive");
+        }
+
+        if (cid != null && cclientid != null) {
+            throw new IllegalArgumentException("Incompatible options: --cid and --cclientid 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");
+        }
+
+        super.processOptions(commandInvocation);
+    }
+
+    public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+        ConfigData config = loadConfig();
+        config = copyWithServerInfo(config);
+
+        setupTruststore(config, commandInvocation);
+
+        String auth = null;
+
+        config = ensureAuthInfo(config, commandInvocation);
+        config = copyWithServerInfo(config);
+        if (credentialsAvailable(config)) {
+            auth = ensureToken(config);
+        }
+
+        auth = auth != null ? "Bearer " + auth : null;
+
+        final String server = config.getServerUrl();
+        final String realm = getTargetRealm(config);
+        final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server);
+
+        if (isUserSpecified()) {
+            if (uid == null) {
+                uid = UserOperations.getIdFromUsername(adminRoot, realm, auth, uusername);
+            }
+            if (isClientSpecified()) {
+                // list client roles for a user
+                if (cid == null) {
+                    cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
+                }
+                if (available) {
+                    super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/clients/" + cid + "/available");
+                } else if (effective) {
+                    super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/clients/" + cid + "/composite");
+                } else {
+                    super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/clients/" + cid);
+                }
+            } else {
+                // list realm roles for a user
+                if (available) {
+                    super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/realm/available");
+                } 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");
+                }
+            }
+        } else if (isGroupSpecified()) {
+            if (gname != null) {
+                gid = GroupOperations.getIdFromName(adminRoot, realm, auth, gname);
+            } else if (gpath != null) {
+                gid = GroupOperations.getIdFromPath(adminRoot, realm, auth, gpath);
+            }
+            if (isClientSpecified()) {
+                // list client roles for a group
+                if (cid == null) {
+                    cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
+                }
+                if (available) {
+                    super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/clients/" + cid + "/available");
+                } else if (effective) {
+                    super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/clients/" + cid + "/composite");
+                } else {
+                    super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/clients/" + cid);
+                }
+            } else {
+                // list realm roles for a group
+                if (available) {
+                    super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/realm/available");
+                } 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");
+                }
+            }
+        } else if (isClientSpecified()) {
+            if (cid == null) {
+                cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
+            }
+
+            if (isRoleSpecified()) {
+                // get specific client role
+                if (rname == null) {
+                    rname = RoleOperations.getClientRoleNameFromId(adminRoot, realm, auth, cid, rid);
+                }
+                super.url = composeResourceUrl(adminRoot, realm, "clients/" + cid + "/roles/" + rname);
+            } else {
+                // list defined client roles
+                super.url = composeResourceUrl(adminRoot, realm, "clients/" + cid + "/roles");
+            }
+        } else {
+            if (isRoleSpecified()) {
+                // get specific realm role
+                if (rname == null) {
+                    rname = RoleOperations.getClientRoleNameFromId(adminRoot, realm, auth, cid, rid);
+                }
+                super.url = composeResourceUrl(adminRoot, realm, "roles/" + rname);
+            } else {
+                // list defined realm roles
+                super.url = composeResourceUrl(adminRoot, realm, "roles");
+            }
+        }
+
+        return super.process(commandInvocation);
+    }
+
+    private boolean isRoleSpecified() {
+        return rid != null || rname != null;
+    }
+
+    private boolean isClientSpecified() {
+        return cid != null || cclientid != null;
+    }
+
+    private boolean isGroupSpecified() {
+        return gid != null || gname != null || gpath != null;
+    }
+
+    private boolean isUserSpecified() {
+        return uid != null || uusername != null;
+    }
+
+    protected String suggestHelp() {
+        return "";
+    }
+
+    protected boolean nothingToDo() {
+        return false;
+    }
+
+    protected String help() {
+        return usage();
+    }
+
+    public static String usage() {
+        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();
+        out.println("Command to list realm or client roles on a realm, user or group.");
+        out.println();
+        out.println("Use `" + CMD + " config credentials` to establish an authenticated session, or use CREDENTIALS OPTIONS");
+        out.println("to perform one time authentication.");
+        out.println();
+        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 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();
+        out.println("Arguments:");
+        out.println();
+        out.println("  Global options:");
+        out.println("    -x                    Print full stack trace when exiting with error");
+        out.println("    --config              Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+        out.println("    --truststore PATH     Path to a truststore containing trusted certificates");
+        out.println("    --trustpass PASSWORD  Truststore password (prompted for if not specified and --truststore is used)");
+        out.println("    CREDENTIALS OPTIONS   Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
+        out.println("                          an authenticated sessions. This allows on-the-fly transient authentication that does");
+        out.println("                          not touch a config file.");
+        out.println();
+        out.println("  Command specific options:");
+        out.println("    --uusername               User's 'username'. If more than one user exists with the same username");
+        out.println("                              you'll have to use --uid to specify the target user");
+        out.println("    --uid                     User's 'id' attribute");
+        out.println("    --gname                   Group's 'name'. If more than one group exists with the same name you'll have");
+        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("    --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("    -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();
+        out.println("Examples:");
+        out.println();
+        out.println("Get all realm roles defined on a realm:");
+        out.println("  " + PROMPT + " " + CMD + " get-roles -r demorealm");
+        out.println();
+        out.println("Get all client roles defined on a specific client, displaying only 'id' and 'name':");
+        out.println("  " + PROMPT + " " + CMD + " get-roles -r demorealm --cclientid realm-management --fields id,name");
+        out.println();
+        out.println("List all realm roles for a specific user:");
+        out.println("  " + PROMPT + " " + CMD + " get-roles -r demorealm --uusername testuser");
+        out.println();
+        out.println("List effective client roles for 'realm-management' client for a specific user:");
+        out.println("  " + PROMPT + " " + CMD + " get-roles -r demorealm --uusername testuser --cclientid realm-management --effective");
+        out.println();
+        out.println();
+        out.println("Use '" + CMD + " help' for general information and a list of commands");
+        return sb.toString();
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/HelpCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/HelpCmd.java
new file mode 100644
index 0000000..191f7d9
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/HelpCmd.java
@@ -0,0 +1,107 @@
+/*
+ * 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.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.Arguments;
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.console.command.Command;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+
+import java.util.List;
+
+import static org.keycloak.client.admin.cli.util.IoUtil.printOut;
+
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "help", description = "This help")
+public class HelpCmd implements Command {
+
+    @Arguments
+    List<String> args;
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+        try {
+            if (args == null || args.size() == 0) {
+                printOut(KcAdmCmd.usage());
+            } else {
+                outer:
+                switch (args.get(0)) {
+                    case "config": {
+                        if (args.size() > 1) {
+                            switch (args.get(1)) {
+                                case "credentials": {
+                                    printOut(ConfigCredentialsCmd.usage());
+                                    break outer;
+                                }
+                                case "truststore": {
+                                    printOut(ConfigTruststoreCmd.usage());
+                                    break outer;
+                                }
+                            }
+                        }
+                        printOut(ConfigCmd.usage());
+                        break;
+                    }
+                    case "create": {
+                        printOut(CreateCmd.usage());
+                        break;
+                    }
+                    case "get": {
+                        printOut(GetCmd.usage());
+                        break;
+                    }
+                    case "update": {
+                        printOut(UpdateCmd.usage());
+                        break;
+                    }
+                    case "delete": {
+                        printOut(DeleteCmd.usage());
+                        break;
+                    }
+                    case "get-roles": {
+                        printOut(GetRolesCmd.usage());
+                        break;
+                    }
+                    case "add-roles": {
+                        printOut(AddRolesCmd.usage());
+                        break;
+                    }
+                    case "remove-roles": {
+                        printOut(RemoveRolesCmd.usage());
+                        break;
+                    }
+                    case "set-password": {
+                        printOut(SetPasswordCmd.usage());
+                        break;
+                    }
+                    default: {
+                        throw new RuntimeException("Unknown command: " + args.get(0));
+                    }
+                }
+            }
+
+            return CommandResult.SUCCESS;
+        } finally {
+            commandInvocation.stop();
+        }
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/KcAdmCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/KcAdmCmd.java
new file mode 100644
index 0000000..d621834
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/KcAdmCmd.java
@@ -0,0 +1,101 @@
+/*
+ * 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.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.GroupCommandDefinition;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+
+import java.io.PrintWriter;
+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.IoUtil.printErr;
+import static org.keycloak.client.admin.cli.util.IoUtil.printOut;
+import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
+import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
+
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+
+@GroupCommandDefinition(name = "kcadm", description = "COMMAND [ARGUMENTS]", groupCommands = {
+    HelpCmd.class, ConfigCmd.class, NewObjectCmd.class, CreateCmd.class, GetCmd.class, UpdateCmd.class, DeleteCmd.class,
+        AddRolesCmd.class, RemoveRolesCmd.class, GetRolesCmd.class, SetPasswordCmd.class} )
+public class KcAdmCmd extends AbstractGlobalOptionsCmd {
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+        try {
+            // if --help was requested then status is SUCCESS
+            // if not we print help anyway, but status is FAILURE
+            if (printHelp()) {
+                return CommandResult.SUCCESS;
+            } else if (args != null && args.size() > 0) {
+                printErr("Unknown command: " + args.get(0));
+                return CommandResult.FAILURE;
+            } else {
+                printOut(usage());
+                return CommandResult.FAILURE;
+            }
+        } finally {
+            commandInvocation.stop();
+        }
+    }
+
+    public static String usage() {
+        StringWriter sb = new StringWriter();
+        PrintWriter out = new PrintWriter(sb);
+        out.println("Keycloak Admin CLI");
+        out.println();
+        out.println("Use '" + CMD + " config credentials' command with username and password to start a session against a specific");
+        out.println("server and realm.");
+        out.println();
+        out.println("For example:");
+        out.println();
+        out.println("  " + PROMPT + " " + CMD + " config credentials --server http://localhost:8080/auth --realm master --user admin");
+        out.println("  Enter password: ");
+        out.println("  Logging into http://localhost:8080/auth as user admin of realm master");
+        out.println();
+        out.println("Any configured username can be used for login, but to perform admin operations the user");
+        out.println("needs proper roles, otherwise operations will fail.");
+        out.println();
+        out.println("Usage: " + CMD + " COMMAND [ARGUMENTS]");
+        out.println();
+        out.println("Global options:");
+        out.println("  -x            Print full stack trace when exiting with error");
+        out.println("  --help        Print help for specific command");
+        out.println("  --config      Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+        out.println();
+        out.println("Commands: ");
+        out.println("  config        Set up credentials, and other configuration settings using the config file");
+        out.println("  create        Create new resource");
+        out.println("  get           Get a resource");
+        out.println("  update        Update a resource");
+        out.println("  delete        Delete a resource");
+        out.println("  get-roles     List roles for a user or a group");
+        out.println("  add-roles     Add role to a user or a group");
+        out.println("  remove-roles  Remove role from a user or a group");
+        out.println("  set-password  Re-set password for a user");
+        out.println("  help          This help");
+        out.println();
+        out.println("Use '" + CMD + " help <command>' for more information about a given command.");
+        return sb.toString();
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/NewObjectCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/NewObjectCmd.java
new file mode 100644
index 0000000..33fcdc6
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/NewObjectCmd.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.commands;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.admin.cli.common.AttributeOperation;
+import org.keycloak.client.admin.cli.common.CmdStdinContext;
+import org.keycloak.client.admin.cli.util.AccessibleBufferOutputStream;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.charset.Charset;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.SET;
+import static org.keycloak.client.admin.cli.util.IoUtil.copyStream;
+import static org.keycloak.client.admin.cli.util.IoUtil.printErr;
+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;
+import static org.keycloak.client.admin.cli.util.OutputUtil.MAPPER;
+import static org.keycloak.client.admin.cli.util.ParseUtil.mergeAttributes;
+import static org.keycloak.client.admin.cli.util.ParseUtil.parseFileOrStdin;
+import static org.keycloak.client.admin.cli.util.ParseUtil.parseKeyVal;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "new-object", description = "Command to create new JSON objects locally")
+public class NewObjectCmd extends AbstractGlobalOptionsCmd {
+
+    @Option(shortName = 'f', name = "file", description = "Read object from file or standard input if FILENAME is set to '-'", hasValue = true)
+    String file;
+
+    @Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false)
+    boolean compressed;
+
+    //@OptionGroup(shortName = 's', name = "set", description = "Set attribute to the specified value")
+    //Map<String, String> attributes = new LinkedHashMap<>();
+
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+        try {
+            if (printHelp()) {
+                return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
+            }
+
+            processGlobalOptions();
+
+            return process(commandInvocation);
+        } catch (IllegalArgumentException e) {
+            throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
+        } finally {
+            commandInvocation.stop();
+        }
+    }
+
+    public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+        List<AttributeOperation> attrs = new LinkedList<>();
+
+        Iterator<String> it = args.iterator();
+
+        while (it.hasNext()) {
+            String option = it.next();
+            switch (option) {
+                case "-s":
+                case "--set": {
+                    if (!it.hasNext()) {
+                        throw new IllegalArgumentException("Option " + option + " requires a value");
+                    }
+                    String[] keyVal = parseKeyVal(it.next());
+                    attrs.add(new AttributeOperation(SET, keyVal[0], keyVal[1]));
+                    break;
+                }
+                default: {
+                    throw new IllegalArgumentException("Invalid option: " + option);
+                }
+            }
+        }
+
+        InputStream body = null;
+
+        CmdStdinContext<JsonNode> ctx = new CmdStdinContext<>();
+
+        if (file != null) {
+            ctx = parseFileOrStdin(file);
+        }
+
+        if (attrs.size() > 0) {
+            ctx = mergeAttributes(ctx, MAPPER.createObjectNode(), attrs);
+        }
+
+        if (body == null && ctx.getContent() != null) {
+            body = new ByteArrayInputStream(ctx.getContent().getBytes(Charset.forName("utf-8")));
+        }
+
+        AccessibleBufferOutputStream abos = new AccessibleBufferOutputStream(System.out);
+
+        if (!compressed) {
+            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+            copyStream(body, buffer);
+
+            try {
+                JsonNode rootNode = MAPPER.readValue(buffer.toByteArray(), JsonNode.class);
+                // now pretty print it to output
+                MAPPER.writeValue(abos, rootNode);
+            } catch (Exception ignored) {
+                copyStream(new ByteArrayInputStream(buffer.toByteArray()), abos);
+            }
+        } else {
+            copyStream(body, System.out);
+        }
+
+        int lastByte = abos.getLastByte();
+        if (lastByte != -1 && lastByte != 13 && lastByte != 10) {
+            printErr("");
+        }
+
+        return CommandResult.SUCCESS;
+    }
+
+
+    @Override
+    protected boolean nothingToDo() {
+        return file == null && (args == null || args.size() == 0);
+    }
+
+    protected String suggestHelp() {
+        return EOL + "Try '" + CMD + " help create' for more information";
+    }
+
+    protected String help() {
+        return usage();
+    }
+
+    public static String usage() {
+        StringWriter sb = new StringWriter();
+        PrintWriter out = new PrintWriter(sb);
+        out.println("Usage: " + CMD + " new-object [ARGUMENTS]");
+        out.println();
+        out.println("Command to compose JSON objects from attributes, and merge changes into existing JSON documents.");
+        out.println();
+        out.println("This is a local command that does not perform any server requests. It's functionality is fully ");
+        out.println("integrated into 'create', 'update' and 'delete' commands. It's supposed to be a helper tool only.");
+        out.println();
+        out.println("Arguments:");
+        out.println();
+        out.println("  Global options:");
+        out.println("    -x                    Print full stack trace when exiting with error");
+        out.println();
+        out.println("  Command specific options:");
+        out.println("    -s, --set NAME=VALUE  Set a specific attribute NAME to a specified value VALUE");
+        out.println("    -f, --file FILENAME   Read object from file or standard input if FILENAME is set to '-'");
+        out.println("    -c, --compressed      Don't pretty print the output");
+        out.println();
+        out.println("Examples:");
+        out.println();
+        out.println("Create a new JSON document with two top level attributes:");
+        out.println("  " + PROMPT + " " + CMD + " new-object -s realm=demorealm -s enabled=true");
+        out.println();
+        out.println("Read a JSON document and apply changes on top of it:");
+        if (OS_ARCH.isWindows()) {
+            out.println("  " + PROMPT + " echo { \"clientId\": \"my_client\" } | " + CMD + " new-object -s enabled=true -f -");
+        } else {
+            out.println("  " + PROMPT + " " + CMD + " new-object -s enabled=true -f - << EOF");
+            out.println("  {");
+            out.println("    \"clientId\": \"my_client\"");
+            out.println("  }");
+            out.println("  EOF");
+        }
+        out.println();
+        out.println();
+        out.println("Use '" + CMD + " help' for general information and a list of commands");
+        return sb.toString();
+    }
+
+}
                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
new file mode 100644
index 0000000..84698c3
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/RemoveRolesCmd.java
@@ -0,0 +1,334 @@
+/*
+ * 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.client.admin.cli.commands;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.admin.cli.config.ConfigData;
+import org.keycloak.client.admin.cli.operations.ClientOperations;
+import org.keycloak.client.admin.cli.operations.GroupOperations;
+import org.keycloak.client.admin.cli.operations.RoleOperations;
+import org.keycloak.client.admin.cli.operations.LocalSearch;
+import org.keycloak.client.admin.cli.operations.UserOperations;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
+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.PROMPT;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "remove-roles", description = "[ARGUMENTS]")
+public class RemoveRolesCmd extends AbstractAuthOptionsCmd {
+
+    @Option(name = "uusername", description = "Target user's 'username'")
+    String uusername;
+
+    @Option(name = "uid", description = "Target user's 'id'")
+    String uid;
+
+    @Option(name = "gname", description = "Target group's 'name'")
+    String gname;
+
+    @Option(name = "gpath", description = "Target group's 'path'")
+    String gpath;
+
+    @Option(name = "gid", description = "Target group's 'id'")
+    String gid;
+
+    @Option(name = "cclientid", description = "Target client's 'clientId'")
+    String cclientid;
+
+    @Option(name = "cid", description = "Target client's 'id'")
+    String cid;
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+        List<String> roleNames = new LinkedList<>();
+        List<String> roleIds = new LinkedList<>();
+
+        try {
+            if (printHelp()) {
+                return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
+            }
+
+            processGlobalOptions();
+
+            Iterator<String> it = args.iterator();
+
+            while (it.hasNext()) {
+                String option = it.next();
+                switch (option) {
+                    case "--rolename": {
+                        optionRequiresValueCheck(it, option);
+                        roleNames.add(it.next());
+                        break;
+                    }
+                    case "--roleid": {
+                        optionRequiresValueCheck(it, option);
+                        roleIds.add(it.next());
+                        break;
+                    }
+                    default: {
+                        throw new IllegalArgumentException("Invalid option: " + option);
+                    }
+                }
+            }
+
+            if (uid != null && uusername != null) {
+                throw new IllegalArgumentException("Incompatible options: --uid and --uusername are mutually exclusive");
+            }
+
+            if ((gid != null && gname != null) || (gid != null && gpath != null) || (gname != null && gpath != null)) {
+                throw new IllegalArgumentException("Incompatible options: --gid, --gname and --gpath are mutually exclusive");
+            }
+
+            if (roleNames.isEmpty() && roleIds.isEmpty()) {
+                throw new IllegalArgumentException("No role specified. Use --rolename or --roleid to specify roles");
+            }
+
+            if (cid != null && cclientid != null) {
+                throw new IllegalArgumentException("Incompatible options: --cid and --cclientid 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");
+            }
+
+
+            ConfigData config = loadConfig();
+            config = copyWithServerInfo(config);
+
+            setupTruststore(config, commandInvocation);
+
+            String auth = null;
+
+            config = ensureAuthInfo(config, commandInvocation);
+            config = copyWithServerInfo(config);
+            if (credentialsAvailable(config)) {
+                auth = ensureToken(config);
+            }
+
+            auth = auth != null ? "Bearer " + auth : null;
+
+            final String server = config.getServerUrl();
+            final String realm = getTargetRealm(config);
+            final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server);
+
+
+            if (isUserSpecified()) {
+                if (uid == null) {
+                    uid = UserOperations.getIdFromUsername(adminRoot, realm, auth, uusername);
+                }
+                if (isClientSpecified()) {
+                    // remove client roles from a user
+                    if (cid == null) {
+                        cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
+                    }
+
+                    List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
+                    Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
+
+                    // now remove the roles
+                    UserOperations.removeClientRoles(adminRoot, realm, auth, uid, cid, new ArrayList<>(rolesToAdd));
+
+                } else {
+
+                    Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
+                            new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
+
+                    // now remove the roles
+                    UserOperations.removeRealmRoles(adminRoot, realm, auth, uid, new ArrayList<>(rolesToAdd));
+                }
+
+            } else if (isGroupSpecified()) {
+                if (gname != null) {
+                    gid = GroupOperations.getIdFromName(adminRoot, realm, auth, gname);
+                } else if (gpath != null) {
+                    gid = GroupOperations.getIdFromPath(adminRoot, realm, auth, gpath);
+                }
+                if (isClientSpecified()) {
+                    // remove client roles from a group
+                    if (cid == null) {
+                        cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
+                    }
+
+                    List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
+                    Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
+
+                    // now remove the roles
+                    GroupOperations.removeClientRoles(adminRoot, realm, auth, gid, cid, new ArrayList<>(rolesToAdd));
+
+                } else {
+
+                    Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
+                            new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
+
+                    // now remove the roles
+                    GroupOperations.removeRealmRoles(adminRoot, realm, auth, gid, new ArrayList<>(rolesToAdd));
+                }
+
+            } else {
+
+                throw new IllegalArgumentException("No user nor group specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group");
+            }
+
+            return CommandResult.SUCCESS;
+
+        } catch (IllegalArgumentException e) {
+            throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
+        } finally {
+            commandInvocation.stop();
+        }
+    }
+
+    private Set<ObjectNode> getRoleRepresentations(List<String> roleNames, List<String> roleIds, LocalSearch roleSearch) {
+        Set<ObjectNode> rolesToAdd = new HashSet<>();
+
+        // now we process roles
+        for (String name : roleNames) {
+            ObjectNode r = roleSearch.exactMatchOne(name, "name");
+            if (r == null) {
+                throw new RuntimeException("Role not found for name: " + name);
+            }
+            rolesToAdd.add(r);
+        }
+        for (String id : roleIds) {
+            ObjectNode r = roleSearch.exactMatchOne(id, "id");
+            if (r == null) {
+                throw new RuntimeException("Role not found for id: " + id);
+            }
+            rolesToAdd.add(r);
+        }
+        return rolesToAdd;
+    }
+
+    private void optionRequiresValueCheck(Iterator<String> it, String option) {
+        if (!it.hasNext()) {
+            throw new IllegalArgumentException("Option " + option + " requires a value");
+        }
+    }
+
+    private boolean isClientSpecified() {
+        return cid != null || cclientid != null;
+    }
+
+    private boolean isGroupSpecified() {
+        return gid != null || gname != null || gpath != null;
+    }
+
+    private boolean isUserSpecified() {
+        return uid != null || uusername != null;
+    }
+
+
+    @Override
+    protected boolean nothingToDo() {
+        return noOptions() && uusername == null && uid == null && cclientid == null && (args == null || args.size() == 0);
+    }
+
+    protected String suggestHelp() {
+        return EOL + "Try '" + CMD + " help remove-roles' for more information";
+    }
+
+    protected String help() {
+        return usage();
+    }
+
+    public static String usage() {
+        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();
+        out.println("Command to remove realm or client roles from a user or group.");
+        out.println();
+        out.println("Use `" + CMD + " config credentials` to establish an authenticated session, or use CREDENTIALS OPTIONS");
+        out.println("to perform one time authentication.");
+        out.println();
+        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();
+        out.println("Arguments:");
+        out.println();
+        out.println("  Global options:");
+        out.println("    -x                    Print full stack trace when exiting with error");
+        out.println("    --config              Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+        out.println("    --truststore PATH     Path to a truststore containing trusted certificates");
+        out.println("    --trustpass PASSWORD  Truststore password (prompted for if not specified and --truststore is used)");
+        out.println("    CREDENTIALS OPTIONS   Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
+        out.println("                          an authenticated sessions. This allows on-the-fly transient authentication that does");
+        out.println("                          not touch a config file.");
+        out.println();
+        out.println("  Command specific options:");
+        out.println("    --uusername           User's 'username'. If more than one user exists with the same username");
+        out.println("                          you'll have to use --uid to specify the target user");
+        out.println("    --uid                 User's 'id' attribute");
+        out.println("    --gname               Group's 'name'. If more than one group exists with the same name you'll have");
+        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("    --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("    -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();
+        out.println("Examples:");
+        out.println();
+        out.println("Remove 'offline_access' realm role from a user:");
+        out.println("  " + PROMPT + " " + CMD + " remove-roles -r demorealm --uusername testuser --rolename offline_access");
+        out.println();
+        out.println("Remove 'realm-management' client roles 'view-users', 'view-clients' and 'view-realm' from a user:");
+        out.println("  " + PROMPT + " " + CMD + " remove-roles -r demorealm --uusername testuser --cclientid realm-management --rolename view-users --rolename view-clients --rolename view-realm");
+        out.println();
+        out.println("Remove 'uma_authorization' realm role to a group:");
+        out.println("  " + PROMPT + " " + CMD + " remove-roles -r demorealm --gname PowerUsers --rolename uma_authorization");
+        out.println();
+        out.println("Remove 'realm-management' client roles 'realm-admin' from a group:");
+        out.println("  " + PROMPT + " " + CMD + " remove-roles -r demorealm --gname PowerUsers --cclientid realm-management --rolename realm-admin");
+        out.println();
+        out.println();
+        out.println("Use '" + CMD + " help' for general information and a list of commands");
+        return sb.toString();
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/SetPasswordCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/SetPasswordCmd.java
new file mode 100644
index 0000000..f3ce6ea
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/SetPasswordCmd.java
@@ -0,0 +1,177 @@
+/*
+ * 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.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.CommandException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.admin.cli.config.ConfigData;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import static org.keycloak.client.admin.cli.operations.UserOperations.getIdFromUsername;
+import static org.keycloak.client.admin.cli.operations.UserOperations.resetUserPassword;
+import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
+import static org.keycloak.client.admin.cli.util.IoUtil.readSecret;
+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.PROMPT;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "set-password", description = "[ARGUMENTS]")
+public class SetPasswordCmd extends AbstractAuthOptionsCmd {
+
+    @Option(name = "username", description = "Username")
+    String username;
+
+    @Option(name = "userid", description = "User ID")
+    String userid;
+
+    @Option(shortName = 'p', name = "new-password", description = "New password")
+    String pass;
+
+    @Option(shortName = 't', name = "temporary", description = "is password temporary", hasValue = false)
+    boolean temporary;
+
+
+    @Override
+    public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+        try {
+            if (printHelp()) {
+                return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
+            }
+
+            processGlobalOptions();
+
+            return process(commandInvocation);
+        } catch (IllegalArgumentException e) {
+            throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
+        } finally {
+            commandInvocation.stop();
+        }
+    }
+
+
+    public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
+
+        if (args != null && args.size() > 0) {
+            throw new IllegalArgumentException("Invalid option: " + args.get(0));
+        }
+
+        if (userid == null && username == null) {
+            throw new IllegalArgumentException("No user specified. Use --username or --userid to specify user");
+        }
+
+        if (userid != null && username != null) {
+            throw new IllegalArgumentException("Options --userid and --username are mutually exclusive");
+        }
+
+        if (pass == null) {
+            pass = readSecret("Enter password: ", commandInvocation);
+        }
+
+        ConfigData config = loadConfig();
+        config = copyWithServerInfo(config);
+
+        setupTruststore(config, commandInvocation);
+
+        String auth = null;
+
+        config = ensureAuthInfo(config, commandInvocation);
+        config = copyWithServerInfo(config);
+        if (credentialsAvailable(config)) {
+            auth = ensureToken(config);
+        }
+
+        auth = auth != null ? "Bearer " + auth : null;
+
+        final String server = config.getServerUrl();
+        final String realm = getTargetRealm(config);
+        final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server);
+
+        // if username is specified resolve id
+        if (username != null) {
+            userid = getIdFromUsername(adminRoot, realm, auth, username);
+        }
+
+        resetUserPassword(adminRoot, realm, auth, userid, pass, temporary);
+
+        return CommandResult.SUCCESS;
+    }
+
+    @Override
+    protected boolean nothingToDo() {
+        return noOptions() && username == null && userid == null && pass == null;
+    }
+
+    protected String suggestHelp() {
+        return EOL + "Try '" + CMD + " help set-password' for more information";
+    }
+
+
+    protected String help() {
+        return usage();
+    }
+
+    public static String usage() {
+        StringWriter sb = new StringWriter();
+        PrintWriter out = new PrintWriter(sb);
+        out.println("Usage: " + CMD + " set-password (--username USERNAME | --userid ID) [--password PASSWORD] [ARGUMENTS]");
+        out.println();
+        out.println("Command to reset user's password.");
+        out.println();
+        out.println("Use `" + CMD + " config credentials` to establish an authenticated session, or use CREDENTIALS OPTIONS");
+        out.println("to perform one time authentication.");
+        out.println();
+        out.println("Arguments:");
+        out.println();
+        out.println("  Global options:");
+        out.println("    -x                    Print full stack trace when exiting with error");
+        out.println("    --config              Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+        out.println("    --truststore PATH     Path to a truststore containing trusted certificates");
+        out.println("    --trustpass PASSWORD  Truststore password (prompted for if not specified and --truststore is used)");
+        out.println("    CREDENTIALS OPTIONS   Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
+        out.println("                          an authenticated sessions. This allows on-the-fly transient authentication that does");
+        out.println("                          not touch a config file.");
+        out.println();
+        out.println("  Command specific options:");
+        out.println("    --username USERNAME       Identify target user by 'username'");
+        out.println("    --userid ID               Identify target user by 'id'");
+        out.println("    -p, --new-password        New password to set. If not specified you will be prompted for it.");
+        out.println("    -t, --temporary           Make the new password temporary - user has to change it on next logon");
+        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();
+        out.println("Examples:");
+        out.println();
+        out.println("Set new temporary password for the user:");
+        out.println("  " + PROMPT + " " + CMD + " set-password -r demorealm --username testuser --password NEWPASS -t");
+        out.println();
+        out.println();
+        out.println("Use '" + CMD + " help' for general information and a list of commands");
+        return sb.toString();
+    }
+
+}
                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
new file mode 100644
index 0000000..3ce91b3
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/UpdateCmd.java
@@ -0,0 +1,165 @@
+/*
+ * 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.client.admin.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+
+import java.io.PrintWriter;
+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.PROMPT;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@CommandDefinition(name = "update", description = "CLIENT_ID [ARGUMENTS]")
+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 = '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;
+
+    @Option(shortName = 'H', name = "print-headers", description = "Print response headers", hasValue = false)
+    boolean printHeaders;
+
+    @Option(shortName = 'm', name = "merge", description = "Merge new values with existing configuration on the server - for when the default is not to merge (i.e. if --file is used)", hasValue = false)
+    boolean mergeMode;
+
+    @Option(shortName = 'n', name = "no-merge", description = "Don't merge new values with existing configuration on the server - for when the default is to merge (i.e. is --set is used while --file is not used)", hasValue = false)
+    boolean noMerge;
+
+    @Option(shortName = 'o', name = "output", description = "After update output the new client configuration", hasValue = false)
+    boolean outputResult;
+
+    @Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false)
+    boolean compressed;
+
+    //@GroupOption(shortName = 's', name = "set", description = "Set specific attribute to a specified value", hasValue = true)
+    //private List<String> attributes = new ArrayList<>();
+
+
+    @Override
+    void initOptions() {
+        // set options on parent
+        super.file = file;
+        super.fields = fields;
+        super.printHeaders = printHeaders;
+        super.returnId = false;
+        super.outputResult = true;
+        super.compressed = compressed;
+        super.mergeMode = mergeMode;
+        super.noMerge = noMerge;
+        super.outputResult = outputResult;
+        super.httpVerb = "put";
+    }
+
+    @Override
+    protected boolean nothingToDo() {
+        return noOptions() && file == null && (args == null || args.size() == 0);
+    }
+
+    protected String suggestHelp() {
+        return EOL + "Try '" + CMD + " help update' for more information";
+    }
+
+    protected String help() {
+        return usage();
+    }
+
+    public static String usage() {
+        StringWriter sb = new StringWriter();
+        PrintWriter out = new PrintWriter(sb);
+        out.println("Usage: " + CMD + " update ENDPOINT_URI [ARGUMENTS]");
+        out.println();
+        out.println("Command to update existing resources on the server.");
+        out.println();
+        out.println("Use `" + CMD + " config credentials` to establish an authenticated sessions, or use CREDENTIALS OPTIONS");
+        out.println("to perform one time authentication.");
+        out.println();
+        out.println("Arguments:");
+        out.println();
+        out.println("  Global options:");
+        out.println("    -x                    Print full stack trace when exiting with error");
+        out.println("    --config              Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
+        out.println("    --truststore PATH     Path to a truststore containing trusted certificates");
+        out.println("    --trustpass PASSWORD  Truststore password (prompted for if not specified and --truststore is used)");
+        out.println("    CREDENTIALS OPTIONS   Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
+        out.println("                          an authenticated sessions. This allows on-the-fly transient authentication that does");
+        out.println("                          not touch a config file.");
+        out.println();
+        out.println("  Command specific options:");
+        out.println("    ENDPOINT_URI              URI used to compose a target resource url. Commonly used values start with:");
+        out.println("                              realms/, users/, roles/, groups/, clients/, keys/, components/ ...");
+        out.println("                              If it starts with 'http://' then it will be used as target resource url");
+        out.println("    -r, --target-realm REALM  Target realm to issue requests against if not the one authenticated against");
+        out.println("    -s, --set NAME=VALUE      Set a specific attribute NAME to a specified value VALUE");
+        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("    -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");
+        out.println("                              Merge is automatically enabled unless --file is specified");
+        out.println("    -n, --no-merge            Suppress merge mode");
+        out.println();
+        out.println("    -H, --print-headers       Print response headers");
+        out.println("    -o, --output              After update output the new resource to standard output");
+        out.println("    -F, --fields FILTER       A filter pattern to specify which fields of a JSON response to output");
+        out.println("    -c, --compressed          Don't pretty print the output");
+        out.println("    -a, --admin-root URL      URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin");
+        out.println();
+        out.println();
+        out.println("Nested attributes are supported by using '.' to separate components of a KEY. Optionaly, the KEY components ");
+        out.println("can be quoted with double quotes - e.g. my_client.attributes.\"external.user.id\". If VALUE starts with [ and ");
+        out.println("ends with ] the attribute will be set as a JSON array. If VALUE starts with { and ends with } the attribute ");
+        out.println("will be set as a JSON object. If KEY ends with an array index - e.g. clients[3]=VALUE - then the specified item");
+        out.println("of the array is updated. If KEY+=VALUE syntax is used, then KEY is assumed to be an array, and another item is");
+        out.println("added to it.");
+        out.println();
+        out.println("Attributes can also be deleted. If KEY ends with an array index, then the targeted item of an array is removed");
+        out.println("and the following items are shifted.");
+        out.println();
+        out.println("Merged mode fetches target resource item from the server, applies attribute changes to it, and sends it");
+        out.println("back to the server.");
+        out.println();
+        out.println();
+        out.println("Examples:");
+        out.println();
+        out.println("Update a target realm by fetching current configuration from the server, and applying specified changes");
+        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/*\"]'");
+        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");
+        out.println();
+        out.println("Reset user's password (replace ID with user's 'id'):");
+        out.println("  " + PROMPT + " " + CMD + " update users/ID/reset-password -r demorealm -s type=password -s value=NEWPASSWORD -s temporary=true -n");
+        out.println();
+        out.println();
+        out.println("Use '" + CMD + " help' for general information and a list of commands");
+        return sb.toString();
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/AttributeKey.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/AttributeKey.java
new file mode 100644
index 0000000..7cb0dc8
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/AttributeKey.java
@@ -0,0 +1,170 @@
+/*
+ * 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.client.admin.cli.common;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class AttributeKey {
+
+    private static final int START = 0;
+    private static final int QUOTED = 1;
+    private static final int UNQUOTED = 2;
+    private static final int END = 3;
+
+    private List<Component> components;
+    private boolean append;
+
+    public AttributeKey() {
+        components = Collections.emptyList();
+    }
+
+    public AttributeKey(String key) {
+        if (key.endsWith("+")) {
+            append = true;
+            key = key.substring(0, key.length() - 1);
+        }
+        components = parse(key);
+    }
+
+    static List<Component> parse(String key) {
+
+        if (key == null || "".equals(key)) {
+            return Collections.emptyList();
+        }
+
+        List<Component> cs = new LinkedList<>();
+        StringBuilder sb = new StringBuilder();
+        int state = START;
+
+        char[] buf = key.toCharArray();
+
+        for (int pos = 0; pos < buf.length; pos++) {
+            char c = buf[pos];
+
+            if (state == START) {
+                if ('\"' == c) {
+                    state = QUOTED;
+                } else if ('.' == c) {
+                    throw new RuntimeException("Invalid attribute key: " + key + " (at position " + (pos + 1) + ")");
+                } else {
+                    state = UNQUOTED;
+                    sb.append(c);
+                }
+            } else if (state == QUOTED) {
+                if ('\"' == c) {
+                    state = END;
+                } else {
+                    sb.append(c);
+                }
+            } else if (state == UNQUOTED || state == END) {
+                if ('.' == c) {
+                    state = START;
+                    cs.add(new Component(sb.toString()));
+                    sb.setLength(0);
+                } else if (state == END || '\"' == c) {
+                    throw new RuntimeException("Invalid attribute key: " + key + " (at position " + (pos + 1) + ")");
+                } else {
+                    sb.append(c);
+                }
+            }
+        }
+
+        boolean ok = false;
+        if (sb.length() > 0) {
+            if (state == UNQUOTED || state == END) {
+                cs.add(new Component(sb.toString()));
+                ok = true;
+            }
+        } else if (state == END) {
+            ok = true;
+        }
+
+        if (!ok) {
+            throw new RuntimeException("Invalid attribute key: " + key + " (at position " + (buf.length) + ")");
+        }
+
+        return Collections.unmodifiableList(cs);
+    }
+
+    public List<Component> getComponents() {
+        return components;
+    }
+
+    public boolean isAppend() {
+        return append;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        for (Component c: components) {
+            if (sb.length() > 0) {
+                sb.append(".");
+            }
+            sb.append(c.toString());
+        }
+        return sb.toString();
+    }
+
+
+
+    public static class Component {
+
+        private int index = -1;
+        private String name;
+
+        Component(String name) {
+            if (name.endsWith("]")) {
+                int pos = name.lastIndexOf("[", name.length() - 1);
+                if (pos == -1) {
+                    throw new RuntimeException("Invalid attribute key: " + name + " (']' not allowed here)");
+                }
+                String idx = name.substring(pos + 1, name.length() - 1);
+                try {
+                    index = Integer.parseInt(idx);
+                } catch (Exception e) {
+                    throw new RuntimeException("Invalid attribute key: " + name + " (Invalid array index: '[" + idx + "]')");
+                }
+                this.name = name.substring(0, pos);
+            } else {
+                this.name = name;
+            }
+        }
+
+        public boolean isArray() {
+            return index >= 0;
+        }
+
+        public int getIndex() {
+            return index;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        @Override
+        public String toString() {
+            return name + (index != -1 ? "[" + index + "]" : "");
+        }
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/AttributeOperation.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/AttributeOperation.java
new file mode 100644
index 0000000..4db483e
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/AttributeOperation.java
@@ -0,0 +1,58 @@
+/*
+ * 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.client.admin.cli.common;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class AttributeOperation {
+
+    private Type type;
+    private AttributeKey key;
+    private String value;
+
+    public AttributeOperation(Type type, String key) {
+        this(type, key, null);
+    }
+
+    public AttributeOperation(Type type, String key, String value) {
+        if (type == Type.DELETE && value != null) {
+            throw new IllegalArgumentException("When type is DELETE, value has to be null");
+        }
+        this.type = type;
+        this.key = new AttributeKey(key);
+        this.value = value;
+    }
+
+    public Type getType() {
+        return type;
+    }
+
+    public AttributeKey getKey() {
+        return key;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+
+    public enum Type {
+        SET,
+        DELETE
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/CmdStdinContext.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/CmdStdinContext.java
new file mode 100644
index 0000000..1c67526
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/common/CmdStdinContext.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.common;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class CmdStdinContext<T> {
+
+    private T result;
+    private String content;
+
+    public CmdStdinContext() {}
+
+    public T getResult() {
+        return result;
+    }
+
+    public void setResult(T result) {
+        this.result = result;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigData.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigData.java
new file mode 100644
index 0000000..e327335
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigData.java
@@ -0,0 +1,176 @@
+/*
+ * 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.client.admin.cli.config;
+
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class ConfigData {
+
+    private String serverUrl;
+
+    private String realm;
+
+    private String truststore;
+
+    private String trustpass;
+
+    private Map<String, Map<String, RealmConfigData>> endpoints = new HashMap<>();
+
+
+    public String getServerUrl() {
+        return serverUrl;
+    }
+
+    public void setServerUrl(String serverUrl) {
+        this.serverUrl = serverUrl;
+    }
+
+    public String getRealm() {
+        return realm;
+    }
+
+    public void setRealm(String realm) {
+        this.realm = realm;
+    }
+
+    public String getTruststore() {
+        return truststore;
+    }
+
+    public void setTruststore(String truststore) {
+        this.truststore = truststore;
+    }
+
+    public String getTrustpass() {
+        return trustpass;
+    }
+
+    public void setTrustpass(String trustpass) {
+        this.trustpass = trustpass;
+    }
+
+    public Map<String, Map<String, RealmConfigData>> getEndpoints() {
+        return endpoints;
+    }
+
+    public void setEndpoints(Map<String, Map<String, RealmConfigData>> endpoints) {
+        for (Map.Entry<String, Map<String, RealmConfigData>> entry: endpoints.entrySet()) {
+            String endpoint = entry.getKey();
+            for (Map.Entry<String, RealmConfigData> sub: entry.getValue().entrySet()) {
+                RealmConfigData rdata = sub.getValue();
+                rdata.serverUrl(endpoint);
+                rdata.realm(sub.getKey());
+            }
+        }
+        this.endpoints = endpoints;
+    }
+
+    public RealmConfigData sessionRealmConfigData() {
+        if (serverUrl == null)
+            throw new RuntimeException("Illegal state - no current endpoint in config data");
+        if (realm == null)
+            throw new RuntimeException("Illegal state - no current realm in config data");
+        return ensureRealmConfigData(serverUrl, realm);
+    }
+
+    public RealmConfigData getRealmConfigData(String endpoint, String realm) {
+        Map<String, RealmConfigData> realmData = endpoints.get(endpoint);
+        if (realmData == null) {
+            return null;
+        }
+        return realmData.get(realm);
+    }
+
+    public RealmConfigData ensureRealmConfigData(String endpoint, String realm) {
+        RealmConfigData result = getRealmConfigData(endpoint, realm);
+        if (result == null) {
+            result = new RealmConfigData();
+            result.serverUrl(endpoint);
+            result.realm(realm);
+            setRealmConfigData(result);
+        }
+        return result;
+    }
+
+
+    public void setRealmConfigData(RealmConfigData data) {
+        Map<String, RealmConfigData> realm = endpoints.get(data.serverUrl());
+        if (realm == null) {
+            realm = new HashMap<>();
+            endpoints.put(data.serverUrl(), realm);
+        }
+        realm.put(data.realm(), data);
+    }
+
+    public void merge(ConfigData source) {
+        serverUrl = source.serverUrl;
+        realm = source.realm;
+        truststore = source.truststore;
+        trustpass = source.trustpass;
+
+        RealmConfigData current = getRealmConfigData(serverUrl, realm);
+        RealmConfigData sourceRealm = source.getRealmConfigData(serverUrl, realm);
+
+        if (current == null) {
+            setRealmConfigData(sourceRealm);
+        } else {
+            current.merge(sourceRealm);
+        }
+    }
+
+    public ConfigData deepcopy() {
+        ConfigData data = new ConfigData();
+        data.serverUrl = serverUrl;
+        data.realm = realm;
+        data.truststore = truststore;
+        data.trustpass = trustpass;
+        data.endpoints = new HashMap<>();
+
+        for (Map.Entry<String, Map<String, RealmConfigData>> item: endpoints.entrySet()) {
+
+            Map<String, RealmConfigData> nuitems = new HashMap<>();
+            Map<String, RealmConfigData> curitems = item.getValue();
+
+            if (curitems != null) {
+                for (Map.Entry<String, RealmConfigData> ditem : curitems.entrySet()) {
+                    RealmConfigData nudata = ditem.getValue();
+                    if (nudata != null) {
+                        nuitems.put(ditem.getKey(), nudata.deepcopy());
+                    }
+                }
+                data.endpoints.put(item.getKey(), nuitems);
+            }
+        }
+        return data;
+    }
+
+    @Override
+    public String toString() {
+        try {
+            return JsonSerialization.writeValueAsPrettyString(this);
+        } catch (IOException e) {
+            return super.toString() + " - Error: " + e.toString();
+        }
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigHandler.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigHandler.java
new file mode 100644
index 0000000..fd318c5
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.config;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public interface ConfigHandler {
+
+    void saveMergeConfig(ConfigUpdateOperation op);
+
+    ConfigData loadConfig();
+
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigUpdateOperation.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigUpdateOperation.java
new file mode 100644
index 0000000..15276a0
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/ConfigUpdateOperation.java
@@ -0,0 +1,26 @@
+/*
+ * 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.client.admin.cli.config;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public interface  ConfigUpdateOperation {
+
+    void update(ConfigData data);
+
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/FileConfigHandler.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/FileConfigHandler.java
new file mode 100644
index 0000000..cb76dfe
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/FileConfigHandler.java
@@ -0,0 +1,135 @@
+/*
+ * 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.client.admin.cli.config;
+
+import org.keycloak.client.admin.cli.util.IoUtil;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.nio.channels.OverlappingFileLockException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.keycloak.client.admin.cli.util.IoUtil.printErr;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class FileConfigHandler implements ConfigHandler {
+
+    private static final long MAX_SIZE = 10 * 1024 * 1024;
+    private static String configFile;
+
+    public static void setConfigFile(String filename) {
+        configFile = filename;
+    }
+
+    public static String getConfigFile() {
+        return configFile;
+    }
+
+    public ConfigData loadConfig() {
+        // for now just dumb impl ignoring file locks for read
+        File file = new File(configFile);
+        if (!file.isFile() || file.length() == 0) {
+            return new ConfigData();
+        }
+
+        try {
+            try (FileInputStream is = new FileInputStream(configFile)) {
+                return JsonSerialization.readValue(is, ConfigData.class);
+            }
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to load " + configFile, e);
+        }
+    }
+
+    public static void ensureFile() {
+        Path path = null;
+        try {
+            path = Paths.get(new File(configFile).getAbsolutePath());
+            IoUtil.ensureFile(path);
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to create config file: " + path, e);
+        }
+    }
+
+    public void saveMergeConfig(ConfigUpdateOperation op) {
+        try {
+            ensureFile();
+
+            try (RandomAccessFile file = new RandomAccessFile(new File(configFile), "rw")) {
+                FileChannel fileChannel = file.getChannel();
+
+                FileLock fileLock = null;
+
+                // lock file for write
+                int tryCount = 0;
+                do try {
+                    fileLock = fileChannel.tryLock();
+                    break;
+                } catch (OverlappingFileLockException e) {
+                    // sleep a little, and try again
+                    try {
+                        Thread.sleep(100);
+                        continue;
+                    } catch (InterruptedException e1) {
+                        throw new RuntimeException("Interrupted");
+                    }
+                } while (tryCount++ < 10);
+
+                if (fileLock != null) {
+                    try {
+                        // load config from file
+                        ConfigData config = new ConfigData();
+                        long size = file.length();
+                        if (size > MAX_SIZE) {
+                            printErr("Config file " + configFile + " is too big. It will be overwritten.");
+                            file.setLength(0);
+                        } else if (size > 0){
+                            byte[] buf = new byte[(int) size];
+                            file.readFully(buf);
+                            config = JsonSerialization.readValue(new ByteArrayInputStream(buf), ConfigData.class);
+                        }
+
+                        // update loaded config
+                        op.update(config);
+
+                        // save config to file
+                        byte [] content = JsonSerialization.writeValueAsPrettyString(config).getBytes("utf-8");
+                        file.seek(0);
+                        file.write(content);
+                        file.setLength(content.length);
+
+                    } finally {
+                        fileLock.release();
+                    }
+                } else {
+                    throw new RuntimeException("Failed to get lock on " + configFile);
+                }
+            }
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to save " + configFile, e);
+        }
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/InMemoryConfigHandler.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/InMemoryConfigHandler.java
new file mode 100644
index 0000000..7ac4377
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/InMemoryConfigHandler.java
@@ -0,0 +1,39 @@
+/*
+ * 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.client.admin.cli.config;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class InMemoryConfigHandler implements ConfigHandler {
+
+    private ConfigData cached;
+
+    @Override
+    public void saveMergeConfig(ConfigUpdateOperation config) {
+        config.update(cached);
+    }
+
+    @Override
+    public ConfigData loadConfig() {
+        return cached;
+    }
+
+    public void setConfigData(ConfigData data) {
+        this.cached = data;
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/RealmConfigData.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/RealmConfigData.java
new file mode 100644
index 0000000..2a8b163
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/config/RealmConfigData.java
@@ -0,0 +1,172 @@
+/*
+ * 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.client.admin.cli.config;
+
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class RealmConfigData {
+
+    private String serverUrl;
+
+    private String realm;
+
+    private String clientId;
+
+    private String token;
+
+    private String refreshToken;
+
+    private String signingToken;
+
+    private String secret;
+
+    private Long expiresAt;
+
+    private Long refreshExpiresAt;
+
+    private Long sigExpiresAt;
+
+
+    public String serverUrl() {
+        return serverUrl;
+    }
+
+    public void serverUrl(String serverUrl) {
+        this.serverUrl = serverUrl;
+    }
+
+    public String realm() {
+        return realm;
+    }
+
+    public void realm(String realm) {
+        this.realm = realm;
+    }
+
+    public String getClientId() {
+        return clientId;
+    }
+
+    public void setClientId(String clientId) {
+        this.clientId = clientId;
+    }
+
+    public String getToken() {
+        return token;
+    }
+
+    public void setToken(String token) {
+        this.token = token;
+    }
+
+    public String getRefreshToken() {
+        return refreshToken;
+    }
+
+    public void setRefreshToken(String refreshToken) {
+        this.refreshToken = refreshToken;
+    }
+
+    public String getSigningToken() {
+        return signingToken;
+    }
+
+    public void setSigningToken(String signingToken) {
+        this.signingToken = signingToken;
+    }
+
+    public String getSecret() {
+        return secret;
+    }
+
+    public void setSecret(String secret) {
+        this.secret = secret;
+    }
+
+    public Long getExpiresAt() {
+        return expiresAt;
+    }
+
+    public void setExpiresAt(Long expiresAt) {
+        this.expiresAt = expiresAt;
+    }
+
+    public Long getRefreshExpiresAt() {
+        return refreshExpiresAt;
+    }
+
+    public void setRefreshExpiresAt(Long refreshExpiresAt) {
+        this.refreshExpiresAt = refreshExpiresAt;
+    }
+
+    public Long getSigExpiresAt() {
+        return sigExpiresAt;
+    }
+
+    public void setSigExpiresAt(Long sigExpiresAt) {
+        this.sigExpiresAt = sigExpiresAt;
+    }
+
+    public void merge(RealmConfigData source) {
+        serverUrl = source.serverUrl;
+        realm = source.realm;
+        clientId = source.clientId;
+        token = source.token;
+        refreshToken = source.refreshToken;
+        signingToken = source.signingToken;
+        secret = source.secret;
+        expiresAt = source.expiresAt;
+        refreshExpiresAt = source.refreshExpiresAt;
+        sigExpiresAt = source.sigExpiresAt;
+    }
+
+    public void mergeRefreshTokens(RealmConfigData source) {
+        token = source.token;
+        refreshToken = source.refreshToken;
+        expiresAt = source.expiresAt;
+        refreshExpiresAt = source.refreshExpiresAt;
+    }
+
+    @Override
+    public String toString() {
+        try {
+            return JsonSerialization.writeValueAsPrettyString(this);
+        } catch (IOException e) {
+            return super.toString() + " - Error: " + e.toString();
+        }
+    }
+
+    public RealmConfigData deepcopy() {
+        RealmConfigData data = new RealmConfigData();
+        data.serverUrl = serverUrl;
+        data.realm = realm;
+        data.clientId = clientId;
+        data.token = token;
+        data.refreshToken = refreshToken;
+        data.signingToken = signingToken;
+        data.secret = secret;
+        data.expiresAt = expiresAt;
+        data.refreshExpiresAt = refreshExpiresAt;
+        data.sigExpiresAt = sigExpiresAt;
+        return data;
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/httpcomponents/HttpDelete.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/httpcomponents/HttpDelete.java
new file mode 100644
index 0000000..7d553ea
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/httpcomponents/HttpDelete.java
@@ -0,0 +1,39 @@
+/*
+ * 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.client.admin.cli.httpcomponents;
+
+import org.apache.http.annotation.NotThreadSafe;
+import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
+
+import java.net.URI;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+@NotThreadSafe
+public class HttpDelete extends HttpEntityEnclosingRequestBase {
+
+    public HttpDelete(final String uri) {
+        super();
+        setURI(URI.create(uri));
+    }
+
+    @Override
+    public String getMethod() {
+        return "DELETE";
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/KcAdmMain.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/KcAdmMain.java
new file mode 100644
index 0000000..9480077
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/KcAdmMain.java
@@ -0,0 +1,94 @@
+/*
+ * 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.client.admin.cli;
+
+import org.jboss.aesh.console.AeshConsoleBuilder;
+import org.jboss.aesh.console.AeshConsoleImpl;
+import org.jboss.aesh.console.Prompt;
+import org.jboss.aesh.console.command.registry.AeshCommandRegistryBuilder;
+import org.jboss.aesh.console.command.registry.CommandRegistry;
+import org.jboss.aesh.console.settings.Settings;
+import org.jboss.aesh.console.settings.SettingsBuilder;
+import org.keycloak.client.admin.cli.aesh.AeshEnhancer;
+import org.keycloak.client.admin.cli.aesh.Globals;
+import org.keycloak.client.admin.cli.aesh.ValveInputStream;
+import org.keycloak.client.admin.cli.commands.KcAdmCmd;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KcAdmMain {
+
+    public static void main(String [] args) {
+
+        Globals.stdin = new ValveInputStream();
+
+        Settings settings = new SettingsBuilder()
+                .logging(false)
+                .readInputrc(false)
+                .disableCompletion(true)
+                .disableHistory(true)
+                .enableAlias(false)
+                .enableExport(false)
+                .inputStream(Globals.stdin)
+                .create();
+
+        CommandRegistry registry = new AeshCommandRegistryBuilder()
+                .command(KcAdmCmd.class)
+                .create();
+
+        AeshConsoleImpl console = (AeshConsoleImpl) new AeshConsoleBuilder()
+                .settings(settings)
+                .commandRegistry(registry)
+                .prompt(new Prompt(""))
+//                .commandInvocationProvider(new CommandInvocationServices() {
+//
+//                })
+                .create();
+
+        AeshEnhancer.enhance(console);
+
+        // work around parser issues with quotes and brackets
+        ArrayList<String> arguments = new ArrayList<>();
+        arguments.add("kcadm");
+        arguments.addAll(Arrays.asList(args));
+        Globals.args = arguments;
+
+        StringBuilder b = new StringBuilder();
+        for (String s : args) {
+            // quote if necessary
+            boolean needQuote = false;
+            needQuote = s.indexOf(' ') != -1 || s.indexOf('\"') != -1 || s.indexOf('\'') != -1;
+            b.append(' ');
+            if (needQuote) {
+                b.append('\'');
+            }
+            b.append(s);
+            if (needQuote) {
+                b.append('\'');
+            }
+        }
+        console.setEcho(false);
+
+        console.execute("kcadm" + b.toString());
+
+        console.start();
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/ClientOperations.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/ClientOperations.java
new file mode 100644
index 0000000..7de4e29
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/ClientOperations.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.operations;
+
+import static org.keycloak.client.admin.cli.util.HttpUtil.getIdForType;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class ClientOperations {
+
+    public static String getIdFromClientId(String rootUrl, String realm, String auth, String clientId) {
+        return getIdForType(rootUrl, realm, auth, "clients", "clientId", clientId);
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/GroupOperations.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/GroupOperations.java
new file mode 100644
index 0000000..a9cec66
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/GroupOperations.java
@@ -0,0 +1,58 @@
+/*
+ * 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.client.admin.cli.operations;
+
+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.doPostJSON;
+import static org.keycloak.client.admin.cli.util.HttpUtil.getIdForType;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class GroupOperations {
+
+    public static String getIdFromName(String rootUrl, String realm, String auth, String groupname) {
+        return getIdForType(rootUrl, realm, auth, "groups", "name", groupname);
+    }
+
+    public static String getIdFromPath(String rootUrl, String realm, String auth, String path) {
+        return getIdForType(rootUrl, realm, auth, "groups", "path", path);
+    }
+
+    public static void addRealmRoles(String rootUrl, String realm, String auth, String groupid, List<?> roles) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/realm");
+        doPostJSON(resourceUrl, auth, roles);
+    }
+
+    public static void addClientRoles(String rootUrl, String realm, String auth, String groupid, String idOfClient, List<?> roles) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/clients/" + idOfClient);
+        doPostJSON(resourceUrl, auth, roles);
+    }
+
+    public static void removeRealmRoles(String rootUrl, String realm, String auth, String groupid, List<?> roles) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/realm");
+        doDeleteJSON(resourceUrl, auth, roles);
+    }
+
+    public static void removeClientRoles(String rootUrl, String realm, String auth, String groupid, String idOfClient, List<?> roles) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/clients/" + idOfClient);
+        doDeleteJSON(resourceUrl, auth, roles);
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/LocalSearch.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/LocalSearch.java
new file mode 100644
index 0000000..c91bb95
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/LocalSearch.java
@@ -0,0 +1,60 @@
+/*
+ * 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.client.admin.cli.operations;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class LocalSearch {
+
+    private List<ObjectNode> items;
+
+    public LocalSearch(List<ObjectNode> items) {
+        this.items = items;
+    }
+
+    public ObjectNode exactMatchOne(String value, String ... attrs) {
+
+        List<ObjectNode> matched = new LinkedList<>();
+
+        for (ObjectNode item: items) {
+            for (String attr: attrs) {
+                JsonNode node = item.get(attr);
+                if (node != null && node.asText().equals(value)) {
+                    matched.add(item);
+                    break;
+                }
+            }
+        }
+
+        if (matched.size() == 0) {
+            return null;
+        }
+
+        if (matched.size() > 1) {
+            throw new RuntimeException("More than one match");
+        }
+
+        return matched.get(0);
+    }
+}
                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
new file mode 100644
index 0000000..d3274fd
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/RoleOperations.java
@@ -0,0 +1,129 @@
+/*
+ * 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.client.admin.cli.operations;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.keycloak.representations.idm.RoleRepresentation;
+
+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.doGetJSON;
+import static org.keycloak.client.admin.cli.util.HttpUtil.getAttrForType;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class RoleOperations {
+
+    public static class LIST_OF_ROLES extends ArrayList<RoleRepresentation>{};
+    public static class LIST_OF_NODES extends ArrayList<ObjectNode>{};
+
+    public static String getRoleNameFromId(String adminRoot, String realm, String auth, String rid) {
+        return getAttrForType(adminRoot, realm, auth, "roles", "id", rid, "name");
+    }
+
+    public static String getClientRoleNameFromId(String adminRoot, String realm, String auth, String cid, String rid) {
+        return getAttrForType(adminRoot, realm, auth, "clients/" + cid + "/roles", "id", rid, "name");
+    }
+
+    public static List<RoleRepresentation> getRealmRoles(String rootUrl, String realm, String auth) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "roles");
+        return doGetJSON(LIST_OF_ROLES.class, resourceUrl, auth);
+    }
+
+    public static ObjectNode getRealmRole(String rootUrl, String realm, String rolename, String auth) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "roles/" + rolename);
+        return doGetJSON(ObjectNode.class, resourceUrl, auth);
+    }
+
+    public static List<ObjectNode> getClientRoles(String rootUrl, String realm, String idOfClient, String auth) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "clients/" + idOfClient + "/roles");
+        return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+    }
+
+    public static ObjectNode getClientRole(String rootUrl, String realm, String idOfClient, String rolename, String auth) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "clients/" + idOfClient + "/roles/" + rolename);
+        return doGetJSON(ObjectNode.class, resourceUrl, auth);
+    }
+
+    public static List<ObjectNode> getRealmRolesAsNodes(String rootUrl, String realm, String auth) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "roles");
+        return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+    }
+
+    public static List<ObjectNode> getRealmRolesForUserAsNodes(String rootUrl, String realm, String userid, String auth) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/realm");
+        return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+    }
+
+    public static List<ObjectNode> getCompositeRealmRolesForUserAsNodes(String rootUrl, String realm, String userid, String auth) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/realm/composite");
+        return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+    }
+
+    public static List<ObjectNode> getAvailableRealmRolesForUserAsNodes(String rootUrl, String realm, String userid, String auth) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/realm/available");
+        return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+    }
+
+    public static List<ObjectNode> getClientRolesForUserAsNodes(String rootUrl, String realm, String userid, String idOfClient, String auth) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/clients/" + idOfClient);
+        return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+    }
+
+    public static List<ObjectNode> getCompositeClientRolesForUserAsNodes(String rootUrl, String realm, String userid, String idOfClient, String auth) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/clients/" + idOfClient + "/composite");
+        return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+    }
+
+    public static List<ObjectNode> getAvailableClientRolesForUserAsNodes(String rootUrl, String realm, String userid, String idOfClient, String auth) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/clients/" + idOfClient + "/available");
+        return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+    }
+
+    public static List<ObjectNode> getRealmRolesForGroupAsNodes(String rootUrl, String realm, String groupid, String auth) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/realm");
+        return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+    }
+
+    public static List<ObjectNode> getCompositeRealmRolesForGroupAsNodes(String rootUrl, String realm, String groupid, String auth) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/realm/composite");
+        return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+    }
+
+    public static List<ObjectNode> getAvailableRealmRolesForGroupAsNodes(String rootUrl, String realm, String groupid, String auth) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/realm/available");
+        return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+    }
+
+    public static List<ObjectNode> getClientRolesForGroupAsNodes(String rootUrl, String realm, String groupid, String idOfClient, String auth) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/clients/" + idOfClient);
+        return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+    }
+
+    public static List<ObjectNode> getCompositeClientRolesForGroupAsNodes(String rootUrl, String realm, String groupid, String idOfClient, String auth) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/clients/" + idOfClient + "/composite");
+        return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+    }
+
+    public static List<ObjectNode> getAvailableClientRolesForGroupAsNodes(String rootUrl, String realm, String groupid, String idOfClient, String auth) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/clients/" + idOfClient + "/available");
+        return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/UserOperations.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/UserOperations.java
new file mode 100644
index 0000000..593c97e
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/UserOperations.java
@@ -0,0 +1,96 @@
+/*
+ * 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.client.admin.cli.operations;
+
+import org.keycloak.client.admin.cli.util.Headers;
+import org.keycloak.client.admin.cli.util.HeadersBody;
+import org.keycloak.client.admin.cli.util.HeadersBodyStatus;
+import org.keycloak.client.admin.cli.util.HttpUtil;
+import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+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.doPostJSON;
+import static org.keycloak.client.admin.cli.util.HttpUtil.getIdForType;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class UserOperations {
+
+    public static void addRealmRoles(String rootUrl, String realm, String auth, String userid, List<?> roles) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/realm");
+        doPostJSON(resourceUrl, auth, roles);
+    }
+
+    public static void addClientRoles(String rootUrl, String realm, String auth, String userid, String idOfClient, List<?> roles) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/clients/" + idOfClient);
+        doPostJSON(resourceUrl, auth, roles);
+    }
+
+    public static void removeRealmRoles(String rootUrl, String realm, String auth, String userid, List<?> roles) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/realm");
+        doDeleteJSON(resourceUrl, auth, roles);
+    }
+
+    public static void removeClientRoles(String rootUrl, String realm, String auth, String userid, String idOfClient, List<?> roles) {
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/clients/" + idOfClient);
+        doDeleteJSON(resourceUrl, auth, roles);
+    }
+
+    public static void resetUserPassword(String rootUrl, String realm, String auth, String userid, String password, boolean temporary) {
+
+        String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/reset-password");
+
+        Headers headers = new Headers();
+        if (auth != null) {
+            headers.add("Authorization", auth);
+        }
+        headers.add("Content-Type", "application/json");
+
+        CredentialRepresentation credentials = new CredentialRepresentation();
+        credentials.setType("password");
+        credentials.setTemporary(temporary);
+        credentials.setValue(password);
+
+        HeadersBodyStatus response;
+
+        byte[] body;
+        try {
+            body = JsonSerialization.writeValueAsBytes(credentials);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to serialize JSON", e);
+        }
+
+        try {
+            response = HttpUtil.doRequest("put", resourceUrl, new HeadersBody(headers, new ByteArrayInputStream(body)));
+        } catch (IOException e) {
+            throw new RuntimeException("HTTP request failed: PUT " + resourceUrl + "\n" + new String(body), e);
+        }
+
+        response.checkSuccess();
+    }
+
+    public static String getIdFromUsername(String rootUrl, String realm, String auth, String username) {
+        return getIdForType(rootUrl, realm, auth, "users", "username", username);
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AccessibleBufferOutputStream.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AccessibleBufferOutputStream.java
new file mode 100644
index 0000000..984784e
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AccessibleBufferOutputStream.java
@@ -0,0 +1,66 @@
+/*
+ * 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.client.admin.cli.util;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class AccessibleBufferOutputStream extends FilterOutputStream{
+
+    private byte[] buf;
+
+    /**
+     * Creates an output stream filter built on top of the specified
+     * underlying output stream.
+     *
+     * @param out the underlying output stream to be assigned to
+     *            the field <tt>this.out</tt> for later use, or
+     *            <code>null</code> if this instance is to be
+     *            created without an underlying stream.
+     */
+    public AccessibleBufferOutputStream(OutputStream out) {
+        super(out);
+    }
+
+    @Override
+    public void write(int b) throws IOException {
+        super.write(b);
+        buf = new byte[] {(byte) b};
+    }
+
+    @Override
+    public void write(byte[] b, int off, int len) throws IOException {
+        super.write(b, off, len);
+        buf = new byte[len];
+        System.arraycopy(b, off, buf, 0, len);
+    }
+
+    public byte[] getBuffer() {
+        return buf;
+    }
+
+    public int getLastByte() {
+        if (buf != null && buf.length > 0) {
+            return 0xFF & buf[buf.length-1];
+        }
+        return -1;
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AttributeException.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AttributeException.java
new file mode 100644
index 0000000..721baf2
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AttributeException.java
@@ -0,0 +1,39 @@
+/*
+ * 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.client.admin.cli.util;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class AttributeException extends RuntimeException {
+
+    private final String attrName;
+
+    public AttributeException(String attrName, String message) {
+        super(message);
+        this.attrName = attrName;
+    }
+
+    public AttributeException(String attrName, String message, Throwable th) {
+        super(message, th);
+        this.attrName = attrName;
+    }
+
+    public String getAttributeName() {
+        return attrName;
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AuthUtil.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AuthUtil.java
new file mode 100644
index 0000000..ddfca0c
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/AuthUtil.java
@@ -0,0 +1,202 @@
+/*
+ * 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.client.admin.cli.util;
+
+import org.keycloak.client.admin.cli.config.ConfigData;
+import org.keycloak.client.admin.cli.config.RealmConfigData;
+import org.keycloak.common.util.KeystoreUtil;
+import org.keycloak.common.util.Time;
+import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.representations.JsonWebToken;
+import org.keycloak.util.BasicAuthHelper;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.KeyPair;
+import java.util.UUID;
+
+import static java.lang.System.currentTimeMillis;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.checkAuthInfo;
+import static org.keycloak.client.admin.cli.util.ConfigUtil.saveMergeConfig;
+import static org.keycloak.client.admin.cli.util.HttpUtil.APPLICATION_FORM_URL_ENCODED;
+import static org.keycloak.client.admin.cli.util.HttpUtil.APPLICATION_JSON;
+import static org.keycloak.client.admin.cli.util.HttpUtil.doPost;
+import static org.keycloak.client.admin.cli.util.HttpUtil.urlencode;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class AuthUtil {
+
+    public static String ensureToken(ConfigData config) {
+
+        checkAuthInfo(config);
+
+        RealmConfigData realmConfig = config.sessionRealmConfigData();
+
+        long now = currentTimeMillis();
+
+        // check expires of access_token against time
+        // if it's less than 5s to expiry, renew it
+        if (realmConfig.getExpiresAt() - now < 5000) {
+
+            // check refresh_token against expiry time
+            // if it's less than 5s to expiry, fail with credentials expired
+            if (realmConfig.getRefreshExpiresAt() - now < 5000) {
+                throw new RuntimeException("Session has expired. Login again with '" + OsUtil.CMD + " config credentials'");
+            }
+
+            if (realmConfig.getSigExpiresAt() != null && realmConfig.getSigExpiresAt() - now < 5000) {
+                throw new RuntimeException("Session has expired. Login again with '" + OsUtil.CMD + " config credentials'");
+            }
+
+            try {
+                String authorization = null;
+
+                StringBuilder body = new StringBuilder("grant_type=refresh_token")
+                        .append("&refresh_token=").append(realmConfig.getRefreshToken())
+                        .append("&client_id=").append(urlencode(realmConfig.getClientId()));
+
+                if (realmConfig.getSigningToken() != null) {
+                    body.append("&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
+                            .append("&client_assertion=").append(realmConfig.getSigningToken());
+                } else if (realmConfig.getSecret() != null) {
+                    authorization = BasicAuthHelper.createHeader(realmConfig.getClientId(), realmConfig.getSecret());
+                }
+
+                InputStream result = doPost(realmConfig.serverUrl() + "/realms/" + realmConfig.realm() + "/protocol/openid-connect/token",
+                        APPLICATION_FORM_URL_ENCODED, APPLICATION_JSON, body.toString(), authorization);
+
+                AccessTokenResponse token = JsonSerialization.readValue(result, AccessTokenResponse.class);
+
+                saveMergeConfig(cfg -> {
+                    RealmConfigData realmData = cfg.sessionRealmConfigData();
+                    realmData.setToken(token.getToken());
+                    realmData.setRefreshToken(token.getRefreshToken());
+                    realmData.setExpiresAt(currentTimeMillis() + token.getExpiresIn() * 1000);
+                    realmData.setRefreshExpiresAt(currentTimeMillis() + token.getRefreshExpiresIn() * 1000);
+                });
+                return token.getToken();
+
+            } catch (Exception e) {
+                throw new RuntimeException("Failed to refresh access token - " + e.getMessage(), e);
+            }
+        }
+
+        return realmConfig.getToken();
+    }
+
+    public static AccessTokenResponse getAuthTokens(String server, String realm, String user, String password, String clientId) {
+        StringBuilder body = new StringBuilder();
+        try {
+            body.append("grant_type=password")
+                    .append("&username=").append(urlencode(user))
+                    .append("&password=").append(urlencode(password))
+                    .append("&client_id=").append(urlencode(clientId));
+
+            InputStream result = doPost(server + "/realms/" + realm + "/protocol/openid-connect/token",
+                    APPLICATION_FORM_URL_ENCODED, APPLICATION_JSON, body.toString(), null);
+            return JsonSerialization.readValue(result, AccessTokenResponse.class);
+
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Unexpected error: ", e);
+        } catch (IOException e) {
+            throw new RuntimeException("Error receiving response: ", e);
+        }
+    }
+
+    public static AccessTokenResponse getAuthTokensByJWT(String server, String realm, String user, String password, String clientId, String signedRequestToken) {
+        StringBuilder body = new StringBuilder();
+        try {
+            body.append("client_id=").append(urlencode(clientId))
+                    .append("&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
+                    .append("&client_assertion=").append(signedRequestToken);
+
+            if (user != null) {
+                if (password == null) {
+                    throw new RuntimeException("No password specified");
+                }
+                body.append("&grant_type=password")
+                        .append("&username=").append(urlencode(user))
+                        .append("&password=").append(urlencode(password));
+            } else {
+                body.append("&grant_type=client_credentials");
+            }
+
+            InputStream result = doPost(server + "/realms/" + realm + "/protocol/openid-connect/token",
+                    APPLICATION_FORM_URL_ENCODED, APPLICATION_JSON, body.toString(), null);
+            return JsonSerialization.readValue(result, AccessTokenResponse.class);
+
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Unexpected error: ", e);
+        } catch (IOException e) {
+            throw new RuntimeException("Error receiving response: ", e);
+        }
+    }
+
+    public static AccessTokenResponse getAuthTokensBySecret(String server, String realm, String user, String password, String clientId, String secret) {
+
+        StringBuilder body = new StringBuilder();
+        try {
+            if (user != null) {
+                if (password == null) {
+                    throw new RuntimeException("No password specified");
+                }
+
+                body.append("client_id=").append(urlencode(clientId))
+                        .append("&grant_type=password")
+                        .append("&username=").append(urlencode(user))
+                        .append("&password=").append(urlencode(password));
+            } else {
+                body.append("grant_type=client_credentials");
+            }
+
+            InputStream result = doPost(server + "/realms/" + realm + "/protocol/openid-connect/token",
+                    APPLICATION_FORM_URL_ENCODED, APPLICATION_JSON, body.toString(), BasicAuthHelper.createHeader(clientId, secret));
+            return JsonSerialization.readValue(result, AccessTokenResponse.class);
+
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Unexpected error: ", e);
+        } catch (IOException e) {
+            throw new RuntimeException("Error receiving response: ", e);
+        }
+    }
+
+    public static String getSignedRequestToken(String keystore, String storePass, String keyPass, String alias, int sigLifetime, String clientId, String realmInfoUrl) {
+
+        KeyPair keypair = KeystoreUtil.loadKeyPairFromKeystore(keystore, storePass, keyPass, alias, KeystoreUtil.KeystoreFormat.JKS);
+
+        JsonWebToken reqToken = new JsonWebToken();
+        reqToken.id(UUID.randomUUID().toString());
+        reqToken.issuer(clientId);
+        reqToken.subject(clientId);
+        reqToken.audience(realmInfoUrl);
+
+        int now = Time.currentTime();
+        reqToken.issuedAt(now);
+        reqToken.expiration(now + sigLifetime);
+        reqToken.notBefore(now);
+
+        String signedRequestToken = new JWSBuilder()
+                .jsonContent(reqToken)
+                .rsa256(keypair.getPrivate());
+        return signedRequestToken;
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ConfigUtil.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ConfigUtil.java
new file mode 100644
index 0000000..3699048
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ConfigUtil.java
@@ -0,0 +1,116 @@
+/*
+ * 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.client.admin.cli.util;
+
+import org.keycloak.client.admin.cli.config.ConfigData;
+import org.keycloak.client.admin.cli.config.ConfigHandler;
+import org.keycloak.client.admin.cli.config.ConfigUpdateOperation;
+import org.keycloak.client.admin.cli.config.InMemoryConfigHandler;
+import org.keycloak.client.admin.cli.config.RealmConfigData;
+import org.keycloak.representations.AccessTokenResponse;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class ConfigUtil {
+
+    public static final String DEFAULT_CLIENT = "admin-cli";
+
+    public static final String DEFAULT_CONFIG_FILE_STRING = OsUtil.OS_ARCH.isWindows() ? "%HOMEDRIVE%%HOMEPATH%\\.keycloak\\kcadm.config" : "~/.keycloak/kcadm.config";
+
+    public static final String DEFAULT_CONFIG_FILE_PATH = System.getProperty("user.home") + "/.keycloak/kcadm.config";
+
+    private static ConfigHandler handler;
+
+    public static ConfigHandler getHandler() {
+        return handler;
+    }
+
+    public static void setHandler(ConfigHandler handler) {
+        ConfigUtil.handler = handler;
+    }
+
+    public static void saveTokens(AccessTokenResponse tokens, String endpoint, String realm, String clientId, String signKey, Long sigExpiresAt, String secret) {
+        handler.saveMergeConfig(config -> {
+            config.setServerUrl(endpoint);
+            config.setRealm(realm);
+
+            RealmConfigData realmConfig = config.ensureRealmConfigData(endpoint, realm);
+            realmConfig.setToken(tokens.getToken());
+            realmConfig.setRefreshToken(tokens.getRefreshToken());
+            realmConfig.setSigningToken(signKey);
+            realmConfig.setSecret(secret);
+            realmConfig.setExpiresAt(System.currentTimeMillis() + tokens.getExpiresIn() * 1000);
+            realmConfig.setRefreshExpiresAt(tokens.getRefreshExpiresIn() == 0 ?
+                    Long.MAX_VALUE : System.currentTimeMillis() + tokens.getRefreshExpiresIn() * 1000);
+            realmConfig.setSigExpiresAt(sigExpiresAt);
+            realmConfig.setClientId(clientId);
+        });
+    }
+
+    public static void checkServerInfo(ConfigData config) {
+        if (config.getServerUrl() == null || config.getRealm() == null) {
+            throw new RuntimeException("No server or realm specified. Use --server, --realm, or '" + OsUtil.CMD + " config credentials'.");
+        }
+    }
+
+    public static void checkAuthInfo(ConfigData config) {
+        checkServerInfo(config);
+    }
+
+    public static boolean credentialsAvailable(ConfigData config) {
+        return config.getServerUrl() != null && config.getRealm() != null
+                && config.sessionRealmConfigData() != null && config.sessionRealmConfigData().getRefreshToken() != null;
+    }
+
+    public static ConfigData loadConfig() {
+        if (handler == null) {
+            throw new RuntimeException("No ConfigHandler set");
+        }
+
+        return handler.loadConfig();
+    }
+
+    public static void saveMergeConfig(ConfigUpdateOperation op) {
+        if (handler == null) {
+            throw new RuntimeException("No ConfigHandler set");
+        }
+
+        handler.saveMergeConfig(op);
+    }
+
+    public static void setupInMemoryHandler(ConfigData config) {
+        InMemoryConfigHandler memhandler = null;
+        if (handler instanceof InMemoryConfigHandler) {
+            memhandler = (InMemoryConfigHandler) handler;
+        } else {
+            memhandler = new InMemoryConfigHandler();
+            handler = memhandler;
+        }
+        memhandler.setConfigData(config);
+    }
+
+    public static String getEffectiveClientId(ConfigData config) {
+        String clientId = DEFAULT_CLIENT;
+
+        RealmConfigData realmData = config.sessionRealmConfigData();
+        if (realmData != null && realmData.getClientId() != null) {
+            clientId = realmData.getClientId();
+        }
+        return clientId;
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/FilterUtil.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/FilterUtil.java
new file mode 100644
index 0000000..6a68f85
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/FilterUtil.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+import static org.keycloak.client.admin.cli.util.OutputUtil.MAPPER;
+import static org.keycloak.client.admin.cli.util.OutputUtil.convertToJsonNode;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class FilterUtil {
+
+    public static JsonNode copyFilteredObject(Object object, ReturnFields returnFields) throws IOException {
+
+        JsonNode node = convertToJsonNode(object);
+
+        JsonNode r = node;
+        if (node.isArray()) {
+            ArrayNode ar = MAPPER.createArrayNode();
+            for (JsonNode item: node) {
+                ar.add(copyFilteredObject(item, returnFields));
+            }
+            r = ar;
+
+        } else if (node.isObject()){
+            r = MAPPER.createObjectNode();
+            Iterator<String> fieldNames = node.fieldNames();
+            while (fieldNames.hasNext()) {
+                String name = fieldNames.next();
+                if (returnFields.included(name)) {
+                    JsonNode value = copyFilteredObject(node.get(name), returnFields.child(name));
+                    ((ObjectNode) r).set(name, value);
+                }
+            }
+        }
+        return r;
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/Header.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/Header.java
new file mode 100644
index 0000000..6ba4a60
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/Header.java
@@ -0,0 +1,39 @@
+/*
+ * 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.client.admin.cli.util;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class Header {
+
+    private String name;
+    private String value;
+
+    public Header(String key, String value) {
+        this.name = key;
+        this.value = value;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getValue() {
+        return value;
+    }
+}
\ No newline at end of file
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/Headers.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/Headers.java
new file mode 100644
index 0000000..338971f
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/Headers.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class Headers implements Iterable<Header> {
+
+    private LinkedHashMap<String, Header> headers = new LinkedHashMap<>();
+
+    public void add(String header, String value) {
+        headers.put(header.toLowerCase(), new Header(header, value));
+    }
+
+    public boolean addIfMissing(String header, String value) {
+        String key = header.toLowerCase();
+        if (!headers.containsKey(key)) {
+            headers.put(key, new Header(header, value));
+            return true;
+        }
+        return false;
+    }
+
+    public boolean contains(String header) {
+        String key = header.toLowerCase();
+        return headers.containsKey(key);
+    }
+
+    public Header get(String header) {
+        return headers.get(header.toLowerCase());
+    }
+
+    @Override
+    public Iterator<Header> iterator() {
+        return headers.values().iterator();
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HeadersBody.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HeadersBody.java
new file mode 100644
index 0000000..c217fd2
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HeadersBody.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+
+import static org.keycloak.client.admin.cli.util.IoUtil.copyStream;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class HeadersBody {
+
+    private Headers headers;
+    private InputStream body;
+
+
+    public HeadersBody(Headers headers) {
+        this.headers = headers;
+    }
+
+    public HeadersBody(Headers headers, InputStream body) {
+        this.headers = headers;
+        this.body = body;
+    }
+
+    public Headers getHeaders() {
+        return headers;
+    }
+
+    public InputStream getBody() {
+        return body;
+    }
+
+    public String readBodyString() {
+        byte [] buffer = readBodyBytes();
+        return new String(buffer, Charset.forName(getContentCharset()));
+    }
+
+    public byte[] readBodyBytes() {
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        copyStream(getBody(), os);
+        return os.toByteArray();
+    }
+
+    public String getContentCharset() {
+        Header contentType = headers.get("Content-Type");
+        if (contentType != null) {
+            int pos = contentType.getValue().lastIndexOf("charset=");
+            if (pos != -1) {
+                return contentType.getValue().substring(pos + 8);
+            }
+        }
+        return "iso-8859-1";
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HeadersBodyStatus.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HeadersBodyStatus.java
new file mode 100644
index 0000000..9ce8965
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HeadersBodyStatus.java
@@ -0,0 +1,68 @@
+/*
+ * 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.client.admin.cli.util;
+
+import org.keycloak.util.JsonSerialization;
+
+import java.io.InputStream;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class HeadersBodyStatus extends HeadersBody {
+
+    private final String status;
+
+    public HeadersBodyStatus(String status, Headers headers, InputStream body) {
+        super(headers, body);
+        this.status = status;
+    }
+
+    public String getStatus() {
+        return status;
+    }
+
+    private String getStatusCodeAndReason() {
+        return getStatus().substring(9);
+    }
+
+    public void checkSuccess() {
+        int code = getStatusCode();
+        if (code < 200 || code >= 300) {
+            String content = readBodyString();
+            Map<String, String> error = null;
+            try {
+                error = JsonSerialization.readValue(content, Map.class);
+            } catch (Exception ignored) {
+            }
+
+            String message = null;
+            if (error != null) {
+                String description = error.get("error_description");
+                String err = error.get("error");
+                String msg = error.get("errorMessage");
+                message = msg != null ? msg : err != null ? (description + " ["+ error.get("error") + "]") : null;
+            }
+            throw new HttpResponseException(getStatusCodeAndReason(), message, new RuntimeException(content));
+        }
+    }
+
+    public int getStatusCode() {
+        return Integer.valueOf(status.split(" ")[1]);
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HttpResponseException.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HttpResponseException.java
new file mode 100644
index 0000000..692b384
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HttpResponseException.java
@@ -0,0 +1,34 @@
+/*
+ * 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.client.admin.cli.util;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class HttpResponseException extends RuntimeException {
+
+    private String status;
+
+    HttpResponseException(String status, String message, Throwable cause) {
+        super(message != null ? message : "HTTP error - " + status, cause);
+        this.status = status;
+    }
+
+    public int getStatusCode() {
+        return Integer.valueOf(status.split(" ")[0]);
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HttpUtil.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HttpUtil.java
new file mode 100644
index 0000000..f1261ab
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/HttpUtil.java
@@ -0,0 +1,450 @@
+/*
+ * 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.client.admin.cli.util;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.http.HeaderIterator;
+import org.apache.http.HttpHeaders;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpHead;
+import org.apache.http.client.methods.HttpOptions;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.entity.InputStreamEntity;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.ssl.SSLContexts;
+import org.keycloak.client.admin.cli.httpcomponents.HttpDelete;
+import org.keycloak.client.admin.cli.operations.LocalSearch;
+import org.keycloak.client.admin.cli.operations.RoleOperations;
+import org.keycloak.util.JsonSerialization;
+
+import javax.net.ssl.SSLContext;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.keycloak.common.util.ObjectUtil.capitalize;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class HttpUtil {
+
+    public static final String APPLICATION_XML = "application/xml";
+    public static final String APPLICATION_JSON = "application/json";
+    public static final String APPLICATION_FORM_URL_ENCODED = "application/x-www-form-urlencoded";
+    public static final String UTF_8 = "utf-8";
+
+    private static HttpClient httpClient;
+    private static SSLConnectionSocketFactory sslsf;
+
+    public static InputStream doGet(String url, String acceptType, String authorization) {
+        try {
+            HttpGet request = new HttpGet(url);
+            request.setHeader(HttpHeaders.ACCEPT, acceptType);
+            return doRequest(authorization, request);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to send request - " + e.getMessage(), e);
+        }
+    }
+
+    public static InputStream doPost(String url, String contentType, String acceptType, String content, String authorization) {
+        try {
+            return doPostOrPut(contentType, acceptType, content, authorization, new HttpPost(url));
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to send request - " + e.getMessage(), e);
+        }
+    }
+
+    public static InputStream doPut(String url, String contentType, String acceptType, String content, String authorization) {
+        try {
+            return doPostOrPut(contentType, acceptType, content, authorization, new HttpPut(url));
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to send request - " + e.getMessage(), e);
+        }
+    }
+
+    public static void doDelete(String url, String authorization) {
+        try {
+            HttpDelete request = new HttpDelete(url);
+            doRequest(authorization, request);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to send request - " + e.getMessage(), e);
+        }
+    }
+
+
+    public static HeadersBodyStatus doGet(String url, HeadersBody request) throws IOException {
+        return doRequest("get", url, request);
+    }
+
+    public static HeadersBodyStatus doPost(String url, HeadersBody request) throws IOException {
+        return doRequest("post", url, request);
+    }
+
+    public static HeadersBodyStatus doPut(String url, HeadersBody request) throws IOException {
+        return doRequest("put", url, request);
+    }
+
+    public static HeadersBodyStatus doDelete(String url, HeadersBody request) throws IOException {
+        return doRequest("delete", url, request);
+    }
+
+    public static HeadersBodyStatus doRequest(String type, String url, HeadersBody request) throws IOException {
+        HttpRequestBase req;
+        switch (type) {
+            case "get":
+                req = new HttpGet(url);
+                break;
+            case "post":
+                req = new HttpPost(url);
+                break;
+            case "put":
+                req = new HttpPut(url);
+                break;
+            case "delete":
+                req = new HttpDelete(url);
+                break;
+            case "options":
+                req = new HttpOptions(url);
+                break;
+            case "head":
+                req = new HttpHead(url);
+                break;
+            default:
+                throw new RuntimeException("Method not supported: " + type);
+        }
+        addHeaders(req, request.getHeaders());
+
+        if (request.getBody() != null) {
+            if (req instanceof HttpEntityEnclosingRequestBase == false) {
+                throw new RuntimeException("Request type does not support body: " + type);
+            }
+            ((HttpEntityEnclosingRequestBase) req).setEntity(new InputStreamEntity(request.getBody()));
+        }
+
+        HttpResponse res = getHttpClient().execute(req);
+        InputStream responseStream = null;
+        if (res.getEntity() != null) {
+            responseStream = res.getEntity().getContent();
+        } else {
+            responseStream = new InputStream() {
+                @Override
+                public int read () throws IOException {
+                    return -1;
+                }
+            };
+        }
+
+        Headers headers = new Headers();
+        HeaderIterator it = res.headerIterator();
+        while (it.hasNext()) {
+            org.apache.http.Header header = it.nextHeader();
+            headers.add(header.getName(), header.getValue());
+        }
+
+        return new HeadersBodyStatus(res.getStatusLine().toString(), headers, responseStream);
+    }
+
+    private static void addHeaders(HttpRequestBase request, Headers headers) {
+        for (Header header: headers) {
+            request.setHeader(header.getName(), header.getValue());
+        }
+    }
+
+    private static InputStream doPostOrPut(String contentType, String acceptType, String content, String authorization, HttpEntityEnclosingRequestBase request) throws IOException {
+        request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
+        request.setHeader(HttpHeaders.ACCEPT, acceptType);
+        if (content != null) {
+            request.setEntity(new StringEntity(content));
+        }
+
+        return doRequest(authorization, request);
+    }
+
+    private static InputStream doRequest(String authorization, HttpRequestBase request) throws IOException {
+        addAuth(request, authorization);
+
+        HttpResponse response = getHttpClient().execute(request);
+        InputStream responseStream = null;
+        if (response.getEntity() != null) {
+            responseStream = response.getEntity().getContent();
+        }
+
+        int code = response.getStatusLine().getStatusCode();
+        if (code >= 200 && code < 300) {
+            return responseStream;
+        } else {
+            Map<String, String> error = null;
+            try {
+                org.apache.http.Header header = response.getEntity().getContentType();
+                if (header != null && APPLICATION_JSON.equals(header.getValue())) {
+                    error = JsonSerialization.readValue(responseStream, Map.class);
+                }
+            } catch (Exception e) {
+                throw new RuntimeException("Failed to read error response - " + e.getMessage(), e);
+            } finally {
+                responseStream.close();
+            }
+
+            String message = null;
+            if (error != null) {
+                message = error.get("error_description") + " [" + error.get("error") + "]";
+            }
+            throw new RuntimeException(message != null ? message : response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase());
+        }
+    }
+
+    private static void addAuth(HttpRequestBase request, String authorization) {
+        if (authorization != null) {
+            request.setHeader(HttpHeaders.AUTHORIZATION, authorization);
+        }
+    }
+
+    public static HttpClient getHttpClient() {
+        if (httpClient == null) {
+            if (sslsf != null) {
+                httpClient = HttpClientBuilder.create().useSystemProperties().setSSLSocketFactory(sslsf).build();
+            } else {
+                httpClient = HttpClientBuilder.create().useSystemProperties().build();
+            }
+        }
+        return httpClient;
+    }
+
+    public static String urlencode(String value) {
+        try {
+            return URLEncoder.encode(value, UTF_8);
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Failed to urlencode", e);
+        }
+    }
+
+    public static void setTruststore(File file, String password) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, KeyManagementException {
+        if (!file.isFile()) {
+            throw new RuntimeException("Truststore file not found: " + file.getAbsolutePath());
+        }
+        SSLContext theContext = SSLContexts.custom()
+                .useProtocol("TLS")
+                .loadTrustMaterial(file, password == null ? null : password.toCharArray())
+                .build();
+        sslsf = new SSLConnectionSocketFactory(theContext);
+    }
+
+    public static String extractIdFromLocation(String location) {
+        int last = location.lastIndexOf("/");
+        if (last != -1) {
+            return location.substring(last + 1);
+        }
+        return null;
+    }
+
+    public static String addQueryParamsToUri(String uri, String ... queryParams) {
+        if (queryParams == null) {
+            return uri;
+        }
+
+        if (queryParams.length % 2 != 0) {
+            throw new RuntimeException("Value missing for query parameter: " + queryParams[queryParams.length-1]);
+        }
+
+        Map<String, String> params = new LinkedHashMap<>();
+        for (int i = 0; i < queryParams.length; i += 2) {
+            params.put(queryParams[i], queryParams[i+1]);
+        }
+        return addQueryParamsToUri(uri, params);
+    }
+
+    public static String addQueryParamsToUri(String uri, Map<String, String> queryParams) {
+
+        if (queryParams.size() == 0) {
+            return uri;
+        }
+
+        StringBuilder query = new StringBuilder();
+        for (Map.Entry<String, String> params: queryParams.entrySet()) {
+            try {
+                if (query.length() > 0) {
+                    query.append("&");
+                }
+                query.append(params.getKey()).append("=").append(URLEncoder.encode(params.getValue(), "utf-8"));
+            } catch (Exception e) {
+                throw new RuntimeException("Failed to encode query params: " + params.getKey() + "=" + params.getValue());
+            }
+        }
+
+        return uri + (uri.indexOf("?") == -1 ? "?" : "&") + query;
+    }
+
+    public static String composeResourceUrl(String adminRoot, String realm, String uri) {
+        if (!uri.startsWith("http:") && !uri.startsWith("https:")) {
+            if ("realms".equals(uri) || uri.startsWith("realms/")) {
+                uri = normalize(adminRoot) + uri;
+            } else if ("serverinfo".equals(uri)) {
+                    uri = normalize(adminRoot) + uri;
+            } else {
+                uri = normalize(adminRoot) + "realms/" + realm + "/" + uri;
+            }
+        }
+        return uri;
+    }
+
+    public static String normalize(String value) {
+        return value.endsWith("/") ? value : value + "/";
+    }
+
+    public static void checkSuccess(String url, HeadersBodyStatus response) {
+        try {
+            response.checkSuccess();
+        } catch (HttpResponseException e) {
+            if (e.getStatusCode() == 404) {
+                throw new RuntimeException("Resource not found for url: " + url, e);
+            }
+            throw e;
+        }
+    }
+
+    public static <T> T doGetJSON(Class<T> type, String resourceUrl, String auth) {
+
+        Headers headers = new Headers();
+        if (auth != null) {
+            headers.add("Authorization", auth);
+        }
+        headers.add("Accept", "application/json");
+
+        HeadersBodyStatus response;
+        try {
+            response = HttpUtil.doRequest("get", resourceUrl, new HeadersBody(headers));
+        } catch (IOException e) {
+            throw new RuntimeException("HTTP request failed: GET " + resourceUrl, e);
+        }
+
+        checkSuccess(resourceUrl, response);
+
+        T result;
+        try {
+            result = JsonSerialization.readValue(response.getBody(), type);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to read JSON response", e);
+        }
+
+        return result;
+    }
+
+    public static void doPostJSON(String resourceUrl, String auth, Object content) {
+        Headers headers = new Headers();
+        if (auth != null) {
+            headers.add("Authorization", auth);
+        }
+        headers.add("Content-Type", "application/json");
+
+        HeadersBodyStatus response;
+
+        byte[] body;
+        try {
+            body = JsonSerialization.writeValueAsBytes(content);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to serialize JSON", e);
+        }
+
+        try {
+            response = HttpUtil.doRequest("post", resourceUrl, new HeadersBody(headers, new ByteArrayInputStream(body)));
+        } catch (IOException e) {
+            throw new RuntimeException("HTTP request failed: POST " + resourceUrl + "\n" + new String(body), e);
+        }
+
+        checkSuccess(resourceUrl, response);
+    }
+
+    public static void doDeleteJSON(String resourceUrl, String auth, Object content) {
+        Headers headers = new Headers();
+        if (auth != null) {
+            headers.add("Authorization", auth);
+        }
+        headers.add("Content-Type", "application/json");
+
+        HeadersBodyStatus response;
+
+        byte[] body;
+        try {
+            body = JsonSerialization.writeValueAsBytes(content);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to serialize JSON", e);
+        }
+
+        try {
+            response = HttpUtil.doRequest("delete", resourceUrl, new HeadersBody(headers, new ByteArrayInputStream(body)));
+        } catch (IOException e) {
+            throw new RuntimeException("HTTP request failed: DELETE " + resourceUrl + "\n" + new String(body), e);
+        }
+
+        checkSuccess(resourceUrl, response);
+    }
+
+    public static String getIdForType(String rootUrl, String realm, String auth, String resourceEndpoint, String attrName, String attrValue) {
+
+        return getAttrForType(rootUrl, realm, auth, resourceEndpoint, attrName, attrValue, "id");
+    }
+
+    public static String getAttrForType(String rootUrl, String realm, String auth, String resourceEndpoint, String attrName, String attrValue, String returnAttrName) {
+
+        String resourceUrl = composeResourceUrl(rootUrl, realm, resourceEndpoint);
+        resourceUrl = HttpUtil.addQueryParamsToUri(resourceUrl, attrName, attrValue, "first", "0", "max", "2");
+
+        List<ObjectNode> users = doGetJSON(RoleOperations.LIST_OF_NODES.class, resourceUrl, auth);
+
+        ObjectNode user;
+        try {
+            user = new LocalSearch(users).exactMatchOne(attrValue, attrName);
+        } catch (Exception e) {
+            throw new RuntimeException("Multiple " + resourceEndpoint + " found for " + attrName + ": " + attrValue, e);
+        }
+
+        String typeName = singularize(resourceEndpoint);
+        if (user == null) {
+            throw new RuntimeException(capitalize(typeName) + " not found for " + attrName + ": " + attrValue);
+        }
+
+        JsonNode attr = user.get(returnAttrName);
+        if (attr == null) {
+            throw new RuntimeException("Returned " + typeName + " info has no '" + returnAttrName + "' attribute");
+        }
+        return attr.asText();
+    }
+
+
+    public static String singularize(String value) {
+        return value.substring(0, value.length()-1);
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/IoUtil.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/IoUtil.java
new file mode 100644
index 0000000..f0c50d3
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/IoUtil.java
@@ -0,0 +1,255 @@
+/*
+ * 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.client.admin.cli.util;
+
+import org.jboss.aesh.console.AeshConsoleBufferBuilder;
+import org.jboss.aesh.console.AeshInputProcessorBuilder;
+import org.jboss.aesh.console.ConsoleBuffer;
+import org.jboss.aesh.console.InputProcessor;
+import org.jboss.aesh.console.Prompt;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.admin.cli.aesh.Globals;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.AclEntry;
+import java.nio.file.attribute.AclEntryPermission;
+import java.nio.file.attribute.AclEntryType;
+import java.nio.file.attribute.AclFileAttributeView;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.UserPrincipal;
+import java.util.Formatter;
+import java.util.HashSet;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+
+import static java.nio.file.Files.createDirectories;
+import static java.nio.file.Files.createFile;
+import static java.nio.file.Files.isDirectory;
+import static java.nio.file.Files.isRegularFile;
+import static org.keycloak.client.admin.cli.util.OsUtil.OS_ARCH;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class IoUtil {
+
+    public static String readFileOrStdin(String file) {
+        String content;
+        if ("-".equals(file)) {
+            content = readFully(System.in);
+        } else {
+            try (InputStream is = new FileInputStream(file)) {
+                content = readFully(is);
+            } catch (FileNotFoundException e) {
+                throw new RuntimeException("File not found: " + file);
+            } catch (IOException e) {
+                throw new RuntimeException("Failed to read file: " + file, e);
+            }
+        }
+        return content;
+    }
+
+    public static void waitFor(long millis) {
+        try {
+            Thread.sleep(millis);
+        } catch (InterruptedException e) {
+            throw new RuntimeException("Interrupted");
+        }
+    }
+
+    public static String readSecret(String prompt, CommandInvocation invocation) {
+
+        // TODO Windows hack - masking not working on Windows
+        char maskChar = OS_ARCH.isWindows() ? 0 : '*';
+        ConsoleBuffer consoleBuffer = new AeshConsoleBufferBuilder()
+                .shell(invocation.getShell())
+                .prompt(new Prompt(prompt, maskChar))
+                .create();
+        InputProcessor inputProcessor = new AeshInputProcessorBuilder()
+                .consoleBuffer(consoleBuffer)
+                .create();
+
+        consoleBuffer.displayPrompt();
+
+        // activate stdin
+        Globals.stdin.setInputStream(System.in);
+
+        String result;
+        try {
+            do {
+                result = inputProcessor.parseOperation(invocation.getInput());
+            } while (result == null);
+        } catch (Exception e) {
+            throw new RuntimeException("^C", e);
+        }
+        /*
+        if (!Globals.stdin.isStdinAvailable()) {
+            try {
+                return readLine(new InputStreamReader(System.in));
+            } catch (IOException e) {
+                throw new RuntimeException("Standard input not available");
+            }
+        }
+         */
+        // Windows hack - get rid of any \n
+        result = result.replaceAll("\\n", "");
+        return result;
+    }
+
+    public static String readFully(InputStream is) {
+        Charset charset = Charset.forName("utf-8");
+        StringBuilder out = new StringBuilder();
+        byte [] buf = new byte[8192];
+
+        int rc;
+        try {
+            while ((rc = is.read(buf)) != -1) {
+                out.append(new String(buf, 0, rc, charset));
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to read stream", e);
+        }
+        return out.toString();
+    }
+
+    public static void copyStream(InputStream is, OutputStream os) {
+
+        byte [] buf = new byte[8192];
+
+        int rc;
+        try (InputStream input = is) {
+            while ((rc = input.read(buf)) != -1) {
+                os.write(buf, 0, rc);
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to read/write a stream: ", e);
+        } finally {
+            try {
+                os.flush();
+            } catch (IOException e) {
+                throw new RuntimeException("Failed to write a stream: ", e);
+            }
+        }
+    }
+
+    public static void ensureFile(Path path) throws IOException {
+
+        FileSystem fs = FileSystems.getDefault();
+        Set<String> supportedViews = fs.supportedFileAttributeViews();
+        Path parent = path.getParent();
+
+        if (!isDirectory(parent)) {
+            createDirectories(parent);
+            // make sure only owner can read/write it
+            if (supportedViews.contains("posix")) {
+                setUnixPermissions(parent);
+            } else if (supportedViews.contains("acl")) {
+                setWindowsPermissions(parent);
+            } else {
+                warnErr("Failed to restrict access permissions on .keycloak directory: " + parent);
+            }
+        }
+        if (!isRegularFile(path)) {
+            createFile(path);
+            // make sure only owner can read/write it
+            if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
+                setUnixPermissions(path);
+            } else if (supportedViews.contains("acl")) {
+                setWindowsPermissions(path);
+            } else {
+                warnErr("Failed to restrict access permissions on config file: " + path);
+            }
+        }
+    }
+
+    private static void setUnixPermissions(Path path) throws IOException {
+        Set<PosixFilePermission> perms = new HashSet<>();
+        perms.add(PosixFilePermission.OWNER_READ);
+        perms.add(PosixFilePermission.OWNER_WRITE);
+        if (isDirectory(path)) {
+            perms.add(PosixFilePermission.OWNER_EXECUTE);
+        }
+        Files.setPosixFilePermissions(path, perms);
+    }
+
+    private static void setWindowsPermissions(Path path) throws IOException {
+        AclFileAttributeView view = Files.getFileAttributeView(path, AclFileAttributeView.class);
+        UserPrincipal owner = view.getOwner();
+        List<AclEntry> acl = view.getAcl();
+        ListIterator<AclEntry> it = acl.listIterator();
+        while (it.hasNext()) {
+            AclEntry entry = it.next();
+            if ("BUILTIN\\Administrators".equals(entry.principal().getName()) || "NT AUTHORITY\\SYSTEM".equals(entry.principal().getName())) {
+                continue;
+            }
+            it.remove();
+        }
+        AclEntry entry = AclEntry.newBuilder()
+                .setType(AclEntryType.ALLOW)
+                .setPrincipal(owner)
+                .setPermissions(AclEntryPermission.READ_DATA, AclEntryPermission.WRITE_DATA,
+                        AclEntryPermission.APPEND_DATA, AclEntryPermission.READ_NAMED_ATTRS,
+                        AclEntryPermission.WRITE_NAMED_ATTRS, AclEntryPermission.EXECUTE,
+                        AclEntryPermission.READ_ATTRIBUTES, AclEntryPermission.WRITE_ATTRIBUTES,
+                        AclEntryPermission.DELETE, AclEntryPermission.READ_ACL, AclEntryPermission.SYNCHRONIZE)
+                .build();
+        acl.add(entry);
+        view.setAcl(acl);
+    }
+
+    public static void printOut(String msg) {
+        System.out.println(msg);
+    }
+
+    public static void printErr(String msg) {
+        System.err.println(msg);
+    }
+
+    public static void printfOut(String format, String ... params) {
+        System.out.println(new Formatter().format("WARN: " + format, params));
+    }
+
+    public static void warnOut(String msg) {
+        System.out.println("WARN: " + msg);
+    }
+
+    public static void warnErr(String msg) {
+        System.err.println("WARN: " + msg);
+    }
+
+    public static void warnfOut(String format, String ... params) {
+        System.out.println(new Formatter().format("WARN: " + format, params));
+    }
+
+    public static void warnfErr(String format, String ... params) {
+        System.err.println(new Formatter().format("WARN: " + format, params));
+    }
+
+    public static void logOut(String msg) {
+        System.out.println("LOG: " + msg);
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OsArch.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OsArch.java
new file mode 100644
index 0000000..13cf424
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OsArch.java
@@ -0,0 +1,71 @@
+/*
+ * 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.client.admin.cli.util;
+
+/**
+ * @author <a href="mailto:marko.strukelj@gmail.com">Marko Strukelj</a>
+ */
+public class OsArch {
+
+    private String os;
+    private String arch;
+    private boolean legacy;
+
+    public OsArch(String os, String arch) {
+        this(os, arch, false);
+    }
+
+    public OsArch(String os, String arch, boolean legacy) {
+        this.os = os;
+        this.arch = arch;
+        this.legacy = legacy;
+    }
+
+    public String os() {
+        return os;
+    }
+
+    public String arch() {
+        return arch;
+    }
+
+    public boolean isLegacy() {
+        return legacy;
+    }
+
+    public boolean isWindows() {
+        return "win32".equals(os);
+    }
+
+    public String envVar(String var) {
+        if (isWindows()) {
+            return "%" + var + "%";
+        } else {
+            return "$" + var;
+        }
+    }
+
+    public String path(String path) {
+        if (isWindows()) {
+            path = path.replaceAll("/", "\\\\");
+            if (path.startsWith("~")) {
+                path =  "%HOMEPATH%" + path.substring(1);
+            }
+        }
+        return path;
+    }
+}
\ No newline at end of file
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OsUtil.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OsUtil.java
new file mode 100644
index 0000000..d7a3840
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OsUtil.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.client.admin.cli.util;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class OsUtil {
+
+    public static final OsArch OS_ARCH = determineOSAndArch();
+    // TODO: move CMD out of this class
+    public static final String CMD = OS_ARCH.isWindows() ? "kcadm.bat" : "kcadm.sh";
+
+    public static final String PROMPT = OS_ARCH.isWindows() ? "c:\\>" : "$";
+
+    public static final String EOL = OS_ARCH.isWindows() ? "\r\n" : "\n";
+
+
+    public static OsArch determineOSAndArch() {
+        String os = System.getProperty("os.name").toLowerCase();
+        String arch = System.getProperty("os.arch");
+
+        if (arch.equals("amd64")) {
+            arch = "x86_64";
+        }
+
+        if (os.startsWith("linux")) {
+            if (arch.equals("x86") || arch.equals("i386") || arch.equals("i586")) {
+                arch = "i686";
+            }
+            return new OsArch("linux", arch);
+        } else if (os.startsWith("windows")) {
+            if (arch.equals("x86")) {
+                arch = "i386";
+            }
+            if (os.indexOf("2008") != -1 || os.indexOf("2003") != -1 || os.indexOf("vista") != -1) {
+                return new OsArch("win32", arch, true);
+            } else {
+                return new OsArch("win32", arch);
+            }
+        } else if (os.startsWith("sunos")) {
+            return new OsArch("sunos5", "x86_64");
+        } else if (os.startsWith("mac os x")) {
+            return new OsArch("osx", "x86_64");
+        }
+
+        // unsupported platform
+        throw new RuntimeException("Could not determine OS and architecture for this operating system: " + os);
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OutputFormat.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OutputFormat.java
new file mode 100644
index 0000000..31181cc
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OutputFormat.java
@@ -0,0 +1,25 @@
+/*
+ * 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.client.admin.cli.util;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public enum OutputFormat {
+    JSON,
+    CSV
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OutputUtil.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OutputUtil.java
new file mode 100644
index 0000000..a9931d7
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/OutputUtil.java
@@ -0,0 +1,107 @@
+/*
+ * 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.client.admin.cli.util;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.Map;
+
+import static org.keycloak.client.admin.cli.util.IoUtil.printOut;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class OutputUtil {
+
+    public static ObjectMapper MAPPER = new ObjectMapper();
+
+    static {
+        MAPPER.enable(SerializationFeature.INDENT_OUTPUT);
+        MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+    }
+
+    public static JsonNode convertToJsonNode(Object object) throws IOException {
+        if (object instanceof JsonNode) {
+            return (JsonNode) object;
+        }
+
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        buffer.write(JsonSerialization.writeValueAsBytes(object));
+        return MAPPER.readValue(buffer.toByteArray(), JsonNode.class);
+    }
+
+
+    public static void printAsCsv(Object object, ReturnFields fields, boolean unquoted) throws IOException {
+
+        JsonNode node = convertToJsonNode(object);
+        if (!node.isArray()) {
+            ArrayNode listNode = MAPPER.createArrayNode();
+            listNode.add(node);
+            node = listNode;
+        }
+
+        for (JsonNode item: node) {
+            StringBuilder buffer = new StringBuilder();
+            printObjectAsCsv(buffer, item, fields, unquoted);
+
+            printOut(buffer.length() > 0 ? buffer.substring(1) : "");
+        }
+    }
+
+    static void printObjectAsCsv(StringBuilder out, JsonNode node, boolean unquoted) {
+        printObjectAsCsv(out, node, null, unquoted);
+    }
+
+    static void printObjectAsCsv(StringBuilder out, JsonNode node, ReturnFields fields, boolean unquoted) {
+
+        if (node.isObject()) {
+            if (fields == null) {
+                Iterator<Map.Entry<String, JsonNode>> it = node.fields();
+                while (it.hasNext()) {
+                    printObjectAsCsv(out, it.next().getValue(), unquoted);
+                }
+            } else {
+                Iterator<String> it = fields.iterator();
+                while (it.hasNext()) {
+                    String field = it.next();
+                    JsonNode attr = node.get(field);
+                    printObjectAsCsv(out, attr, fields.child(field), unquoted);
+                }
+            }
+        } else if (node.isArray()) {
+            for (JsonNode item: node) {
+                printObjectAsCsv(out, item, fields, unquoted);
+            }
+        } else if (node != null) {
+            out.append(",");
+            if (unquoted && node instanceof TextNode) {
+                out.append(node.asText());
+            } else {
+                out.append(node.toString());
+            }
+        }
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ParseUtil.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ParseUtil.java
new file mode 100644
index 0000000..e1b2b9a
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ParseUtil.java
@@ -0,0 +1,111 @@
+/*
+ * 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.client.admin.cli.util;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.keycloak.client.admin.cli.common.AttributeOperation;
+import org.keycloak.client.admin.cli.common.CmdStdinContext;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.keycloak.client.admin.cli.util.IoUtil.readFileOrStdin;
+import static org.keycloak.client.admin.cli.util.ReflectionUtil.setAttributes;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class ParseUtil {
+
+    public static String[] parseKeyVal(String keyval) {
+        // we expect = as a separator
+        int pos = keyval.indexOf("=");
+        if (pos <= 0) {
+            throw new RuntimeException("Invalid key=value parameter: [" + keyval + "]");
+        }
+
+        String [] parsed = new String[2];
+        parsed[0] = keyval.substring(0, pos);
+        parsed[1] = keyval.substring(pos+1);
+
+        return parsed;
+    }
+
+    public static CmdStdinContext<JsonNode> parseFileOrStdin(String file) {
+
+        String content = readFileOrStdin(file).trim();
+        JsonNode result = null;
+
+        if (content.length() == 0) {
+            throw new RuntimeException("Document provided by --file option is empty");
+        }
+
+        try {
+            result = JsonSerialization.readValue(content, JsonNode.class);
+        } catch (JsonParseException e) {
+            throw new RuntimeException("Not a valid JSON document - " + e.getMessage(), e);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to read the input document as JSON: " + e.getMessage(), e);
+        } catch (Exception e) {
+            throw new RuntimeException("Not a valid JSON document", e);
+        }
+
+        CmdStdinContext<JsonNode> ctx = new CmdStdinContext<>();
+        ctx.setContent(content);
+        ctx.setResult(result);
+        return ctx;
+    }
+
+    public static <T> CmdStdinContext<JsonNode> mergeAttributes(CmdStdinContext<JsonNode> ctx, ObjectNode newObject, List<AttributeOperation> attrs) {
+        String content = ctx.getContent();
+        JsonNode node = ctx.getResult();
+        if (node != null && !node.isObject()) {
+            throw new RuntimeException("Not a JSON object: " + node);
+        }
+        ObjectNode result = (ObjectNode) node;
+        try {
+
+            if (result == null) {
+                try {
+                    result = newObject;
+                } catch (Throwable e) {
+                    throw new RuntimeException("Failed to instantiate object: " + e.getMessage(), e);
+                }
+            }
+
+            if (result != null) {
+                try {
+                    setAttributes(result, attrs);
+                } catch (AttributeException e) {
+                    throw new RuntimeException("Failed to set attribute '" + e.getAttributeName() + "' on document type '" + result.getClass().getName() + "'", e);
+                }
+                content = JsonSerialization.writeValueAsString(result);
+            } else {
+                throw new RuntimeException("Setting attributes is not supported for type: " + result.getClass().getName());
+            }
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to merge set attributes with configuration from file", e);
+        }
+
+        ctx.setContent(content);
+        ctx.setResult(result);
+        return ctx;
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ReflectionUtil.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ReflectionUtil.java
new file mode 100644
index 0000000..81c2d29
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ReflectionUtil.java
@@ -0,0 +1,228 @@
+/*
+ * 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.client.admin.cli.util;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.BooleanNode;
+import com.fasterxml.jackson.databind.node.DoubleNode;
+import com.fasterxml.jackson.databind.node.LongNode;
+import com.fasterxml.jackson.databind.node.NullNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import org.keycloak.client.admin.cli.common.AttributeKey;
+import org.keycloak.client.admin.cli.common.AttributeOperation;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.SET;
+import static org.keycloak.client.admin.cli.util.OutputUtil.MAPPER;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class ReflectionUtil {
+
+    public static void setAttributes(JsonNode client, List<AttributeOperation> attrs) {
+        for (AttributeOperation item: attrs) {
+            AttributeKey attr = item.getKey();
+            JsonNode nested = client;
+
+            List<AttributeKey.Component> cs = attr.getComponents();
+            for (int i = 0; i < cs.size(); i++) {
+                AttributeKey.Component c = cs.get(i);
+
+                // if this is the last component of the name,
+                //    then if SET we need to set value on nested:
+                //             if value already set on nested, then overwrite, maybe remove node + add new node
+                //         if DELETE we need to remove or nullify value (if isArray)
+                // else get child and
+                //    if exist set nested to child
+                //    else if SET create new empty object or array - depending on c.isArray()
+                //
+
+                // if this is the last component of the name
+                if (i == cs.size() - 1) {
+                    String val = item.getValue();
+                    ObjectNode obj = (ObjectNode) nested;
+
+                    if (SET == item.getType()) {
+                        JsonNode valNode = valueToJsonNode(val);
+                        if (c.isArray() || attr.isAppend()) {
+                            JsonNode list = obj.get(c.getName());
+                            // child expected to be an array
+                            if ( ! (list instanceof ArrayNode)) {
+                                // replace with new array
+                                list = MAPPER.createArrayNode();
+                                obj.set(c.getName(), list);
+                            }
+                            setArrayItem((ArrayNode) list, c.getIndex(), valNode);
+                        } else {
+                            ((ObjectNode) nested).set(c.getName(), valNode);
+                        }
+                    } else {
+                        // type == DELETE
+                        if (c.isArray()) {
+                            JsonNode list = obj.get(c.getName());
+                            // child expected to be an array
+                            if (list instanceof ArrayNode) {
+                                removeArrayItem((ArrayNode) list, c.getIndex());
+                            }
+                        } else {
+                            obj.remove(c.getName());
+                        }
+                    }
+                } else {
+                    // get child and
+                    //    if exist set nested to child
+                    //    else create new empty object or array - depending on c.isArray()
+                    JsonNode node = nested.get(c.getName());
+                    if (node == null) {
+                        if (c.isArray()) {
+                            node = MAPPER.createArrayNode();
+                        } else {
+                            node = MAPPER.createObjectNode();
+                        }
+                        ((ObjectNode) nested).set(c.getName(), node);
+                    }
+                    nested = node;
+                }
+            }
+        }
+    }
+
+    private static void setArrayItem(ArrayNode list, int index, JsonNode valNode) {
+        if (index == -1) {
+            // append to end of array
+            list.add(valNode);
+            return;
+        }
+        // make sure items up to index exist
+        for (int i = list.size(); i < index+1; i++) {
+            list.add(NullNode.instance);
+        }
+        list.set(index, valNode);
+    }
+
+    private static void removeArrayItem(ArrayNode list, int index) {
+        if (index == -1) {
+            throw new IllegalArgumentException("Internal error - should never be called with index == -1");
+        }
+        list.remove(index);
+    }
+
+    private static JsonNode valueToJsonNode(String val) {
+        // try get value as JSON object
+        try {
+            return MAPPER.readValue(val, ObjectNode.class);
+        } catch (Exception ignored) {
+        }
+
+        // try get value as JSON array
+        try {
+            return MAPPER.readValue(val, ArrayNode.class);
+        } catch (Exception ignored) {
+        }
+
+        if (isBoolean(val)) {
+            return BooleanNode.valueOf(Boolean.valueOf(val));
+        } else if (isInteger(val)) {
+            return LongNode.valueOf(Long.valueOf(val));
+        } else if (isNumber(val)) {
+            return DoubleNode.valueOf(Double.valueOf(val));
+        } else if (isQuoted(val)) {
+            return TextNode.valueOf(unquote(val));
+        }
+
+        return TextNode.valueOf(val);
+    }
+
+    private static boolean isInteger(String val) {
+        try {
+            Long.valueOf(val);
+            return true;
+        } catch (Exception ignored) {
+            return false;
+        }
+    }
+
+    private static boolean isNumber(String val) {
+        try {
+            Double.valueOf(val);
+            return true;
+        } catch (Exception ignored) {
+            return false;
+        }
+    }
+
+    private static boolean isBoolean(String val) {
+        return "false".equals(val) || "true".equals(val);
+    }
+
+    private static boolean isQuoted(String val) {
+        return val.startsWith("'") || val.startsWith("\"");
+    }
+
+    private static String unquote(String val) {
+        if (!(val.startsWith("'") || val.startsWith("\"")) || !(val.endsWith("'") || val.endsWith("\""))) {
+            throw new RuntimeException("Invalid string value: " + val);
+        }
+        return val.substring(1, val.length()-1);
+    }
+
+    public static void merge(JsonNode source, ObjectNode dest) {
+        // Iterate over source
+        // For each child check if exists on the destination
+        // if it does go deep
+        // otherwise copy over
+        // if it's last component, set it on destination
+
+        if (!source.isObject()) {
+            throw new RuntimeException("Not a JSON object: " + source);
+        }
+
+        Iterator<Map.Entry<String, JsonNode>> it = ((ObjectNode) source).fields();
+        while (it.hasNext()) {
+            Map.Entry<String, JsonNode> item = it.next();
+            String name = item.getKey();
+            JsonNode node = item.getValue();
+
+            JsonNode destNode = dest.get(name);
+            if (destNode != null) {
+                if (destNode.isObject()) {
+                    if (node.isObject()) {
+                        merge(node, (ObjectNode) destNode);
+                    } else {
+                        throw new RuntimeException("Attribute is of incompatible type - " + name + ": " + node);
+                    }
+                } else if (destNode.isArray()) {
+                    if (node.isArray()) {
+                        dest.set(name, node);
+                    } else {
+                        throw new RuntimeException("Attribute is of incompatible type - " + name + ": " + node);
+                    }
+                } else {
+                    dest.set(name, node);
+                }
+            } else {
+                dest.set(name, node);
+            }
+        }
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ReturnFields.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ReturnFields.java
new file mode 100644
index 0000000..a185471
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ReturnFields.java
@@ -0,0 +1,333 @@
+/*
+ * 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.client.admin.cli.util;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:marko.strukelj@gmail.com">Marko Strukelj</a>
+ */
+public class ReturnFields implements Iterable<String> {
+
+    public static ReturnFields ALL = new ReturnFields() {
+        @Override
+        public ReturnFields child(String field) {
+            return NONE;
+        }
+
+        @Override
+        public boolean included(String... pathSegments) {
+            return true;
+        }
+
+        @Override
+        public boolean excluded(String field) {
+            return false;
+        }
+
+        @Override
+        public Iterator<String> iterator() {
+            return Collections.singletonList("*").iterator();
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return false;
+        }
+
+        public boolean isAll() {
+            return true;
+        }
+
+        @Override
+        public String toString() {
+            return "[ReturnFields ALL]";
+        }
+    };
+
+    public static ReturnFields NONE = new ReturnFields() {
+        @Override
+        public ReturnFields child(String field) {
+            return this;
+        }
+
+        @Override
+        public boolean included(String... pathSegments) {
+            return false;
+        }
+
+        @Override
+        public boolean excluded(String field) {
+            return false;
+        }
+
+        @Override
+        public Iterator<String> iterator() {
+            List<String> emptyList = Collections.emptyList();
+            return emptyList.iterator();
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return true;
+        }
+
+        @Override
+        public boolean isAll() {
+            return false;
+        }
+
+        @Override
+        public String toString() {
+            return "[ReturnFields NONE]";
+        }
+    };
+
+    public static ReturnFields ALL_RECURSIVELY = new ReturnFields() {
+        @Override
+        public ReturnFields child(String field) {
+            return this;
+        }
+
+        @Override
+        public boolean included(String... pathSegments) {
+            return true;
+        }
+
+        @Override
+        public boolean excluded(String field) {
+            return false;
+        }
+
+        @Override
+        public Iterator<String> iterator() {
+            List<String> emptyList = Collections.emptyList();
+            return emptyList.iterator();
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return false;
+        }
+
+        @Override
+        public boolean isAll() {
+            return true;
+        }
+    };
+    
+    private enum TargetState {
+        IdentCommaOpen,
+        Ident,
+        Comma,
+        Anything
+    }
+
+    private enum FieldState {
+        start,
+        name,
+        end
+    }
+
+
+    private HashMap<String, ReturnFields> fields = new LinkedHashMap<>();
+    
+    
+    
+    public ReturnFields() {}
+    
+    public ReturnFields(String spec) {
+
+        if (spec == null || spec.trim().length() == 0) {
+            throw new IllegalArgumentException("Fields spec is null or empty!");
+        }
+        // parse the spec, building up the tree for nested children
+        char[] buf = spec.toCharArray();
+        StringBuilder token = new StringBuilder(buf.length);
+
+        // stack for handling depth
+        LinkedList<HashMap<String, ReturnFields>> specs = new LinkedList<>();
+        specs.add(fields);
+
+        // parser state
+        FieldState fldState = FieldState.start;
+        TargetState state = TargetState.Ident;
+
+        int i;
+        for (i = 0; i < buf.length; i++) {
+            char c = buf[i];
+
+            if (c == ',') {
+                if (state == TargetState.Ident) {
+                    error(spec, i);
+                }
+                if (fldState == FieldState.name) {
+                    specs.getLast().put(token.toString(), null);
+                    token.setLength(0);
+                }
+                state = TargetState.Ident;
+                fldState = FieldState.start;
+            } else if (c == '(') {
+                if (state != TargetState.IdentCommaOpen && state != TargetState.Anything) {
+                    error(spec, i);
+                }
+                ReturnFields sub = new ReturnFields();
+                specs.getLast().put(token.toString(), sub);
+                specs.add(sub.fields);
+                token.setLength(0);
+
+                state = TargetState.Ident;
+                fldState = FieldState.start;
+            } else if (c == ')') {
+                if (state != TargetState.Anything) {
+                    error(spec, i);
+                }
+                if (fldState == FieldState.name) {
+                    specs.getLast().put(token.toString(), null);
+                    token.setLength(0);
+
+                }
+                specs.removeLast();
+
+                fldState = FieldState.end;
+                state = specs.size() > 1 ? TargetState.Anything : TargetState.Comma;
+            } else {
+                token.append(c);
+                if (fldState == FieldState.start) {
+                    fldState = FieldState.name;
+                    state = specs.size() > 1 ? TargetState.Anything : TargetState.IdentCommaOpen;
+                }
+            }
+        }
+
+        if (specs.size() > 1) {
+            error(spec, i);
+        }
+
+        if (token.length() > 0) {
+            specs.getLast().put(token.toString(), null);
+        } else if (!(state == TargetState.Anything || state == TargetState.Comma)) {
+            error(spec, i);
+        }
+    }
+
+    private void error(String spec, int i) {
+        throw new RuntimeException("Invalid fields specification at position " + i + ": " + spec);
+    }
+
+    
+    
+    /**
+     * Get ReturnFields for a child field of JSONObject type.
+     *
+     * <p>For basic-typed fields this always returns null. Use included() for those.</p>
+     *
+     * @param field The child field name for nested returns.
+     * @return ReturnFields for a child field
+     */
+    public ReturnFields child(String field) {
+        ReturnFields returnFields = fields.get(field);
+        if (returnFields == null) {
+            returnFields = fields.get("*");
+            if (returnFields == null) {
+                returnFields = ReturnFields.NONE;
+            }
+        }
+        return returnFields;
+    }
+
+    /**
+     * Check to see if the field should be included in JSON response.
+     *
+     * <p>The check can be performed for any level of depth relative to current nesting level, by specifying multiple path segments.</p>
+     *
+     * @param pathSegments Segments to test in the tree of return fields.
+     * @return true if the specified path should be part of JSON response or not
+     */
+    public boolean included(String... pathSegments) {
+
+        if (pathSegments == null || pathSegments.length == 0) {
+            throw new IllegalArgumentException("No path specified!");
+        }
+        ReturnFields current = this;
+
+        for (String path : pathSegments) {
+            if (current == null) {
+                return false;
+            }
+
+            if (current.fields.containsKey("-" + path)) {
+                return false;
+            }
+            if (current.fields.containsKey("*")) {
+                return true;
+            }
+            if (!current.fields.containsKey(path)) {
+                return false;
+            }
+            current = current.fields.get(path);
+        }
+        return true;
+    }
+
+    /**
+     * Check to see if the field specified is set to be explicitly excluded.
+     * @param field The field name to check
+     * @return If the field was explicitly set to be excluded
+     */
+    public boolean excluded(String field) {
+        if (fields.containsKey("-" + field)) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Iterate over child fields to be included in response.
+     *
+     * <p>To get nested field specifier use child(name) passing the field name this iterator returns.</p>
+     *
+     * @return iterator over child fields to be included in response.
+     */
+    public Iterator<String> iterator() {
+        return fields.keySet().iterator();
+    }
+
+    /**
+     * Determine if zero fields should be returned.
+     *
+     * @return <code>true</code> if the list is empty, else, <code>false</code>
+     */
+    public boolean isEmpty() {
+        return this.fields.isEmpty();
+    }
+
+    public boolean isAll() {
+        return this.fields.keySet().contains("*");
+    }
+
+    @Override
+    public String toString() {
+        return "[ReturnFieldsImpl: fields=" + this.fields + "]";
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/main/resources/META-INF/services/javax.ws.rs.ext.Providers b/integration/client-cli/admin-cli/src/main/resources/META-INF/services/javax.ws.rs.ext.Providers
new file mode 100644
index 0000000..bc2a259
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/main/resources/META-INF/services/javax.ws.rs.ext.Providers
@@ -0,0 +1,23 @@
+org.jboss.resteasy.plugins.providers.jackson.ResteasyJackson2Provider
+org.jboss.resteasy.plugins.providers.DataSourceProvider
+org.jboss.resteasy.plugins.providers.DocumentProvider
+org.jboss.resteasy.plugins.providers.DefaultTextPlain
+org.jboss.resteasy.plugins.providers.StringTextStar
+org.jboss.resteasy.plugins.providers.SourceProvider
+org.jboss.resteasy.plugins.providers.InputStreamProvider
+org.jboss.resteasy.plugins.providers.ReaderProvider
+org.jboss.resteasy.plugins.providers.ByteArrayProvider
+org.jboss.resteasy.plugins.providers.FormUrlEncodedProvider
+org.jboss.resteasy.plugins.providers.JaxrsFormProvider
+org.jboss.resteasy.plugins.providers.FileProvider
+org.jboss.resteasy.plugins.providers.FileRangeWriter
+org.jboss.resteasy.plugins.providers.StreamingOutputProvider
+org.jboss.resteasy.plugins.providers.IIOImageProvider
+org.jboss.resteasy.plugins.providers.SerializableProvider
+org.jboss.resteasy.plugins.interceptors.CacheControlFeature
+org.jboss.resteasy.plugins.interceptors.encoding.AcceptEncodingGZIPInterceptor
+org.jboss.resteasy.plugins.interceptors.encoding.AcceptEncodingGZIPFilter
+org.jboss.resteasy.plugins.interceptors.encoding.ClientContentEncodingAnnotationFeature
+org.jboss.resteasy.plugins.interceptors.encoding.GZIPDecodingInterceptor
+org.jboss.resteasy.plugins.interceptors.encoding.GZIPEncodingInterceptor
+org.jboss.resteasy.plugins.interceptors.encoding.ServerContentEncodingAnnotationFeature
                diff --git a/integration/client-cli/admin-cli/src/test/java/org/keycloak/client/admin/cli/util/MergeAttributesTest.java b/integration/client-cli/admin-cli/src/test/java/org/keycloak/client/admin/cli/util/MergeAttributesTest.java
new file mode 100644
index 0000000..ef6d897
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/test/java/org/keycloak/client/admin/cli/util/MergeAttributesTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.client.admin.cli.util;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.junit.Test;
+import org.keycloak.client.admin.cli.common.AttributeOperation;
+import org.keycloak.client.admin.cli.common.CmdStdinContext;
+
+import java.nio.charset.Charset;
+import java.util.LinkedList;
+import java.util.List;
+
+import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.DELETE;
+import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.SET;
+import static org.keycloak.client.admin.cli.util.OutputUtil.MAPPER;
+import static org.keycloak.client.admin.cli.util.ParseUtil.mergeAttributes;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class MergeAttributesTest {
+
+    @Test
+    public void testMergeAttrs() throws Exception {
+
+        List<AttributeOperation> attrs = new LinkedList<>();
+        attrs.add(new AttributeOperation(SET, "realm", "nurealm"));
+        attrs.add(new AttributeOperation(SET, "enabled", "true"));
+        attrs.add(new AttributeOperation(SET, "revokeRefreshToken", "true"));
+        attrs.add(new AttributeOperation(SET, "accessTokenLifespan", "900"));
+        attrs.add(new AttributeOperation(SET, "smtpServer.host", "localhost"));
+        attrs.add(new AttributeOperation(SET, "extra.key1", "somevalue"));
+        attrs.add(new AttributeOperation(SET, "extra.key2", "[\"somevalue\"]"));
+        attrs.add(new AttributeOperation(SET, "extra.key3[1]", "second item"));
+        attrs.add(new AttributeOperation(SET, "extra.key4", "\"true\""));
+        attrs.add(new AttributeOperation(SET, "extra.key5", "\"1000\""));
+        attrs.add(new AttributeOperation(DELETE, "id"));
+        attrs.add(new AttributeOperation(DELETE, "attributes.\"_browser_header.xFrameOptions\""));
+
+        String localJSON = "{\n" +
+                "  \"id\" : \"24e5d572-756a-435b-8b2b-edbd0a7aa93d\",\n" +
+                "  \"realm\" : \"demorealm\",\n" +
+                "  \"notBefore\" : 0,\n" +
+                "  \"revokeRefreshToken\" : false,\n" +
+                "  \"accessTokenLifespan\" : 300,\n" +
+                "  \"defaultRoles\" : [ \"offline_access\", \"uma_authorization\" ],\n" +
+                "  \"smtpServer\" : { },\n" +
+                "  \"attributes\" : {\n" +
+                "    \"_browser_header.xFrameOptions\" : \"SAMEORIGIN\",\n" +
+                "    \"_browser_header.contentSecurityPolicy\" : \"frame-src 'self'\"\n" +
+                "  }\n" +
+                "}";
+
+        ObjectNode localNode = MAPPER.readValue(localJSON.getBytes(Charset.forName("utf-8")), ObjectNode.class);
+        CmdStdinContext<JsonNode> ctx = new CmdStdinContext<>();
+        ctx.setResult(localNode);
+
+        ctx = mergeAttributes(ctx, MAPPER.createObjectNode(), attrs);
+        System.out.println(ctx);
+
+        String remoteJSON = "{\n" +
+                "  \"id\" : \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\",\n" +
+                "  \"realm\" : \"demorealm\",\n" +
+                "  \"notBefore\" : 0,\n" +
+                "  \"revokeRefreshToken\" : false,\n" +
+                "  \"accessTokenLifespan\" : 300,\n" +
+                "  \"defaultRoles\" : [ \"uma_authorization\" ],\n" +
+                "  \"remote\" : \"value\",\n" +
+                "  \"attributes\" : {\n" +
+                "    \"_browser_header.xFrameOptions\" : \"SAMEORIGIN\",\n" +
+                "    \"_browser_header.x\" : \"ORIGIN\",\n" +
+                "    \"_browser_header.contentSecurityPolicy\" : \"frame-src 'self'\"\n" +
+                "  }\n" +
+                "}";
+
+        ObjectNode remoteNode = MAPPER.readValue(remoteJSON.getBytes(Charset.forName("utf-8")), ObjectNode.class);
+        CmdStdinContext<ObjectNode> ctxremote = new CmdStdinContext<>();
+        ctxremote.setResult(remoteNode);
+
+        ReflectionUtil.merge(ctx.getResult(), ctxremote.getResult());
+        System.out.println(ctx);
+
+        //ctx = mergeAttributes(ctx, MAPPER.createObjectNode(), attrs);
+    }
+}
                diff --git a/integration/client-cli/admin-cli/src/test/java/org/keycloak/client/admin/cli/util/ReturnFieldsTest.java b/integration/client-cli/admin-cli/src/test/java/org/keycloak/client/admin/cli/util/ReturnFieldsTest.java
new file mode 100644
index 0000000..bd2105b
--- /dev/null
+++ b/integration/client-cli/admin-cli/src/test/java/org/keycloak/client/admin/cli/util/ReturnFieldsTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.client.admin.cli.util;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * @author <a href="mailto:marko.strukelj@gmail.com">Marko Strukelj</a>
+ */
+public class ReturnFieldsTest {
+
+    @Test
+    public void testBasic() {
+        String spec = "field1,field2,field3";
+        ReturnFields fspec = new ReturnFields(spec);
+
+        StringBuilder val = new StringBuilder();
+        for (String field : fspec) {
+            if (val.length() > 0)
+                val.append(',');
+            val.append(field);
+        }
+        Assert.assertEquals(spec, val.toString());
+
+        // check catching errors
+
+        String[] specs = {
+                "",
+                null,
+                ",",
+                "field1,",
+                ",field2"
+        };
+
+        for (String filter : specs) {
+            try {
+                fspec = new ReturnFields(filter);
+                Assert.fail("Parsing of fields spec should have failed! : " + filter);
+            } catch (Exception e) {
+                //e.printStackTrace();
+            }
+        }
+    }
+
+    @Test
+    public void testExclude() {
+        ReturnFields spec = new ReturnFields("*,-name,dog(*,-color)");
+
+        Assert.assertTrue(spec.included("foo"));
+        Assert.assertTrue(spec.included("bar"));
+        Assert.assertFalse(spec.included("name"));
+        Assert.assertTrue(spec.included("dog"));
+        Assert.assertTrue(spec.child("dog").included("breed"));
+        Assert.assertFalse(spec.child("dog").included("color"));
+
+        Assert.assertTrue(spec.excluded("name"));
+        Assert.assertFalse(spec.excluded("foo"));
+        Assert.assertFalse(spec.excluded("bar"));
+        Assert.assertTrue(spec.child("dog").excluded("color"));
+        Assert.assertFalse(spec.child("dog").excluded("breed"));
+    }
+
+    @Test
+    public void testNestedWithGlob() {
+        ReturnFields spec = new ReturnFields("name,dog(*)");
+
+        Assert.assertTrue(spec.included("name"));
+        Assert.assertFalse(spec.included("tacos"));
+
+        Assert.assertNotNull(spec.child("dog"));
+        Assert.assertTrue(spec.child("dog").included("dogname"));
+
+        Assert.assertNotNull(spec.child("cat"));
+        Assert.assertFalse(spec.child("cat").included("name"));
+    }
+
+    @Test
+    public void testNested() {
+        String spec = "field1,field2(sub1,sub2(subsub1)),field3";
+        ReturnFields fspec = new ReturnFields(spec);
+
+        String val = traverse(fspec);
+        Assert.assertEquals(spec, val.toString());
+
+
+        // check catching errors
+
+        String[] specs = {
+                "(",
+                ")",
+                "field1,(",
+                "field1,)",
+                "field1,field2(",
+                "field1,field2)",
+                "field1,field2()",
+                "field1,field2(sub1)(",
+                "field1,field2(sub1))",
+                "field1,field2(sub1),"
+        };
+
+        for (String filter : specs) {
+            try {
+                fspec = new ReturnFields(filter);
+                Assert.fail("Parsing of fields spec should have failed! : " + filter);
+            } catch (Exception e) {
+                //e.printStackTrace();
+            }
+        }
+    }
+
+    private String traverse(ReturnFields fspec) {
+        StringBuilder buf = new StringBuilder();
+        for (String field : fspec) {
+            if (buf.length() > 0)
+                buf.append(',');
+            buf.append(field);
+
+            ReturnFields cspec = fspec.child(field);
+            if (cspec != null && cspec != ReturnFields.NONE) {
+                buf.append('(');
+                buf.append(traverse(cspec));
+                buf.append(')');
+            }
+        }
+        return buf.toString();
+    }
+}
                diff --git a/integration/client-cli/client-cli-dist/assembly.xml b/integration/client-cli/client-cli-dist/assembly.xml
index ee27cb2..fc74f20 100755
--- a/integration/client-cli/client-cli-dist/assembly.xml
+++ b/integration/client-cli/client-cli-dist/assembly.xml
@@ -36,11 +36,23 @@
             <outputDirectory>keycloak-client-tools/bin</outputDirectory>
             <filtered>true</filtered>
         </file>
+        <file>
+            <source>../admin-cli/src/main/bin/kcadm.sh</source>
+            <outputDirectory>keycloak-client-tools/bin</outputDirectory>
+            <fileMode>0755</fileMode>
+            <filtered>true</filtered>
+        </file>
+        <file>
+            <source>../admin-cli/src/main/bin/kcadm.bat</source>
+            <outputDirectory>keycloak-client-tools/bin</outputDirectory>
+            <filtered>true</filtered>
+        </file>
     </files>
     <dependencySets>
         <dependencySet>
             <includes>
                 <include>org.keycloak:keycloak-client-registration-cli</include>
+                <include>org.keycloak:keycloak-admin-cli</include>
             </includes>
             <outputDirectory>keycloak-client-tools/bin/client</outputDirectory>
         </dependencySet>
                diff --git a/integration/client-cli/client-cli-dist/pom.xml b/integration/client-cli/client-cli-dist/pom.xml
index 1b567cb..daabefc 100755
--- a/integration/client-cli/client-cli-dist/pom.xml
+++ b/integration/client-cli/client-cli-dist/pom.xml
@@ -34,6 +34,10 @@
             <groupId>org.keycloak</groupId>
             <artifactId>keycloak-client-registration-cli</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-admin-cli</artifactId>
+        </dependency>
     </dependencies>
 
     <build>
                diff --git a/integration/client-cli/client-registration-cli/pom.xml b/integration/client-cli/client-registration-cli/pom.xml
index dc5c210..63a6673 100755
--- a/integration/client-cli/client-registration-cli/pom.xml
+++ b/integration/client-cli/client-registration-cli/pom.xml
@@ -33,7 +33,6 @@
         <dependency>
             <groupId>org.jboss.aesh</groupId>
             <artifactId>aesh</artifactId>
-            <version>0.66.10</version>
         </dependency>
         <dependency>
             <groupId>org.keycloak</groupId>
                diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigTruststoreCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigTruststoreCmd.java
index b8f1f34..ecadf62 100644
--- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigTruststoreCmd.java
+++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigTruststoreCmd.java
@@ -1,3 +1,19 @@
+/*
+ * 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.client.registration.cli.commands;
 
 import org.jboss.aesh.cl.CommandDefinition;
@@ -153,7 +169,7 @@ public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd implements Comma
     public static String usage() {
         StringWriter sb = new StringWriter();
         PrintWriter out = new PrintWriter(sb);
-        out.println("Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWOD] [ARGUMENTS]");
+        out.println("Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWORD] [ARGUMENTS]");
         out.println();
         out.println("Command to configure a global truststore to use when using https to connect to Keycloak server.");
         out.println();
@@ -174,7 +190,7 @@ public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd implements Comma
         out.println("Specify a truststore - you will be prompted for truststore password every time it is used:");
         out.println("  " + PROMPT + " " + CMD + " config truststore " + OS_ARCH.path("~/.keycloak/truststore.jks"));
         out.println();
-        out.println("Specify a truststore, and password - truststore will automatically without prompting for password:");
+        out.println("Specify a truststore, and password - truststore will automatically be used without prompting for password:");
         out.println("  " + PROMPT + " " + CMD + " config truststore --storepass " + OS_ARCH.envVar("PASSWORD") + " " + OS_ARCH.path("~/.keycloak/truststore.jks"));
         out.println();
         out.println("Remove truststore configuration:");
                integration/client-cli/pom.xml 11(+11 -0)
diff --git a/integration/client-cli/pom.xml b/integration/client-cli/pom.xml
index 4759514..20753c9 100644
--- a/integration/client-cli/pom.xml
+++ b/integration/client-cli/pom.xml
@@ -30,8 +30,19 @@
     <artifactId>keycloak-client-cli-parent</artifactId>
     <packaging>pom</packaging>
 
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.jboss.aesh</groupId>
+                <artifactId>aesh</artifactId>
+                <version>0.66.10</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
     <modules>
         <module>client-registration-cli</module>
+        <module>admin-cli</module>
         <module>client-cli-dist</module>
     </modules>
 </project>
\ No newline at end of file
                pom.xml 5(+5 -0)
diff --git a/pom.xml b/pom.xml
index 23c277d..5a9a3f2 100755
--- a/pom.xml
+++ b/pom.xml
@@ -1323,6 +1323,11 @@
             </dependency>
             <dependency>
                 <groupId>org.keycloak</groupId>
+                <artifactId>keycloak-admin-cli</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.keycloak</groupId>
                 <artifactId>keycloak-client-cli-dist</artifactId>
                 <version>${project.version}</version>
                 <type>zip</type>
                diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExec.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExec.java
new file mode 100644
index 0000000..91fcb13
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExec.java
@@ -0,0 +1,245 @@
+package org.keycloak.testsuite.cli.exec;
+
+import org.keycloak.testsuite.cli.OsArch;
+import org.keycloak.testsuite.cli.OsUtils;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public abstract class AbstractExec {
+
+    public static final String WORK_DIR = System.getProperty("user.dir");
+
+    public static final OsArch OS_ARCH = OsUtils.determineOSAndArch();
+
+    private long waitTimeout = 30000;
+
+    private Process process;
+
+    private int exitCode = -1;
+
+    private boolean logStreams = Boolean.valueOf(System.getProperty("cli.log.output", "true"));
+
+    protected boolean dumpStreams;
+
+    protected String workDir = WORK_DIR;
+
+    private String env;
+
+    private String argsLine;
+
+    private ByteArrayOutputStream stdout = new ByteArrayOutputStream();
+
+    private ByteArrayOutputStream stderr = new ByteArrayOutputStream();
+
+    private InputStream stdin = new InteractiveInputStream();
+
+    private Throwable err;
+
+    private Thread stdoutRunner;
+
+    private Thread stderrRunner;
+
+    public AbstractExec(String workDir, String argsLine, InputStream stdin) {
+        this(workDir, argsLine, null, stdin);
+    }
+
+    public AbstractExec(String workDir, String argsLine, String env, InputStream stdin) {
+        if (workDir != null) {
+            this.workDir = workDir;
+        }
+
+        this.argsLine = argsLine;
+        this.env = env;
+
+        if (stdin != null) {
+            this.stdin = stdin;
+        }
+    }
+
+    public abstract String getCmd();
+
+    public void execute() {
+        executeAsync();
+        if (err == null) {
+            waitCompletion();
+        }
+    }
+
+
+    public void executeAsync() {
+
+        try {
+            if (OS_ARCH.isWindows()) {
+                String cmd = (env != null ? "set " + env + " & " : "") + fixPath(getCmd()) + " " + fixQuotes(argsLine);
+                System.out.println("Executing: cmd.exe /c " + cmd);
+                process = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", cmd}, null, new File(workDir));
+            } else {
+                String cmd = (env != null ? env + " " : "") + getCmd() + " " + argsLine;
+                System.out.println("Executing: sh -c " + cmd);
+                process = Runtime.getRuntime().exec(new String[]{"sh", "-c", cmd}, null, new File(workDir));
+            }
+
+            stdoutRunner = new StreamReaderThread(process.getInputStream(), logStreams ? new LoggingOutputStream("STDOUT", stdout) : stdout);
+            stdoutRunner.start();
+
+            stderrRunner = new StreamReaderThread(process.getErrorStream(), logStreams ? new LoggingOutputStream("STDERR", stderr) : stderr);
+            stderrRunner.start();
+
+            new StreamReaderThread(stdin, process.getOutputStream())
+                    .start();
+
+        } catch (Throwable t) {
+            err = t;
+        }
+    }
+
+    private String fixPath(String cmd) {
+        return cmd.replaceAll("/", "\\\\");
+    }
+
+    private String fixQuotes(String argsLine) {
+        argsLine = argsLine + " ";
+        argsLine = argsLine.replaceAll("\"", "\\\\\"");
+        argsLine = argsLine.replaceAll(" '", " \"");
+        argsLine = argsLine.replaceAll("' ", "\" ");
+        return argsLine;
+    }
+
+    public void waitCompletion() {
+
+        // This is necessary to make sure the process isn't stuck reading from stdin
+        if (stdin instanceof InteractiveInputStream) {
+            ((InteractiveInputStream) stdin).close();
+        }
+        try {
+            if (process.waitFor(waitTimeout, TimeUnit.MILLISECONDS)) {
+                exitCode = process.exitValue();
+                if (exitCode != 0) {
+                    dumpStreams = true;
+                }
+                // make sure reading output is really done (just in case)
+                stdoutRunner.join(5000);
+                stderrRunner.join(5000);
+            } else {
+                if (process.isAlive()) {
+                    process.destroyForcibly();
+                }
+                throw new RuntimeException("Timeout after " + (waitTimeout / 1000) + " seconds.");
+            }
+        } catch (InterruptedException e) {
+            dumpStreams = true;
+            throw new RuntimeException("Interrupted ...", e);
+        } catch (Throwable t) {
+            dumpStreams = true;
+            err = t;
+        } finally {
+            if (!logStreams && dumpStreams) try {
+                System.out.println("STDOUT: ");
+                copyStream(new ByteArrayInputStream(stdout.toByteArray()), System.out);
+                System.out.println("STDERR: ");
+                copyStream(new ByteArrayInputStream(stderr.toByteArray()), System.out);
+            } catch (Exception ignored) {
+            }
+        }
+    }
+
+    public int exitCode() {
+        return exitCode;
+    }
+
+    public Throwable error() {
+        return err;
+    }
+
+    public InputStream stdout() {
+        return new ByteArrayInputStream(stdout.toByteArray());
+    }
+
+    public List<String> stdoutLines() {
+        return parseStreamAsLines(new ByteArrayInputStream(stdout.toByteArray()));
+    }
+
+    public String stdoutString() {
+        return new String(stdout.toByteArray());
+    }
+
+    public InputStream stderr() {
+        return new ByteArrayInputStream(stderr.toByteArray());
+    }
+
+    public List<String> stderrLines() {
+        return parseStreamAsLines(new ByteArrayInputStream(stderr.toByteArray()));
+    }
+
+    public String stderrString() {
+        return new String(stderr.toByteArray());
+    }
+
+    static List<String> parseStreamAsLines(InputStream stream) {
+        List<String> lines = new ArrayList<>();
+        try {
+            BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
+
+            String line;
+            while ((line = reader.readLine()) != null) {
+                lines.add(line);
+            }
+            return lines;
+        } catch (IOException e) {
+            throw new RuntimeException("Unexpected I/O error", e);
+        }
+    }
+
+    public void waitForStdout(String content) {
+        long start = System.currentTimeMillis();
+        while (System.currentTimeMillis() - start < waitTimeout) {
+            if (stdoutString().indexOf(content) != -1) {
+                return;
+            }
+            try {
+                Thread.sleep(10);
+            } catch (InterruptedException e) {
+                throw new RuntimeException("Interrupted ...", e);
+            }
+        }
+
+        throw new RuntimeException("Timed while waiting for content to appear in stdout");
+    }
+
+    public void sendToStdin(String s) {
+        if (stdin instanceof InteractiveInputStream) {
+            ((InteractiveInputStream) stdin).pushBytes(s.getBytes());
+        } else {
+            throw new RuntimeException("Can't push to stdin - not interactive");
+        }
+    }
+
+
+
+    static void copyStream(InputStream is, OutputStream os) throws IOException {
+        byte [] buf = new byte[8192];
+
+        try (InputStream iss = is) {
+            int c;
+            while ((c = iss.read(buf)) != -1) {
+                os.write(buf, 0, c);
+                os.flush();
+            }
+        }
+    }
+
+
+}
                diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExecBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExecBuilder.java
new file mode 100644
index 0000000..97ec772
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExecBuilder.java
@@ -0,0 +1,44 @@
+package org.keycloak.testsuite.cli.exec;
+
+import java.io.InputStream;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public abstract class AbstractExecBuilder<T> {
+
+    protected String workDir;
+    protected String argsLine;
+    protected InputStream stdin;
+    protected String env;
+    protected boolean dumpStreams;
+
+    public AbstractExecBuilder<T> workDir(String path) {
+        this.workDir = path;
+        return this;
+    }
+
+    public AbstractExecBuilder<T> argsLine(String cmd) {
+        this.argsLine = cmd;
+        return this;
+    }
+
+    public AbstractExecBuilder<T> stdin(InputStream is) {
+        this.stdin = is;
+        return this;
+    }
+
+    public AbstractExecBuilder<T> env(String env) {
+        this.env = env;
+        return this;
+    }
+
+    public AbstractExecBuilder<T> fullStreamDump() {
+        this.dumpStreams = true;
+        return this;
+    }
+
+    public abstract T execute();
+
+    public abstract T executeAsync();
+}
                diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/InteractiveInputStream.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/InteractiveInputStream.java
new file mode 100644
index 0000000..cbbea72
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/InteractiveInputStream.java
@@ -0,0 +1,120 @@
+package org.keycloak.testsuite.cli.exec;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.util.LinkedList;
+
+class InteractiveInputStream extends InputStream {
+
+    private LinkedList<Byte> queue = new LinkedList<>();
+
+    private Thread consumer;
+
+    private boolean closed;
+
+    @Override
+    public int read(byte b[]) throws IOException {
+        return read(b, 0, b.length);
+    }
+
+    @Override
+    public synchronized int read(byte[] b, int off, int len) throws IOException {
+
+        Byte current = null;
+        int rc = 0;
+        try {
+            consumer = Thread.currentThread();
+
+            do {
+                current = queue.poll();
+                if (current == null) {
+                    if (rc > 0) {
+                        return rc;
+                    } else {
+                        do {
+                            if (closed) {
+                                return -1;
+                            }
+                            wait();
+                        }
+                        while ((current = queue.poll()) == null);
+                    }
+                }
+
+                b[off + rc] = current;
+                rc++;
+            } while (rc < len);
+
+        } catch (InterruptedException e) {
+            throw new InterruptedIOException("Signalled to exit");
+        } finally {
+            consumer = null;
+        }
+        return rc;
+    }
+
+    @Override
+    public long skip(long n) throws IOException {
+        return super.skip(n);
+    }
+
+    @Override
+    public int available() throws IOException {
+        return super.available();
+    }
+
+    @Override
+    public synchronized void mark(int readlimit) {
+        super.mark(readlimit);
+    }
+
+    @Override
+    public synchronized void reset() throws IOException {
+        super.reset();
+    }
+
+    @Override
+    public boolean markSupported() {
+        return super.markSupported();
+    }
+
+    @Override
+    public synchronized int read() throws IOException {
+        // when input is available pass it on
+        Byte current;
+        try {
+            consumer = Thread.currentThread();
+
+            while ((current = queue.poll()) == null) {
+                // we don't check for closed before making sure
+                // that there is nothing more to read
+                if (closed) {
+                    return -1;
+                }
+                wait();
+            }
+
+        } catch (InterruptedException e) {
+            throw new InterruptedIOException("Signalled to exit");
+        } finally {
+            consumer = null;
+        }
+        return current;
+    }
+
+    @Override
+    public synchronized void close() {
+        closed = true;
+        if (consumer != null) {
+            consumer.interrupt();
+        }
+    }
+
+    public synchronized void pushBytes(byte [] buff) {
+        for (byte b : buff) {
+            queue.add(b);
+        }
+        notify();
+    }
+}
\ No newline at end of file
                diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/LoggingOutputStream.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/LoggingOutputStream.java
new file mode 100644
index 0000000..e13fbe3
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/LoggingOutputStream.java
@@ -0,0 +1,53 @@
+package org.keycloak.testsuite.cli.exec;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+class LoggingOutputStream extends FilterOutputStream {
+
+    private ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+    private String name;
+
+    public LoggingOutputStream(String name, OutputStream os) {
+        super(os);
+        this.name = name;
+    }
+
+    @Override
+    public void write(int b) throws IOException {
+        super.write(b);
+        if (b == 10) {
+            log();
+        } else {
+            buffer.write(b);
+        }
+    }
+
+    @Override
+    public void write(byte[] buf) throws IOException {
+        write(buf, 0, buf.length);
+    }
+
+    @Override
+    public void write(byte[] buf, int offs, int len) throws IOException {
+        for (int i = 0; i < len; i++) {
+            write(buf[offs+i]);
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        super.close();
+        if (buffer.size() > 0) {
+            log();
+        }
+    }
+
+    private void log() {
+        String log = new String(buffer.toByteArray());
+        buffer.reset();
+        System.out.println("[" + name + "] " + log);
+    }
+}
\ No newline at end of file
                diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/NullInputStream.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/NullInputStream.java
new file mode 100644
index 0000000..cb34314
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/NullInputStream.java
@@ -0,0 +1,12 @@
+package org.keycloak.testsuite.cli.exec;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+class NullInputStream extends InputStream {
+
+    @Override
+    public int read() throws IOException {
+        return -1;
+    }
+}
\ No newline at end of file
                diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/StreamReaderThread.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/StreamReaderThread.java
new file mode 100644
index 0000000..c7518d5
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/StreamReaderThread.java
@@ -0,0 +1,33 @@
+package org.keycloak.testsuite.cli.exec;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import static org.keycloak.testsuite.cli.exec.AbstractExec.copyStream;
+
+class StreamReaderThread extends Thread {
+
+    private InputStream is;
+    private OutputStream os;
+
+    StreamReaderThread(InputStream is, OutputStream os) {
+        this.is = is;
+        this.os = os;
+    }
+
+    public void run() {
+        try {
+            copyStream(is, os);
+        } catch (IOException e) {
+            throw new RuntimeException("Unexpected I/O error", e);
+        } finally {
+            try {
+                os.close();
+            } catch (IOException ignored) {
+                System.err.print("IGNORED: error while closing output stream: ");
+                ignored.printStackTrace();
+            }
+        }
+    }
+}
\ No newline at end of file
                diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcAdmExec.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcAdmExec.java
new file mode 100644
index 0000000..3ab6b34
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcAdmExec.java
@@ -0,0 +1,58 @@
+package org.keycloak.testsuite.cli;
+
+import org.keycloak.testsuite.cli.exec.AbstractExec;
+import org.keycloak.testsuite.cli.exec.AbstractExecBuilder;
+
+import java.io.InputStream;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KcAdmExec extends AbstractExec {
+
+    public static final String WORK_DIR = System.getProperty("user.dir") + "/target/containers/keycloak-client-tools";
+
+    public static final String CMD = OS_ARCH.isWindows() ? "kcadm.bat" : "kcadm.sh";
+
+    private KcAdmExec(String workDir, String argsLine, InputStream stdin) {
+        this(workDir, argsLine, null, stdin);
+    }
+
+    private KcAdmExec(String workDir, String argsLine, String env, InputStream stdin) {
+        super(workDir, argsLine, env, stdin);
+    }
+
+    @Override
+    public String getCmd() {
+        return "bin/" + CMD;
+    }
+
+    public static KcAdmExec.Builder newBuilder() {
+        return (KcAdmExec.Builder) new KcAdmExec.Builder().workDir(WORK_DIR);
+    }
+
+    public static KcAdmExec execute(String args) {
+        return newBuilder()
+                .argsLine(args)
+                .execute();
+    }
+
+    public static class Builder extends AbstractExecBuilder<KcAdmExec> {
+
+        @Override
+        public KcAdmExec execute() {
+            KcAdmExec exe = new KcAdmExec(workDir, argsLine, env, stdin);
+            exe.dumpStreams = dumpStreams;
+            exe.execute();
+            return exe;
+        }
+
+        @Override
+        public KcAdmExec executeAsync() {
+            KcAdmExec exe = new KcAdmExec(workDir, argsLine, env, stdin);
+            exe.dumpStreams = dumpStreams;
+            exe.executeAsync();
+            return exe;
+        }
+    }
+}
                diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcRegExec.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcRegExec.java
index 5f20c2a..56039af 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcRegExec.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcRegExec.java
@@ -1,5 +1,8 @@
 package org.keycloak.testsuite.cli;
 
+import org.keycloak.testsuite.cli.exec.AbstractExec;
+import org.keycloak.testsuite.cli.exec.AbstractExecBuilder;
+
 import java.io.BufferedReader;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -18,57 +21,27 @@ import java.util.concurrent.TimeUnit;
 /**
  * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
  */
-public class KcRegExec {
+public class KcRegExec extends AbstractExec {
 
     public static final String WORK_DIR = System.getProperty("user.dir") + "/target/containers/keycloak-client-tools";
 
-    public static final OsArch OS_ARCH = OsUtils.determineOSAndArch();
-
     public static final String CMD = OS_ARCH.isWindows() ? "kcreg.bat" : "kcreg.sh";
 
-    private long waitTimeout = 30000;
-
-    private Process process;
-
-    private int exitCode = -1;
-
-    private boolean logStreams = Boolean.valueOf(System.getProperty("cli.log.output", "true"));
-
-    private boolean dumpStreams;
-
-    private String workDir = WORK_DIR;
-
-    private String env;
-
-    private String argsLine;
-
-    private ByteArrayOutputStream stdout = new ByteArrayOutputStream();
-
-    private ByteArrayOutputStream stderr = new ByteArrayOutputStream();
-
-    private InputStream stdin = new InteractiveInputStream();
-
-    private Throwable err;
-
     private KcRegExec(String workDir, String argsLine, InputStream stdin) {
         this(workDir, argsLine, null, stdin);
     }
 
     private KcRegExec(String workDir, String argsLine, String env, InputStream stdin) {
-        if (workDir != null) {
-            this.workDir = workDir;
-        }
-
-        this.argsLine = argsLine;
-        this.env = env;
+        super(workDir, argsLine, env, stdin);
+    }
 
-        if (stdin != null) {
-            this.stdin = stdin;
-        }
+    @Override
+    public String getCmd() {
+        return "bin/" + CMD;
     }
 
-    public static Builder newBuilder() {
-        return new Builder();
+    public static KcRegExec.Builder newBuilder() {
+        return (KcRegExec.Builder) new KcRegExec.Builder().workDir(WORK_DIR);
     }
 
     public static KcRegExec execute(String args) {
@@ -77,225 +50,9 @@ public class KcRegExec {
                 .execute();
     }
 
-    public void execute() {
-        executeAsync();
-        if (err == null) {
-            waitCompletion();
-        }
-    }
-
-
-    public void executeAsync() {
-
-        try {
-            if (OS_ARCH.isWindows()) {
-                String cmd = (env != null ? "set " + env + " & " : "") + "bin\\" + CMD + " " + fixQuotes(argsLine);
-                System.out.println("Executing: cmd.exe /c " + cmd);
-                process = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", cmd}, null, new File(workDir));
-            } else {
-                String cmd = (env != null ? env + " " : "") + "bin/" + CMD + " " + argsLine;
-                System.out.println("Executing: sh -c " + cmd);
-                process = Runtime.getRuntime().exec(new String[]{"sh", "-c", cmd}, null, new File(workDir));
-            }
-
-            new StreamReaderThread(process.getInputStream(), logStreams ? new LoggingOutputStream("STDOUT", stdout) : stdout)
-                    .start();
-
-            new StreamReaderThread(process.getErrorStream(), logStreams ? new LoggingOutputStream("STDERR", stderr) : stderr)
-                    .start();
-
-            new StreamReaderThread(stdin, process.getOutputStream())
-                    .start();
-
-        } catch (Throwable t) {
-            err = t;
-        }
-    }
-
-    private String fixQuotes(String argsLine) {
-        argsLine = argsLine + " ";
-        argsLine = argsLine.replaceAll("\"", "\\\\\"");
-        argsLine = argsLine.replaceAll(" '", " \"");
-        argsLine = argsLine.replaceAll("' ", "\" ");
-        return argsLine;
-    }
-
-    public void waitCompletion() {
-
-        //if (stdin instanceof InteractiveInputStream) {
-        //    ((InteractiveInputStream) stdin).close();
-        //}
-        try {
-            if (process.waitFor(waitTimeout, TimeUnit.MILLISECONDS)) {
-                exitCode = process.exitValue();
-                if (exitCode != 0) {
-                    dumpStreams = true;
-                }
-            } else {
-                if (process.isAlive()) {
-                    process.destroyForcibly();
-                }
-                throw new RuntimeException("Timeout after " + (waitTimeout / 1000) + " seconds.");
-            }
-        } catch (InterruptedException e) {
-            dumpStreams = true;
-            throw new RuntimeException("Interrupted ...", e);
-        } catch (Throwable t) {
-            dumpStreams = true;
-            err = t;
-        } finally {
-            if (!logStreams && dumpStreams) try {
-                System.out.println("STDOUT: ");
-                copyStream(new ByteArrayInputStream(stdout.toByteArray()), System.out);
-                System.out.println("STDERR: ");
-                copyStream(new ByteArrayInputStream(stderr.toByteArray()), System.out);
-            } catch (Exception ignored) {
-            }
-        }
-    }
-
-    public int exitCode() {
-        return exitCode;
-    }
-
-    public Throwable error() {
-        return err;
-    }
-
-    public InputStream stdout() {
-        return new ByteArrayInputStream(stdout.toByteArray());
-    }
-
-    public List<String> stdoutLines() {
-        return parseStreamAsLines(new ByteArrayInputStream(stdout.toByteArray()));
-    }
-
-    public String stdoutString() {
-        return new String(stdout.toByteArray());
-    }
-
-    public InputStream stderr() {
-        return new ByteArrayInputStream(stderr.toByteArray());
-    }
-
-    public List<String> stderrLines() {
-        return parseStreamAsLines(new ByteArrayInputStream(stderr.toByteArray()));
-    }
-
-    public String stderrString() {
-        return new String(stderr.toByteArray());
-    }
-
-    static List<String> parseStreamAsLines(InputStream stream) {
-        List<String> lines = new ArrayList<>();
-        try {
-            BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
-
-            String line;
-            while ((line = reader.readLine()) != null) {
-                lines.add(line);
-            }
-            return lines;
-        } catch (IOException e) {
-            throw new RuntimeException("Unexpected I/O error", e);
-        }
-    }
-
-    public void waitForStdout(String content) {
-        long start = System.currentTimeMillis();
-        while (System.currentTimeMillis() - start < waitTimeout) {
-            if (stdoutString().indexOf(content) != -1) {
-                return;
-            }
-            try {
-                Thread.sleep(10);
-            } catch (InterruptedException e) {
-                throw new RuntimeException("Interrupted ...", e);
-            }
-        }
-
-        throw new RuntimeException("Timed while waiting for content to appear in stdout");
-    }
-
-    public void sendToStdin(String s) {
-        if (stdin instanceof InteractiveInputStream) {
-            ((InteractiveInputStream) stdin).pushBytes(s.getBytes());
-        } else {
-            throw new RuntimeException("Can't push to stdin - not interactive");
-        }
-    }
-
-    static class StreamReaderThread extends Thread {
-
-        private InputStream is;
-        private OutputStream os;
-
-        StreamReaderThread(InputStream is, OutputStream os) {
-            this.is = is;
-            this.os = os;
-        }
-
-        public void run() {
-            try {
-                copyStream(is, os);
-            } catch (IOException e) {
-                throw new RuntimeException("Unexpected I/O error", e);
-            } finally {
-                try {
-                    os.close();
-                } catch (IOException ignored) {
-                    System.err.print("IGNORED: error while closing output stream: ");
-                    ignored.printStackTrace();
-                }
-            }
-        }
-    }
-
-    static void copyStream(InputStream is, OutputStream os) throws IOException {
-        byte [] buf = new byte[8192];
-
-        try (InputStream iss = is) {
-            int c;
-            while ((c = iss.read(buf)) != -1) {
-                os.write(buf, 0, c);
-                os.flush();
-            }
-        }
-    }
-
-    public static class Builder {
-
-        private String workDir;
-        private String argsLine;
-        private InputStream stdin;
-        private String env;
-        private boolean dumpStreams;
-
-        public Builder workDir(String path) {
-            this.workDir = path;
-            return this;
-        }
-
-        public Builder argsLine(String cmd) {
-            this.argsLine = cmd;
-            return this;
-        }
-
-        public Builder stdin(InputStream is) {
-            this.stdin = is;
-            return this;
-        }
-
-        public Builder env(String env) {
-            this.env = env;
-            return this;
-        }
-
-        public Builder fullStreamDump() {
-            this.dumpStreams = true;
-            return this;
-        }
+    public static class Builder extends AbstractExecBuilder<KcRegExec> {
 
+        @Override
         public KcRegExec execute() {
             KcRegExec exe = new KcRegExec(workDir, argsLine, env, stdin);
             exe.dumpStreams = dumpStreams;
@@ -303,6 +60,7 @@ public class KcRegExec {
             return exe;
         }
 
+        @Override
         public KcRegExec executeAsync() {
             KcRegExec exe = new KcRegExec(workDir, argsLine, env, stdin);
             exe.dumpStreams = dumpStreams;
@@ -311,177 +69,4 @@ public class KcRegExec {
         }
     }
 
-    static class NullInputStream extends InputStream {
-
-        @Override
-        public int read() throws IOException {
-            return -1;
-        }
-    }
-
-    static class InteractiveInputStream extends InputStream {
-
-        private LinkedList<Byte> queue = new LinkedList<>();
-
-        private Thread consumer;
-
-        private boolean closed;
-
-        @Override
-        public int read(byte b[]) throws IOException {
-            return read(b, 0, b.length);
-        }
-
-        @Override
-        public synchronized int read(byte[] b, int off, int len) throws IOException {
-
-            Byte current = null;
-            int rc = 0;
-            try {
-                consumer = Thread.currentThread();
-
-                do {
-                    current = queue.poll();
-                    if (current == null) {
-                        if (rc > 0) {
-                            return rc;
-                        } else {
-                            do {
-                                if (closed) {
-                                    return -1;
-                                }
-                                wait();
-                            }
-                            while ((current = queue.poll()) == null);
-                        }
-                    }
-
-                    b[off + rc] = current;
-                    rc++;
-                } while (rc < len);
-
-            } catch (InterruptedException e) {
-                throw new InterruptedIOException("Signalled to exit");
-            } finally {
-                consumer = null;
-            }
-            return rc;
-        }
-
-        @Override
-        public long skip(long n) throws IOException {
-            return super.skip(n);
-        }
-
-        @Override
-        public int available() throws IOException {
-            return super.available();
-        }
-
-        @Override
-        public synchronized void mark(int readlimit) {
-            super.mark(readlimit);
-        }
-
-        @Override
-        public synchronized void reset() throws IOException {
-            super.reset();
-        }
-
-        @Override
-        public boolean markSupported() {
-            return super.markSupported();
-        }
-
-        @Override
-        public synchronized int read() throws IOException {
-            if (closed) {
-                return -1;
-            }
-
-            // when input is available pass it on
-            Byte current;
-            try {
-                consumer = Thread.currentThread();
-
-                while ((current = queue.poll()) == null) {
-                    wait();
-                    if (closed) {
-                        return -1;
-                    }
-                }
-
-            } catch (InterruptedException e) {
-                throw new InterruptedIOException("Signalled to exit");
-            } finally {
-                consumer = null;
-            }
-            return current;
-        }
-
-        @Override
-        public synchronized void close() {
-            closed = true;
-            new RuntimeException("IIS || close").printStackTrace();
-            if (consumer != null) {
-                consumer.interrupt();
-            }
-        }
-
-        public synchronized void pushBytes(byte [] buff) {
-            for (byte b : buff) {
-                queue.add(b);
-            }
-            notify();
-        }
-    }
-
-
-    static class LoggingOutputStream extends FilterOutputStream {
-
-        private ByteArrayOutputStream buffer = new ByteArrayOutputStream();
-        private String name;
-
-        public LoggingOutputStream(String name, OutputStream os) {
-            super(os);
-            this.name = name;
-        }
-
-        @Override
-        public void write(int b) throws IOException {
-            super.write(b);
-            if (b == 10) {
-                log();
-            } else {
-                buffer.write(b);
-            }
-        }
-
-        @Override
-        public void write(byte[] buf) throws IOException {
-            write(buf, 0, buf.length);
-        }
-
-        @Override
-        public void write(byte[] buf, int offs, int len) throws IOException {
-            for (int i = 0; i < len; i++) {
-                write(buf[offs+i]);
-            }
-        }
-
-        @Override
-        public void close() throws IOException {
-            super.close();
-            if (buffer.size() > 0) {
-                log();
-            }
-        }
-
-        private void log() {
-            String log = new String(buffer.toByteArray());
-            buffer.reset();
-            System.out.println("[" + name + "] " + log);
-        }
-    }
-
 }
\ No newline at end of file
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/AbstractCliTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/AbstractCliTest.java
new file mode 100644
index 0000000..8df18ae
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/AbstractCliTest.java
@@ -0,0 +1,54 @@
+package org.keycloak.testsuite.cli;
+
+import org.junit.Assert;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.cli.exec.AbstractExec;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public abstract class AbstractCliTest extends AbstractKeycloakTest {
+
+
+    public void assertExitCodeAndStdOutSize(AbstractExec exe, int exitCode, int stdOutLineCount) {
+        assertExitCodeAndStreamSizes(exe, exitCode, stdOutLineCount, -1);
+    }
+
+    public void assertExitCodeAndStdErrSize(AbstractExec exe, int exitCode, int stdErrLineCount) {
+        assertExitCodeAndStreamSizes(exe, exitCode, -1, stdErrLineCount);
+    }
+
+    public void assertExitCodeAndStreamSizes(AbstractExec exe, int exitCode, int stdOutLineCount, int stdErrLineCount) {
+        Assert.assertEquals("exitCode == " + exitCode, exitCode, exe.exitCode());
+        if (stdOutLineCount != -1) {
+            try {
+                assertLineCount("stdout output", exe.stdoutLines(), stdOutLineCount);
+            } catch (Throwable e) {
+                throw new AssertionError("STDOUT: " + exe.stdoutString(), e);
+            }
+        }
+        if (stdErrLineCount != -1) {
+            try {
+                assertLineCount("stderr output", exe.stderrLines(), stdErrLineCount);
+            } catch (Throwable e) {
+                throw new AssertionError("STDERR: " + exe.stderrString(), e);
+            }
+        }
+    }
+
+    private void assertLineCount(String label, List<String> lines, int count) {
+        if (lines.size() == count) {
+            return;
+        }
+        // there is some kind of race condition in 'kcreg' that results in intermittent extra empty line
+        if (lines.size() == count + 1) {
+            if ("".equals(lines.get(lines.size()-1))) {
+                return;
+            }
+        }
+        Assert.assertTrue(label + " has " + lines.size() + " lines (expected: " + count + ")", lines.size() == count);
+    }
+
+}
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/AbstractAdmCliTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/AbstractAdmCliTest.java
new file mode 100644
index 0000000..d86ddf1
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/AbstractAdmCliTest.java
@@ -0,0 +1,387 @@
+package org.keycloak.testsuite.cli.admin;
+
+import org.junit.Assert;
+import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
+import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
+import org.keycloak.client.admin.cli.config.ConfigData;
+import org.keycloak.client.admin.cli.config.FileConfigHandler;
+import org.keycloak.client.admin.cli.config.RealmConfigData;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.cli.AbstractCliTest;
+import org.keycloak.testsuite.cli.KcAdmExec;
+import org.keycloak.testsuite.util.ClientBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
+import static org.keycloak.testsuite.cli.KcAdmExec.WORK_DIR;
+import static org.keycloak.testsuite.cli.KcAdmExec.execute;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public abstract class AbstractAdmCliTest extends AbstractCliTest {
+
+    protected String serverUrl = isAuthServerSSL() ?
+            "https://localhost:" + getAuthServerHttpsPort() + "/auth" :
+            "http://localhost:" + getAuthServerHttpPort() + "/auth";
+
+    static boolean runIntermittentlyFailingTests() {
+        return "true".equals(System.getProperty("test.intermittent"));
+    }
+
+    static boolean isAuthServerSSL() {
+        return "true".equals(System.getProperty("auth.server.ssl.required"));
+    }
+
+    static File getDefaultConfigFilePath() {
+        return new File(System.getProperty("user.home") + "/.keycloak/kcadm.config");
+    }
+
+    static int getAuthServerHttpsPort() {
+        try {
+            return Integer.valueOf(System.getProperty("auth.server.https.port"));
+        } catch (Exception e) {
+            throw new RuntimeException("System property 'auth.server.https.port' not set or invalid: '"
+                    + System.getProperty("auth.server.https.port") + "'");
+        }
+    }
+
+    static int getAuthServerHttpPort() {
+        try {
+            return Integer.valueOf(System.getProperty("auth.server.http.port"));
+        } catch (Exception e) {
+            throw new RuntimeException("System property 'auth.server.http.port' not set or invalid: '"
+                    + System.getProperty("auth.server.http.port") + "'");
+        }
+    }
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+
+        RealmRepresentation realmRepresentation = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
+        testRealms.add(realmRepresentation);
+
+        // create admin user account with permissions to manage clients
+        UserRepresentation admin = UserBuilder.create()
+                .username("user1")
+                .password("userpass")
+                .enabled(true)
+                .build();
+        HashMap<String, List<String>> clientRoles = new HashMap<>();
+        clientRoles.put("realm-management", Arrays.asList("realm-admin"));
+        admin.setClientRoles(clientRoles);
+        realmRepresentation.getUsers().add(admin);
+
+
+
+        // create client with service account to use Signed JWT credentials with
+        ClientRepresentation regClient = ClientBuilder.create()
+                .clientId("admin-cli-jwt")
+                .attribute(JWTClientAuthenticator.CERTIFICATE_ATTR, "MIICnTCCAYUCBgFXUhpRTTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdyZWctY2xpMB4XDTE2MDkyMjEzMzIxOFoXDTI2MDkyMjEzMzM1OFowEjEQMA4GA1UEAwwHcmVnLWNsaTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMHZn/0Bk1M9oKcTHxzn2cGvBWwO1m6OVLQ8LSVwNIf4ixfGkVIkhI5iEGYND+uD8ame54ZPClTVxMra3JldClLIG+L+ymnbT2vKIhEsVvCROs9PnYxbFALt1dXneLIio2uzF+d7/zQWlmeaWfNunSJT1aHNJDkGgDeUuQa25b0IMqsFjsN8Dg4ATkA97r3wKn4Tp3SE7sTM/B2pmra4atNxGeShVrgihqUiQ/PwDiDGwry64AsexkZnQsCR3bJWBAVUiHef3JWzTfWWN5bfCBG6Mnq1xw7YN+YpV1nR3CGmcKJuLe6aTe7Ps8hYejYiQA7Mp7ZQsoImsVFV5HDOlb0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAZl8XvLfKXTPYvq/QyHOg7EDlAdlV3HkmHP9SBAV4BccmHmorMkm5I6I21UA5mfju+0nhbEd0bm0kvJFxIfNU6lJyyVvQx3Gns37KYUOzIV/ocWZuOTBLp5tfIBYbBwfE/s1J4PhpA/3WhBY9JKiLvdJfxECGIgaLs2M0UsylW/7o04+18Od8j/m7crQc7fpe5gJB5m/+hxUDowIjG5CumffX9OHYGDvHBpaUl7QNSGgjP8Bn9ogmIMUBJ7XSYUcohKuk2Cnj6p+GlLuqHbOISUXLVjf0DxhCu6diVxvacKbgAZmyCIO1tGL/UVRxg9GOYdCiC9vHfPuZ8US+ZB0P9g==")
+                .authenticatorType(JWTClientAuthenticator.PROVIDER_ID)
+                .serviceAccount()
+                .build();
+
+        realmRepresentation.getClients().add(regClient);
+
+        // create service account for client reg-cli with permissions to manage clients
+        addServiceAccount(realmRepresentation, "admin-cli-jwt");
+
+
+
+        // create client to use with user account - enable direct grants
+        regClient = ClientBuilder.create()
+                .clientId("admin-cli-jwt-direct")
+                .attribute(JWTClientAuthenticator.CERTIFICATE_ATTR, "MIICnTCCAYUCBgFXUhpRTTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdyZWctY2xpMB4XDTE2MDkyMjEzMzIxOFoXDTI2MDkyMjEzMzM1OFowEjEQMA4GA1UEAwwHcmVnLWNsaTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMHZn/0Bk1M9oKcTHxzn2cGvBWwO1m6OVLQ8LSVwNIf4ixfGkVIkhI5iEGYND+uD8ame54ZPClTVxMra3JldClLIG+L+ymnbT2vKIhEsVvCROs9PnYxbFALt1dXneLIio2uzF+d7/zQWlmeaWfNunSJT1aHNJDkGgDeUuQa25b0IMqsFjsN8Dg4ATkA97r3wKn4Tp3SE7sTM/B2pmra4atNxGeShVrgihqUiQ/PwDiDGwry64AsexkZnQsCR3bJWBAVUiHef3JWzTfWWN5bfCBG6Mnq1xw7YN+YpV1nR3CGmcKJuLe6aTe7Ps8hYejYiQA7Mp7ZQsoImsVFV5HDOlb0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAZl8XvLfKXTPYvq/QyHOg7EDlAdlV3HkmHP9SBAV4BccmHmorMkm5I6I21UA5mfju+0nhbEd0bm0kvJFxIfNU6lJyyVvQx3Gns37KYUOzIV/ocWZuOTBLp5tfIBYbBwfE/s1J4PhpA/3WhBY9JKiLvdJfxECGIgaLs2M0UsylW/7o04+18Od8j/m7crQc7fpe5gJB5m/+hxUDowIjG5CumffX9OHYGDvHBpaUl7QNSGgjP8Bn9ogmIMUBJ7XSYUcohKuk2Cnj6p+GlLuqHbOISUXLVjf0DxhCu6diVxvacKbgAZmyCIO1tGL/UVRxg9GOYdCiC9vHfPuZ8US+ZB0P9g==")
+                .authenticatorType(JWTClientAuthenticator.PROVIDER_ID)
+                .directAccessGrants()
+                .build();
+
+        realmRepresentation.getClients().add(regClient);
+
+
+
+
+        // create client with service account to use client secret with
+        regClient = ClientBuilder.create()
+                .clientId("admin-cli-secret")
+                .secret("password")
+                .authenticatorType(ClientIdAndSecretAuthenticator.PROVIDER_ID)
+                .serviceAccount()
+                .build();
+
+        realmRepresentation.getClients().add(regClient);
+
+        // create service account for client reg-cli with permissions to manage clients
+        addServiceAccount(realmRepresentation, "admin-cli-secret");
+
+
+
+
+        // create client to use with user account - enable direct grants
+        regClient = ClientBuilder.create()
+                .clientId("admin-cli-secret-direct")
+                .secret("password")
+                .authenticatorType(ClientIdAndSecretAuthenticator.PROVIDER_ID)
+                .directAccessGrants()
+                .build();
+
+        realmRepresentation.getClients().add(regClient);
+
+    }
+
+    FileConfigHandler initCustomConfigFile() {
+        String filename = UUID.randomUUID().toString() + ".config";
+        File cfgFile = new File(WORK_DIR + "/" + filename);
+        FileConfigHandler handler = new FileConfigHandler();
+        handler.setConfigFile(cfgFile.getAbsolutePath());
+        return handler;
+    }
+
+    void assertFieldsEqualWithExclusions(ConfigData config1, ConfigData config2, String ... excluded) {
+
+        HashSet<String> exclusions = new HashSet<>(Arrays.asList(excluded));
+
+        if (!exclusions.contains("serverUrl")) {
+            Assert.assertEquals("serverUrl", config1.getServerUrl(), config2.getServerUrl());
+        }
+        if (!exclusions.contains("realm")) {
+            Assert.assertEquals("realm", config1.getRealm(), config2.getRealm());
+        }
+        if (!exclusions.contains("truststore")) {
+            Assert.assertEquals("truststore", config1.getTruststore(), config2.getTruststore());
+        }
+        if (!exclusions.contains("endpoints")) {
+            Map<String, Map<String, RealmConfigData>> endp1 = config1.getEndpoints();
+            Map<String, Map<String, RealmConfigData>> endp2 = config2.getEndpoints();
+
+            Iterator<Map.Entry<String, Map<String, RealmConfigData>>> it1 = endp1.entrySet().iterator();
+            Iterator<Map.Entry<String, Map<String, RealmConfigData>>> it2 = endp2.entrySet().iterator();
+
+            while (it1.hasNext()) {
+                Map.Entry<String, Map<String, RealmConfigData>> ent1 = it1.next();
+                Map.Entry<String, Map<String, RealmConfigData>> ent2 = it2.next();
+
+                String serverUrl = ent1.getKey();
+                String endpskey = "endpoints." + serverUrl;
+                if (!exclusions.contains(endpskey)) {
+                    Assert.assertEquals(endpskey, ent1.getKey(), ent2.getKey());
+
+                    Map<String, RealmConfigData> realms1 = ent1.getValue();
+                    Map<String, RealmConfigData> realms2 = ent2.getValue();
+
+                    Iterator<Map.Entry<String, RealmConfigData>> rit1 = realms1.entrySet().iterator();
+                    Iterator<Map.Entry<String, RealmConfigData>> rit2 = realms2.entrySet().iterator();
+
+                    while (rit1.hasNext()) {
+                        Map.Entry<String, RealmConfigData> rent1 = rit1.next();
+                        Map.Entry<String, RealmConfigData> rent2 = rit2.next();
+
+                        String realm = rent1.getKey();
+                        String rkey = endpskey + "." + realm;
+                        if (!exclusions.contains(endpskey)) {
+                            Assert.assertEquals(rkey, rent1.getKey(), rent2.getKey());
+
+                            RealmConfigData rdata1 = rent1.getValue();
+                            RealmConfigData rdata2 = rent2.getValue();
+
+                            assertFieldsEqualWithExclusions(serverUrl, realm, rdata1, rdata2, excluded);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    void assertFieldsEqualWithExclusions(String server, String realm, RealmConfigData data1, RealmConfigData data2, String ... excluded) {
+
+        HashSet<String> exclusions = new HashSet<>(Arrays.asList(excluded));
+
+        String pfix = "";
+        if (server != null || realm != null) {
+            pfix = "endpoints." + server + "." + realm + ".";
+        }
+
+        String ekey = pfix + "serverUrl";
+        if (!exclusions.contains(ekey)) {
+            Assert.assertEquals(ekey, data1.serverUrl(), data2.serverUrl());
+        }
+
+        ekey = pfix + "realm";
+        if (!exclusions.contains(ekey)) {
+            Assert.assertEquals(ekey, data1.realm(), data2.realm());
+        }
+
+        ekey = pfix + "clientId";
+        if (!exclusions.contains(ekey)) {
+            Assert.assertEquals(ekey, data1.getClientId(), data2.getClientId());
+        }
+
+        ekey = pfix + "token";
+        if (!exclusions.contains(ekey)) {
+            Assert.assertEquals(ekey, data1.getToken(), data2.getToken());
+        }
+
+        ekey = pfix + "refreshToken";
+        if (!exclusions.contains(ekey)) {
+            Assert.assertEquals(ekey, data1.getRefreshToken(), data2.getRefreshToken());
+        }
+
+        ekey = pfix + "expiresAt";
+        if (!exclusions.contains(ekey)) {
+            Assert.assertEquals(ekey, data1.getExpiresAt(), data2.getExpiresAt());
+        }
+
+        ekey = pfix + "refreshExpiresAt";
+        if (!exclusions.contains(ekey)) {
+            Assert.assertEquals(ekey, data1.getRefreshExpiresAt(), data2.getRefreshExpiresAt());
+        }
+
+        ekey = pfix + "secret";
+        if (!exclusions.contains(ekey)) {
+            Assert.assertEquals(ekey, data1.getSecret(), data2.getSecret());
+        }
+
+        ekey = pfix + "signingToken";
+        if (!exclusions.contains(ekey)) {
+            Assert.assertEquals(ekey, data1.getSigningToken(), data2.getSigningToken());
+        }
+
+        ekey = pfix + "sigExpiresAt";
+        if (!exclusions.contains(ekey)) {
+            Assert.assertEquals(ekey, data1.getSigExpiresAt(), data2.getSigExpiresAt());
+        }
+    }
+
+    void testCRUDWithOnTheFlyAuth(String serverUrl, String credentials, String extraOptions, String loginMessage) throws IOException {
+
+        File configFile = getDefaultConfigFilePath();
+        long lastModified = configFile.exists() ? configFile.lastModified() : 0;
+
+        // This test assumes it is the only user of any instance of on the system
+        KcAdmExec exe = execute("create clients --no-config --server " + serverUrl +
+                " --realm test " + credentials + " " + extraOptions + " -s clientId=test-client -o");
+
+        Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+        Assert.assertEquals("login message", loginMessage, exe.stderrLines().get(0));
+
+        ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+        Assert.assertEquals("clientId", "test-client", client.getClientId());
+
+        long lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
+        Assert.assertEquals("config file not modified", lastModified, lastModified2);
+
+
+
+
+        exe = execute("get clients/" + client.getId() + " --no-config --server " + serverUrl + " --realm test " + credentials + " " + extraOptions);
+
+        assertExitCodeAndStdErrSize(exe, 0, 1);
+
+        ClientRepresentation client2 = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+        Assert.assertEquals("clientId", "test-client", client2.getClientId());
+
+        lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
+        Assert.assertEquals("config file not modified", lastModified, lastModified2);
+
+
+
+
+        exe = execute("update clients/" + client.getId() + " --no-config --server " + serverUrl + " --realm test " +
+                credentials + " " + extraOptions + " -s enabled=false -o");
+
+        assertExitCodeAndStdErrSize(exe, 0, 1);
+
+        ClientRepresentation client4 = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+        Assert.assertEquals("clientId", "test-client", client4.getClientId());
+        Assert.assertFalse("enabled", client4.isEnabled());
+
+        lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
+        Assert.assertEquals("config file not modified", lastModified, lastModified2);
+
+
+
+
+        exe = execute("delete clients/" + client.getId() + " --no-config --server " + serverUrl + " --realm test " + credentials + " " + extraOptions);
+
+        assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+
+        lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
+        Assert.assertEquals("config file not modified", lastModified, lastModified2);
+
+
+
+
+        // subsequent delete should fail
+        exe = execute("delete clients/" + client.getId() + " --no-config --server " + serverUrl + " --realm test " + credentials + " " + extraOptions);
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+        String resourceUri = serverUrl + "/admin/realms/test/clients/" + client.getId();
+        Assert.assertEquals("error message", "Resource not found for url: " + resourceUri, exe.stderrLines().get(1));
+
+        lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
+        Assert.assertEquals("config file not modified", lastModified, lastModified2);
+    }
+
+    File initTempFile(String extension) throws IOException {
+        return initTempFile(extension, null);
+    }
+
+    File initTempFile(String extension, String content) throws IOException {
+        String filename = UUID.randomUUID().toString() + extension;
+        File file = new File(KcAdmExec.WORK_DIR + "/" + filename);
+        if (content != null) {
+            OutputStream os = new FileOutputStream(file);
+            os.write(content.getBytes(Charset.forName("iso_8859_1")));
+            os.close();
+        }
+        return file;
+    }
+
+    void addServiceAccount(RealmRepresentation realm, String clientId) {
+
+        UserRepresentation account = UserBuilder.create()
+                .username("service-account-" + clientId)
+                .enabled(true)
+                .serviceAccountId(clientId)
+                .build();
+
+        HashMap<String, List<String>> clientRoles = new HashMap<>();
+        clientRoles.put("realm-management", Arrays.asList("realm-admin"));
+
+        account.setClientRoles(clientRoles);
+
+        realm.getUsers().add(account);
+    }
+
+    void loginAsUser(File configFile, String server, String realm, String user, String password) {
+
+        KcAdmExec exe = KcAdmExec.execute("config credentials --server " + server + " --realm " + realm +
+                " --user " + user + " --password " + password + " --config " + configFile.getAbsolutePath());
+
+        Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
+
+        List<String> lines = exe.stdoutLines();
+        Assert.assertTrue("stdout output empty", lines.size() == 0);
+
+        lines = exe.stderrLines();
+        Assert.assertTrue("stderr output one line", lines.size() == 1);
+        Assert.assertEquals("stderr first line", "Logging into " + server + " as user " + user + " of realm " + realm, lines.get(0));
+    }
+}
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmCreateTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmCreateTest.java
new file mode 100644
index 0000000..2481b58
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmCreateTest.java
@@ -0,0 +1,131 @@
+package org.keycloak.testsuite.cli.admin;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.client.admin.cli.config.FileConfigHandler;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.testsuite.cli.KcAdmExec;
+import org.keycloak.testsuite.util.TempFileResource;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import static org.keycloak.testsuite.cli.KcAdmExec.execute;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KcAdmCreateTest extends AbstractAdmCliTest {
+
+    @Test
+    public void testCreateWithRealmOverride() throws IOException {
+
+        FileConfigHandler handler = initCustomConfigFile();
+
+        try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
+
+            // authenticate as a regular user against one realm
+            KcAdmExec exe = execute("config credentials -x --config '" + configFile.getName() +
+                    "' --server " + serverUrl + " --realm master --user admin --password admin");
+
+            assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+
+            exe = execute("create clients --config '" + configFile.getName() + "' --server " + serverUrl + " -r test -s clientId=my_first_client");
+
+            assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+        }
+    }
+
+
+    @Test
+    public void testCreateThoroughly() throws IOException {
+
+        FileConfigHandler handler = initCustomConfigFile();
+
+        try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
+
+            final String realm = "test";
+
+            // authenticate as a regular user against one realm
+            KcAdmExec exe = KcAdmExec.execute("config credentials -x --config '" + configFile.getName() +
+                    "' --server " + serverUrl + " --realm master --user admin --password admin");
+
+            assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+
+            // create configuration from file using stdin redirect ... output an object
+            String content = "{\n" +
+                    "        \"clientId\": \"my_client\",\n" +
+                    "        \"enabled\": true,\n" +
+                    "        \"redirectUris\": [\"http://localhost:8980/myapp/*\"],\n" +
+                    "        \"serviceAccountsEnabled\": true,\n" +
+                    "        \"name\": \"My Client App\",\n" +
+                    "        \"implicitFlowEnabled\": false,\n" +
+                    "        \"publicClient\": true,\n" +
+                    "        \"webOrigins\": [\"http://localhost:8980/myapp\"],\n" +
+                    "        \"consentRequired\": false,\n" +
+                    "        \"baseUrl\": \"http://localhost:8980/myapp\",\n" +
+                    "        \"bearerOnly\": true,\n" +
+                    "        \"standardFlowEnabled\": true\n" +
+                    "}";
+
+            try (TempFileResource tmpFile = new TempFileResource(initTempFile(".json", content))) {
+
+                exe = execute("create clients --config '" + configFile.getName() + "' -o -f - < '" + tmpFile.getName() + "'");
+
+                assertExitCodeAndStdErrSize(exe, 0, 0);
+
+                ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+                Assert.assertNotNull("id", client.getId());
+                Assert.assertEquals("clientId", "my_client", client.getClientId());
+                Assert.assertEquals("enabled", true, client.isEnabled());
+                Assert.assertEquals("redirectUris", Arrays.asList("http://localhost:8980/myapp/*"), client.getRedirectUris());
+                Assert.assertEquals("serviceAccountsEnabled", true, client.isServiceAccountsEnabled());
+                Assert.assertEquals("name", "My Client App", client.getName());
+                Assert.assertEquals("implicitFlowEnabled", false, client.isImplicitFlowEnabled());
+                Assert.assertEquals("publicClient", true, client.isPublicClient());
+                // note there is no server-side check if protocol is supported
+                Assert.assertEquals("webOrigins", Arrays.asList("http://localhost:8980/myapp"), client.getWebOrigins());
+                Assert.assertEquals("consentRequired", false, client.isConsentRequired());
+                Assert.assertEquals("baseUrl", "http://localhost:8980/myapp", client.getBaseUrl());
+                Assert.assertEquals("bearerOnly", true, client.isStandardFlowEnabled());
+                Assert.assertFalse("mappers not empty", client.getProtocolMappers().isEmpty());
+
+                // create configuration from file as a template and override clientId and other attributes ... output an object
+                exe = execute("create clients --config '" + configFile.getName() + "' -o -f '" + tmpFile.getName() +
+                        "' -s clientId=my_client2 -s enabled=false -s 'redirectUris=[\"http://localhost:8980/myapp2/*\"]'" +
+                        " -s 'name=My Client App II' -s 'webOrigins=[\"http://localhost:8980/myapp2\"]'" +
+                        " -s baseUrl=http://localhost:8980/myapp2 -s rootUrl=http://localhost:8980/myapp2");
+
+                assertExitCodeAndStdErrSize(exe, 0, 0);
+
+                ClientRepresentation client2 = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+                Assert.assertNotNull("id", client2.getId());
+                Assert.assertEquals("clientId", "my_client2", client2.getClientId());
+                Assert.assertEquals("enabled", false, client2.isEnabled());
+                Assert.assertEquals("redirectUris", Arrays.asList("http://localhost:8980/myapp2/*"), client2.getRedirectUris());
+                Assert.assertEquals("serviceAccountsEnabled", true, client2.isServiceAccountsEnabled());
+                Assert.assertEquals("name", "My Client App II", client2.getName());
+                Assert.assertEquals("implicitFlowEnabled", false, client2.isImplicitFlowEnabled());
+                Assert.assertEquals("publicClient", true, client2.isPublicClient());
+                Assert.assertEquals("webOrigins", Arrays.asList("http://localhost:8980/myapp2"), client2.getWebOrigins());
+                Assert.assertEquals("consentRequired", false, client2.isConsentRequired());
+                Assert.assertEquals("baseUrl", "http://localhost:8980/myapp2", client2.getBaseUrl());
+                Assert.assertEquals("rootUrl", "http://localhost:8980/myapp2", client2.getRootUrl());
+                Assert.assertEquals("bearerOnly", true, client2.isStandardFlowEnabled());
+                Assert.assertFalse("mappers not empty", client2.getProtocolMappers().isEmpty());
+            }
+
+            // simple create, output an id
+            exe = execute("create clients --config '" + configFile.getName() + "' -i -s clientId=my_client3");
+
+            assertExitCodeAndStreamSizes(exe, 0, 1, 0);
+
+            // simple create, default output
+            exe = execute("create clients --config '" + configFile.getName() + "' -s clientId=my_client4");
+
+            assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+            Assert.assertTrue("only id returned", exe.stderrLines().get(0).startsWith("Created new client with id '"));
+        }
+    }
+}
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTest.java
new file mode 100644
index 0000000..fe2caa4
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTest.java
@@ -0,0 +1,561 @@
+package org.keycloak.testsuite.cli.admin;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.client.admin.cli.config.ConfigData;
+import org.keycloak.client.admin.cli.config.FileConfigHandler;
+import org.keycloak.client.admin.cli.config.RealmConfigData;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.testsuite.cli.KcAdmExec;
+import org.keycloak.testsuite.util.TempFileResource;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.UUID;
+
+import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
+import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
+import static org.keycloak.testsuite.cli.KcAdmExec.execute;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KcAdmTest extends AbstractAdmCliTest {
+
+    @Test
+    public void testBadCommand() {
+        /*
+         *  Test most basic execution with non-existent command
+         */
+        KcAdmExec exe = execute("nonexistent");
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 1);
+        Assert.assertEquals("stderr first line", "Unknown command: nonexistent", exe.stderrLines().get(0));
+    }
+
+
+    @Test
+    public void testNoArgs() {
+        /*
+         *  Test (sub)commands without any arguments
+         */
+        KcAdmExec exe = KcAdmExec.execute("");
+
+        assertExitCodeAndStdErrSize(exe, 1, 0);
+
+        List<String> lines = exe.stdoutLines();
+        Assert.assertTrue("stdout output not empty", lines.size() > 0);
+        Assert.assertEquals("stdout first line", "Keycloak Admin CLI", lines.get(0));
+        Assert.assertEquals("stdout one but last line", "Use '" + KcAdmExec.CMD + " help <command>' for more information about a given command.", lines.get(lines.size() - 2));
+        Assert.assertEquals("stdout last line", "", lines.get(lines.size() - 1));
+
+
+        /*
+         * Test commands without arguments
+         */
+        exe = KcAdmExec.execute("config");
+        assertExitCodeAndStreamSizes(exe, 1, 0, 1);
+        Assert.assertEquals("error message",
+                "Sub-command required by '" + CMD + " config' - one of: 'credentials', 'truststore'",
+                exe.stderrLines().get(0));
+
+        exe = KcAdmExec.execute("config credentials");
+        assertExitCodeAndStdErrSize(exe, 1, 0);
+        Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
+        Assert.assertEquals("help message", "Usage: " + CMD + " config credentials --server SERVER_URL --realm REALM --user USER [--password PASSWORD] [ARGUMENTS]", exe.stdoutLines().get(0));
+
+        exe = KcAdmExec.execute("config truststore");
+        assertExitCodeAndStdErrSize(exe, 1, 0);
+        Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
+        Assert.assertEquals("help message", "Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWORD] [ARGUMENTS]", exe.stdoutLines().get(0));
+
+        exe = KcAdmExec.execute("create");
+        assertExitCodeAndStdErrSize(exe, 1, 0);
+        Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
+        Assert.assertEquals("help message", "Usage: " + CMD + " create ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
+        //Assert.assertEquals("error message", "No file nor attribute values specified", exe.stderrLines().get(0));
+
+        exe = KcAdmExec.execute("get");
+        assertExitCodeAndStdErrSize(exe, 1, 0);
+        Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
+        Assert.assertEquals("help message", "Usage: " + CMD + " get ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
+        //Assert.assertEquals("error message", "CLIENT not specified", exe.stderrLines().get(0));
+
+        exe = KcAdmExec.execute("update");
+        assertExitCodeAndStdErrSize(exe, 1, 0);
+        Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
+        Assert.assertEquals("help message", "Usage: " + CMD + " update ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
+        //Assert.assertEquals("error message", "No file nor attribute values specified", exe.stderrLines().get(0));
+
+        exe = KcAdmExec.execute("delete");
+        assertExitCodeAndStdErrSize(exe, 1, 0);
+        Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
+        Assert.assertEquals("help message", "Usage: " + CMD + " delete ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
+        //Assert.assertEquals("error message", "CLIENT not specified", exe.stderrLines().get(0));
+
+        //exe = KcAdmExec.execute("get-roles");
+        //assertExitCodeAndStdErrSize(exe, 0, 0);
+        //try {
+        //    JsonNode node = JsonSerialization.readValue(exe.stdout(), JsonNode.class);
+        //    Assert.assertTrue("is JSON array", node.isArray());
+        //} catch (IOException e) {
+        //    throw new AssertionError("Response should be a JSON array", e);
+        //}
+
+        //Assert.assertTrue("JSON message returned", exe.stdoutLines().size() > 10);
+        //Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
+        //Assert.assertEquals("help message", "Usage: " + CMD + " get-roles [--cclientid CLIENT_ID | --cid ID] [ARGUMENTS]", exe.stdoutLines().get(0));
+
+        exe = KcAdmExec.execute("add-roles");
+        assertExitCodeAndStdErrSize(exe, 1, 0);
+        Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
+        Assert.assertEquals("help message", "Usage: " + CMD + " add-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]", exe.stdoutLines().get(0));
+        //Assert.assertEquals("error message", "CLIENT not specified", exe.stderrLines().get(0));
+
+        exe = KcAdmExec.execute("remove-roles");
+        assertExitCodeAndStdErrSize(exe, 1, 0);
+        Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
+        Assert.assertEquals("help message", "Usage: " + CMD + " remove-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]", exe.stdoutLines().get(0));
+
+        exe = KcAdmExec.execute("set-password");
+        assertExitCodeAndStdErrSize(exe, 1, 0);
+        Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
+        Assert.assertEquals("help message", "Usage: " + CMD + " set-password (--username USERNAME | --userid ID) [--password PASSWORD] [ARGUMENTS]", exe.stdoutLines().get(0));
+        //Assert.assertEquals("error message", "CLIENT not specified", exe.stderrLines().get(0));
+
+        exe = KcAdmExec.execute("help");
+        assertExitCodeAndStdErrSize(exe, 0, 0);
+        lines = exe.stdoutLines();
+        Assert.assertTrue("stdout output not empty", lines.size() > 0);
+        Assert.assertEquals("stdout first line", "Keycloak Admin CLI", lines.get(0));
+        Assert.assertEquals("stdout one but last line", "Use '" + KcAdmExec.CMD + " help <command>' for more information about a given command.", lines.get(lines.size() - 2));
+        Assert.assertEquals("stdout last line", "", lines.get(lines.size() - 1));
+    }
+
+    @Test
+    public void testHelpGlobalOption() {
+        /*
+         *  Test --help for all commands
+         */
+        KcAdmExec exe = KcAdmExec.execute("--help");
+        assertExitCodeAndStdErrSize(exe, 0, 0);
+        Assert.assertEquals("stdout first line", "Keycloak Admin CLI", exe.stdoutLines().get(0));
+
+        exe = KcAdmExec.execute("create --help");
+        assertExitCodeAndStdErrSize(exe, 0, 0);
+        Assert.assertEquals("stdout first line", "Usage: " + CMD + " create ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
+
+        exe = KcAdmExec.execute("get --help");
+        assertExitCodeAndStdErrSize(exe, 0, 0);
+        Assert.assertEquals("stdout first line", "Usage: " + CMD + " get ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
+
+        exe = KcAdmExec.execute("update --help");
+        assertExitCodeAndStdErrSize(exe, 0, 0);
+        Assert.assertEquals("stdout first line", "Usage: " + CMD + " update ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
+
+        exe = KcAdmExec.execute("delete --help");
+        assertExitCodeAndStdErrSize(exe, 0, 0);
+        Assert.assertEquals("stdout first line", "Usage: " + CMD + " delete ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
+
+        exe = KcAdmExec.execute("get-roles --help");
+        assertExitCodeAndStdErrSize(exe, 0, 0);
+        Assert.assertEquals("stdout first line", "Usage: " + CMD + " get-roles [--cclientid CLIENT_ID | --cid ID] [ARGUMENTS]", exe.stdoutLines().get(0));
+
+        exe = KcAdmExec.execute("add-roles --help");
+        assertExitCodeAndStdErrSize(exe, 0, 0);
+        Assert.assertEquals("stdout first line", "Usage: " + CMD + " add-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]", exe.stdoutLines().get(0));
+
+        exe = KcAdmExec.execute("remove-roles --help");
+        assertExitCodeAndStdErrSize(exe, 0, 0);
+        Assert.assertEquals("stdout first line", "Usage: " + CMD + " remove-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]", exe.stdoutLines().get(0));
+
+        exe = KcAdmExec.execute("set-password --help");
+        assertExitCodeAndStdErrSize(exe, 0, 0);
+        Assert.assertEquals("stdout first line", "Usage: " + CMD + " set-password (--username USERNAME | --userid ID) [--password PASSWORD] [ARGUMENTS]", exe.stdoutLines().get(0));
+
+        exe = KcAdmExec.execute("config --help");
+        assertExitCodeAndStdErrSize(exe, 0, 0);
+        Assert.assertEquals("stdout first line", "Usage: " + CMD + " config SUB_COMMAND [ARGUMENTS]", exe.stdoutLines().get(0));
+
+        exe = KcAdmExec.execute("config credentials --help");
+        assertExitCodeAndStdErrSize(exe, 0, 0);
+        Assert.assertEquals("stdout first line",
+                "Usage: " + CMD + " config credentials --server SERVER_URL --realm REALM --user USER [--password PASSWORD] [ARGUMENTS]",
+                exe.stdoutLines().get(0));
+
+        exe = KcAdmExec.execute("config truststore --help");
+        assertExitCodeAndStdErrSize(exe, 0, 0);
+        Assert.assertEquals("stdout first line",
+                "Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWORD] [ARGUMENTS]",
+                exe.stdoutLines().get(0));
+    }
+
+    @Test
+    public void testBadOptionInPlaceOfCommand() {
+        /*
+         *  Test most basic execution with non-existent option
+         */
+        KcAdmExec exe = KcAdmExec.execute("--nonexistent");
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 1);
+        Assert.assertEquals("stderr first line", "Unknown command: --nonexistent", exe.stderrLines().get(0));
+    }
+
+    @Test
+    public void testBadOption() {
+        /*
+         *  Test sub-command execution with non-existent option
+         */
+
+        KcAdmExec exe = KcAdmExec.execute("get users --nonexistent");
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+        Assert.assertEquals("stderr first line", "Invalid option: --nonexistent", exe.stderrLines().get(0));
+        Assert.assertEquals("try help", "Try '" + CMD + " help get' for more information", exe.stderrLines().get(1));
+
+        // set-password doesn't use @Arguments injection thus unsupported options are handled by Aesh
+        exe = KcAdmExec.execute("set-password --nonexistent");
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+        Assert.assertEquals("stderr first line", "Invalid option: --nonexistent", exe.stderrLines().get(0));
+        Assert.assertEquals("try help", "Try '" + CMD + " help set-password' for more information", exe.stderrLines().get(1));
+    }
+
+    @Test
+    public void testCredentialsServerAndRealmWithDefaultConfig() {
+        /*
+         *  Test without --server specified
+         */
+        KcAdmExec exe = KcAdmExec.execute("config credentials --server " + serverUrl + " --realm master");
+
+        assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+    }
+
+    @Test
+    public void testCredentialsNoServerWithDefaultConfig() {
+        /*
+         *  Test without --server specified
+         */
+        KcAdmExec exe = KcAdmExec.execute("config credentials --realm master --user admin --password admin");
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+        Assert.assertEquals("stderr first line", "Required option not specified: --server", exe.stderrLines().get(0));
+        Assert.assertEquals("try help", "Try '" + CMD + " help config credentials' for more information", exe.stderrLines().get(1));
+    }
+
+    @Test
+    public void testCredentialsNoRealmWithDefaultConfig() {
+        /*
+         *  Test without --server specified
+         */
+        KcAdmExec exe = KcAdmExec.execute("config credentials --server " + serverUrl + " --user admin --password admin");
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+        Assert.assertEquals("stderr first line", "Required option not specified: --realm", exe.stderrLines().get(0));
+        Assert.assertEquals("try help", "Try '" + CMD + " help config credentials' for more information", exe.stderrLines().get(1));
+    }
+
+    @Test
+    public void testUserLoginWithDefaultConfig() {
+        /*
+         *  Test most basic user login, using the default admin-cli as a client
+         */
+        KcAdmExec exe = KcAdmExec.execute("config credentials --server " + serverUrl + " --realm master --user admin --password admin");
+
+        assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+        Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as user admin of realm master", exe.stderrLines().get(0));
+    }
+
+    @Test
+    public void testUserLoginWithDefaultConfigInteractive() throws IOException {
+        /*
+         *  Test user login with interaction - provide user password after prompted for it
+         */
+
+        if (!runIntermittentlyFailingTests()) {
+            System.out.println("TEST SKIPPED - This test currently suffers from intermittent failures. Use -Dtest.intermittent=true to run it.");
+            return;
+        }
+
+        KcAdmExec exe = KcAdmExec.newBuilder()
+                .argsLine("config credentials --server " + serverUrl + " --realm master --user admin")
+                .executeAsync();
+
+        exe.waitForStdout("Enter password: ");
+        exe.sendToStdin("admin" + EOL);
+        exe.waitCompletion();
+
+        assertExitCodeAndStreamSizes(exe, 0, 1, 1);
+        Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as user admin of realm master", exe.stderrLines().get(0));
+
+
+        /*
+         *  Run the test one more time with stdin redirect
+         */
+        File tmpFile = new File(KcAdmExec.WORK_DIR + "/" + UUID.randomUUID().toString() + ".tmp");
+        try {
+            FileOutputStream tmpos = new FileOutputStream(tmpFile);
+            tmpos.write("admin".getBytes());
+            tmpos.write(EOL.getBytes());
+            tmpos.close();
+
+            exe = KcAdmExec.execute("config credentials --server " + serverUrl + " --realm master --user admin < '" + tmpFile.getName() + "'");
+
+            assertExitCodeAndStreamSizes(exe, 0, 1, 1);
+            Assert.assertTrue("Enter password prompt", exe.stdoutLines().get(0).startsWith("Enter password: "));
+            Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as user admin of realm master", exe.stderrLines().get(0));
+
+        } finally {
+            tmpFile.delete();
+        }
+    }
+
+    @Test
+    public void testClientLoginWithDefaultConfigInteractive() throws IOException {
+        /*
+         *  Test client login with interaction - login using service account, and provide a client secret after prompted for it
+         */
+
+        if (!runIntermittentlyFailingTests()) {
+            System.out.println("TEST SKIPPED - This test currently suffers from intermittent failures. Use -Dtest.intermittent=true to run it.");
+            return;
+        }
+
+        // use -Dtest.intermittent=true to run this test
+        KcAdmExec exe = KcAdmExec.newBuilder()
+                .argsLine("config credentials --server " + serverUrl + " --realm test --client admin-cli-secret")
+                .executeAsync();
+
+        exe.waitForStdout("Enter client secret: ");
+        exe.sendToStdin("password" + EOL);
+        exe.waitCompletion();
+
+        assertExitCodeAndStreamSizes(exe, 0, 1, 1);
+        Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as service-account-admin-cli-secret of realm test", exe.stderrLines().get(0));
+
+        /*
+         *  Run the test one more time with stdin redirect
+         */
+        File tmpFile = new File(KcAdmExec.WORK_DIR + "/" + UUID.randomUUID().toString() + ".tmp");
+        try {
+            FileOutputStream tmpos = new FileOutputStream(tmpFile);
+            tmpos.write("password".getBytes());
+            tmpos.write(EOL.getBytes());
+            tmpos.close();
+
+            exe = KcAdmExec.newBuilder()
+                    .argsLine("config credentials --server " + serverUrl + " --realm test --client admin-cli-secret < '" + tmpFile.getName() + "'")
+                    .execute();
+
+            assertExitCodeAndStreamSizes(exe, 0, 1, 1);
+            Assert.assertTrue("Enter client secret prompt", exe.stdoutLines().get(0).startsWith("Enter client secret: "));
+            Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as service-account-admin-cli-secret of realm test", exe.stderrLines().get(0));
+        } finally {
+            tmpFile.delete();
+        }
+    }
+
+    @Test
+    public void testUserLoginWithCustomConfig() {
+        /*
+         *  Test user login using a custom config file
+         */
+        FileConfigHandler handler = initCustomConfigFile();
+
+        File configFile = new File(handler.getConfigFile());
+        try {
+            KcAdmExec exe = KcAdmExec.execute("config credentials --server " + serverUrl + " --realm master" +
+                    " --user admin --password admin --config '" + configFile.getName() + "'");
+
+            assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+            Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as user admin of realm master", exe.stderrLines().get(0));
+
+            // make sure the config file exists, and has the right content
+            ConfigData config = handler.loadConfig();
+            Assert.assertEquals("serverUrl", serverUrl, config.getServerUrl());
+            Assert.assertEquals("realm", "master", config.getRealm());
+            RealmConfigData realmcfg = config.sessionRealmConfigData();
+            Assert.assertNotNull("realm config data no null", realmcfg);
+            Assert.assertEquals("realm cfg serverUrl", serverUrl, realmcfg.serverUrl());
+            Assert.assertEquals("realm cfg realm", "master", realmcfg.realm());
+            Assert.assertEquals("client id", "admin-cli", realmcfg.getClientId());
+            Assert.assertNotNull("token not null", realmcfg.getToken());
+            Assert.assertNotNull("refresh token not null", realmcfg.getRefreshToken());
+            Assert.assertNotNull("token expires not null", realmcfg.getExpiresAt());
+            Assert.assertNotNull("token expires in future", realmcfg.getExpiresAt() > System.currentTimeMillis());
+            Assert.assertNotNull("refresh token expires not null", realmcfg.getRefreshExpiresAt());
+            Assert.assertNotNull("refresh token expires in future", realmcfg.getRefreshExpiresAt() > System.currentTimeMillis());
+
+        } finally {
+            configFile.delete();
+        }
+    }
+
+    @Test
+    public void testCustomConfigLoginCreateDelete() throws IOException {
+        /*
+         *  Test user login, create, delete session using a custom config file
+         */
+
+        // prepare for loading a config file
+        FileConfigHandler handler = initCustomConfigFile();
+
+        try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
+
+            KcAdmExec exe = KcAdmExec.execute("config credentials --server " + serverUrl +
+                    " --realm master --user admin --password admin --config '" + configFile.getName() + "'");
+
+            assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+
+            // remember the state of config file
+            ConfigData config1 = handler.loadConfig();
+
+
+
+
+            exe = KcAdmExec.execute("create --config '" + configFile.getName() + "' clients -s clientId=test-client -o");
+
+            assertExitCodeAndStdErrSize(exe, 0, 0);
+
+            // check changes to config file
+            ConfigData config2 = handler.loadConfig();
+            assertFieldsEqualWithExclusions(config1, config2);
+
+
+            ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+            Assert.assertEquals("clientId", "test-client", client.getClientId());
+
+
+
+            exe = KcAdmExec.execute("delete clients/" + client.getId() + " --config '" + configFile.getName() + "'");
+
+            assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+            // check changes to config file
+            ConfigData config3 = handler.loadConfig();
+            assertFieldsEqualWithExclusions(config2, config3);
+        }
+    }
+
+    @Test
+    public void testCRUDWithOnTheFlyUserAuth() throws IOException {
+        /*
+         *  Test create, get, update, and delete using on-the-fly authentication - without using any config file.
+         *  Login is performed by each operation again, and again using username, and password.
+         */
+        testCRUDWithOnTheFlyAuth(serverUrl, "--user user1 --password userpass", "",
+                "Logging into " + serverUrl + " as user user1 of realm test");
+    }
+
+    @Test
+    public void testCRUDWithOnTheFlyUserAuthWithClientSecret() throws IOException {
+        /*
+         *  Test create, get, update, and delete using on-the-fly authentication - without using any config file.
+         *  Login is performed by each operation again, and again using username, password, and client secret.
+         */
+        // try client without direct grants enabled
+        KcAdmExec exe = KcAdmExec.execute("get clients --no-config --server " + serverUrl + " --realm test" +
+                " --user user1 --password userpass --client admin-cli-secret --secret password");
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+        Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
+        Assert.assertEquals("error message", "Client not allowed for direct access grants [invalid_grant]", exe.stderrLines().get(1));
+
+
+        // try wrong user password
+        exe = KcAdmExec.execute("get clients --no-config --server " + serverUrl + " --realm test" +
+                " --user user1 --password wrong --client admin-cli-secret-direct --secret password");
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+        Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
+        Assert.assertEquals("error message", "Invalid user credentials [invalid_grant]", exe.stderrLines().get(1));
+
+
+        // try wrong client secret
+        exe = KcAdmExec.execute("get clients --no-config --server " + serverUrl + " --realm test" +
+                " --user user1 --password userpass --client admin-cli-secret-direct --secret wrong");
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+        Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
+        Assert.assertEquals("error message", "Invalid client secret [unauthorized_client]", exe.stderrLines().get(1));
+
+
+        // try whole CRUD
+        testCRUDWithOnTheFlyAuth(serverUrl, "--user user1 --password userpass --client admin-cli-secret-direct --secret password", "",
+                "Logging into " + serverUrl + " as user user1 of realm test");
+    }
+
+    @Test
+    public void testCRUDWithOnTheFlyUserAuthWithSignedJwtClient() throws IOException {
+        /*
+         *  Test create, get, update, and delete using on-the-fly authentication - without using any config file.
+         *  Login is performed by each operation again, and again using username, password, and client JWT signature.
+         */
+        File keystore = new File(System.getProperty("user.dir") + "/src/test/resources/cli/kcadm/admin-cli-keystore.jks");
+        Assert.assertTrue("admin-cli-keystore.jks exists", keystore.isFile());
+
+        // try client without direct grants enabled
+        KcAdmExec exe = KcAdmExec.execute("get clients --no-config --server " + serverUrl + " --realm test" +
+                " --user user1 --password userpass --client admin-cli-jwt --keystore '" + keystore.getAbsolutePath() + "'" +
+                " --storepass storepass --keypass keypass --alias admin-cli");
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+        Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
+        Assert.assertEquals("error message", "Client not allowed for direct access grants [invalid_grant]", exe.stderrLines().get(1));
+
+
+        // try wrong user password
+        exe = KcAdmExec.execute("get clients --no-config --server " + serverUrl + " --realm test" +
+                " --user user1 --password wrong --client admin-cli-jwt-direct --keystore '" + keystore.getAbsolutePath() + "'" +
+                " --storepass storepass --keypass keypass --alias admin-cli");
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+        Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
+        Assert.assertEquals("error message", "Invalid user credentials [invalid_grant]", exe.stderrLines().get(1));
+
+
+        // try wrong storepass
+        exe = KcAdmExec.execute("get clients --no-config --server " + serverUrl + " --realm test" +
+                " --user user1 --password userpass --client admin-cli-jwt-direct --keystore '" + keystore.getAbsolutePath() + "'" +
+                " --storepass wrong --keypass keypass --alias admin-cli");
+
+        assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+        Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
+        Assert.assertEquals("error message", "Failed to load private key: Keystore was tampered with, or password was incorrect", exe.stderrLines().get(1));
+
+
+        // try whole CRUD
+        testCRUDWithOnTheFlyAuth(serverUrl,
+                "--user user1 --password userpass  --client admin-cli-jwt-direct --keystore '" + keystore.getAbsolutePath() + "'" +
+                        " --storepass storepass --keypass keypass --alias admin-cli", "",
+                "Logging into " + serverUrl + " as user user1 of realm test");
+
+    }
+
+    @Test
+    public void testCRUDWithOnTheFlyServiceAccountWithClientSecret() throws IOException {
+        /*
+         *  Test create, get, update, and delete using on-the-fly authentication - without using any config file.
+         *  Login is performed by each operation again, and again using only client secret - service account is used.
+         */
+        testCRUDWithOnTheFlyAuth(serverUrl, "--client admin-cli-secret --secret password", "",
+                "Logging into " + serverUrl + " as service-account-admin-cli-secret of realm test");
+    }
+
+    @Test
+    public void testCRUDWithOnTheFlyServiceAccountWithSignedJwtClient() throws IOException {
+        /*
+         *  Test create, get, update, and delete using on-the-fly authentication - without using any config file.
+         *  Login is performed by each operation again, and again using only client JWT signature - service account is used.
+         */
+        File keystore = new File(System.getProperty("user.dir") + "/src/test/resources/cli/kcadm/admin-cli-keystore.jks");
+        Assert.assertTrue("admin-cli-keystore.jks exists", keystore.isFile());
+
+        testCRUDWithOnTheFlyAuth(serverUrl,
+                "--client admin-cli-jwt --keystore '" + keystore.getAbsolutePath() + "' --storepass storepass --keypass keypass --alias admin-cli", "",
+                "Logging into " + serverUrl + " as service-account-admin-cli-jwt of realm test");
+    }
+}
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTruststoreTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTruststoreTest.java
new file mode 100644
index 0000000..1346442
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTruststoreTest.java
@@ -0,0 +1,115 @@
+package org.keycloak.testsuite.cli.admin;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.client.admin.cli.config.ConfigData;
+import org.keycloak.client.admin.cli.config.FileConfigHandler;
+import org.keycloak.testsuite.cli.KcAdmExec;
+import org.keycloak.testsuite.util.TempFileResource;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_PATH;
+import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
+import static org.keycloak.testsuite.cli.KcAdmExec.CMD;
+import static org.keycloak.testsuite.cli.KcAdmExec.execute;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KcAdmTruststoreTest extends AbstractAdmCliTest {
+
+    @Test
+    public void testTruststore() throws IOException {
+
+        // only run this test if ssl protected keycloak server is available
+        if (!isAuthServerSSL()) {
+            System.out.println("TEST SKIPPED - This test requires HTTPS. Run with '-Pauth-server-wildfly -Dauth.server.ssl.required=true'");
+            return;
+        }
+
+        File truststore = new File("src/test/resources/keystore/keycloak.truststore");
+
+        FileConfigHandler handler = initCustomConfigFile();
+
+        try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
+
+            if (runIntermittentlyFailingTests()) {
+                // configure truststore
+                KcAdmExec exe = execute("config truststore --config '" + configFile.getName() + "' '" + truststore.getAbsolutePath() + "'");
+
+                assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+
+                // perform authentication against server - asks for password, then for truststore password
+                exe = KcAdmExec.newBuilder()
+                        .argsLine("config credentials --server " + serverUrl + " --realm test --user user1" +
+                                " --config '" + configFile.getName() + "'")
+                        .executeAsync();
+
+                exe.waitForStdout("Enter password: ");
+                exe.sendToStdin("userpass" + EOL);
+                exe.waitForStdout("Enter truststore password: ");
+                exe.sendToStdin("secret" + EOL);
+                exe.waitCompletion();
+
+                assertExitCodeAndStreamSizes(exe, 0, 2, 1);
+
+
+                // configure truststore with password
+                exe = execute("config truststore --config '" + configFile.getName() + "' --trustpass secret '" + truststore.getAbsolutePath() + "'");
+
+                assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+                // perform authentication against server - asks for password, then for truststore password
+                exe = KcAdmExec.newBuilder()
+                        .argsLine("config credentials --server " + serverUrl + " --realm test --user user1" +
+                                " --config '" + configFile.getName() + "'")
+                        .executeAsync();
+
+                exe.waitForStdout("Enter password: ");
+                exe.sendToStdin("userpass" + EOL);
+                exe.waitCompletion();
+
+                assertExitCodeAndStreamSizes(exe, 0, 1, 1);
+
+            } else {
+                System.out.println("TEST SKIPPED PARTIALLY - This test currently suffers from intermittent failures. Use -Dtest.intermittent=true to run it in full.");
+            }
+        }
+
+        // configure truststore with password
+        KcAdmExec exe = execute("config truststore --trustpass secret '" + truststore.getAbsolutePath() + "'");
+        assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+        // perform authentication against server - asks for password, then for truststore password
+        exe = execute("config credentials --server " + serverUrl + " --realm test --user user1 --password userpass");
+        assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+
+        exe = execute("config truststore --delete");
+        assertExitCodeAndStreamSizes(exe, 0, 0, 0);
+
+        exe = execute("config truststore --delete '" + truststore.getAbsolutePath() + "'");
+        assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+        Assert.assertEquals("incompatible", "Option --delete is mutually exclusive with specifying a TRUSTSTORE", exe.stderrLines().get(0));
+        Assert.assertEquals("try help", "Try '" + CMD + " help config truststore' for more information", exe.stderrLines().get(1));
+
+        exe = execute("config truststore --delete --trustpass secret");
+        assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+        Assert.assertEquals("no truststore error", "Options --trustpass and --delete are mutually exclusive", exe.stderrLines().get(0));
+        Assert.assertEquals("try help", "Try '" + CMD + " help config truststore' for more information", exe.stderrLines().get(1));
+
+        FileConfigHandler cfghandler = new FileConfigHandler();
+        cfghandler.setConfigFile(DEFAULT_CONFIG_FILE_PATH);
+        ConfigData config = cfghandler.loadConfig();
+        Assert.assertNull("truststore null", config.getTruststore());
+        Assert.assertNull("trustpass null", config.getTrustpass());
+
+
+        // perform no-config CRUD test against ssl protected endpoint
+        testCRUDWithOnTheFlyAuth(serverUrl,
+                "--user user1 --password userpass", " --truststore '" + truststore.getAbsolutePath() + "' --trustpass secret",
+                "Logging into " + serverUrl + " as user user1 of realm test");
+    }
+}
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmUpdateTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmUpdateTest.java
new file mode 100644
index 0000000..6e75f59
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmUpdateTest.java
@@ -0,0 +1,130 @@
+package org.keycloak.testsuite.cli.admin;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.client.admin.cli.config.FileConfigHandler;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.testsuite.cli.KcAdmExec;
+import org.keycloak.testsuite.util.TempFileResource;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+import static org.keycloak.testsuite.cli.KcAdmExec.CMD;
+import static org.keycloak.testsuite.cli.KcAdmExec.execute;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KcAdmUpdateTest extends AbstractAdmCliTest {
+
+    @Test
+    public void testUpdateThoroughly() throws IOException {
+
+        FileConfigHandler handler = initCustomConfigFile();
+
+        try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
+
+            final String realm = "test";
+
+            loginAsUser(configFile.getFile(), serverUrl, realm, "user1", "userpass");
+
+
+            // create an object so we can update it
+            KcAdmExec exe = execute("create clients --config '" + configFile.getName() + "' -o -s clientId=my_client");
+
+            assertExitCodeAndStdErrSize(exe, 0, 0);
+
+            ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+
+            Assert.assertEquals("enabled", true, client.isEnabled());
+            Assert.assertEquals("publicClient", false, client.isPublicClient());
+            Assert.assertEquals("bearerOnly", false, client.isBearerOnly());
+            Assert.assertTrue("redirectUris is empty", client.getRedirectUris().isEmpty());
+
+
+            // Merge update
+            exe = execute("update clients/" + client.getId() + " --config '" + configFile.getName() + "' -o " +
+                    " -s enabled=false -s 'redirectUris=[\"http://localhost:8980/myapp/*\"]'");
+
+            assertExitCodeAndStdErrSize(exe, 0, 0);
+
+            client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+            Assert.assertEquals("enabled", false, client.isEnabled());
+            Assert.assertEquals("redirectUris", Arrays.asList("http://localhost:8980/myapp/*"), client.getRedirectUris());
+
+
+
+            // Another merge update - test deleting an attribute, deleting a list item and adding a list item
+            exe = execute("update clients/" + client.getId() + " --config '" + configFile.getName() + "' -o -d redirectUris[0] -s webOrigins+=http://localhost:8980/myapp -s webOrigins+=http://localhost:8981/myapp -d webOrigins[0]");
+
+            assertExitCodeAndStdErrSize(exe, 0, 0);
+
+            client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+
+            Assert.assertTrue("redirectUris is empty", client.getRedirectUris().isEmpty());
+            Assert.assertEquals("webOrigins", Arrays.asList("http://localhost:8981/myapp"), client.getWebOrigins());
+
+
+
+            // Another merge update - test nested attributes and setting an attribute using json format
+            // TODO KEYCLOAK-3705 Updating protocolMapper config via client registration endpoint has no effect
+            /*
+            exe = execute("update my_client --config '" + configFile.getName() + "' -o -s 'protocolMappers[0].config.\"id.token.claim\"=false' " +
+                    "-s 'protocolMappers[4].config={\"single\": \"true\", \"attribute.nameformat\": \"Basic\", \"attribute.name\": \"Role\"}'");
+
+            assertExitCodeAndStdErrSize(exe, 0, 0);
+
+            client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+            Assert.assertEquals("protocolMapper[0].config.\"id.token.claim\"", "false", client.getProtocolMappers().get(0).getConfig().get("id.token.claim"));
+            Assert.assertEquals("protocolMappers[4].config.single", "true", client.getProtocolMappers().get(4).getConfig().get("single"));
+            Assert.assertEquals("protocolMappers[4].config.\"attribute.nameformat\"", "Basic", client.getProtocolMappers().get(4).getConfig().get("attribute.nameformat"));
+            Assert.assertEquals("protocolMappers[4].config.\"attribute.name\"", "Role", client.getProtocolMappers().get(4).getConfig().get("attribute.name"));
+            */
+
+            // update using oidc format
+
+
+            // check that using an invalid attribute key is not ignored
+            exe = execute("update clients/" + client.getId() + " --nonexisting --config '" + configFile.getName() + "'");
+
+            assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+            Assert.assertEquals("error message", "Invalid option: --nonexisting", exe.stderrLines().get(0));
+            Assert.assertEquals("try help", "Try '" + CMD + " help update' for more information", exe.stderrLines().get(1));
+
+
+            // test overwrite from file
+            exe = KcAdmExec.newBuilder()
+                    .argsLine("update clients/" + client.getId() + " --config '" + configFile.getName() +
+                            "' -o  -s clientId=my_client -s 'redirectUris=[\"http://localhost:8980/myapp/*\"]' -f -")
+                    .stdin(new ByteArrayInputStream("{ \"enabled\": false }".getBytes()))
+                    .execute();
+
+            assertExitCodeAndStdErrSize(exe, 0, 0);
+
+            client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+            // web origin is not sent to the server, thus it retains the current value
+            Assert.assertEquals("webOrigins", Arrays.asList("http://localhost:8981/myapp"), client.getWebOrigins());
+            Assert.assertFalse("enabled is false", client.isEnabled());
+            Assert.assertEquals("redirectUris", Arrays.asList("http://localhost:8980/myapp/*"), client.getRedirectUris());
+
+
+            // test using merge with file
+            exe = KcAdmExec.newBuilder()
+                    .argsLine("update clients/" + client.getId() + " --config '" + configFile.getName() +
+                            "' -o -s enabled=true -m -f -")
+                    .stdin(new ByteArrayInputStream("{ \"webOrigins\": [\"http://localhost:8980/myapp\"] }".getBytes()))
+                    .execute();
+
+            assertExitCodeAndStdErrSize(exe, 0, 0);
+
+
+            client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
+            Assert.assertEquals("webOrigins", Arrays.asList("http://localhost:8980/myapp"), client.getWebOrigins());
+            Assert.assertTrue("enabled is true", client.isEnabled());
+            Assert.assertEquals("redirectUris", Arrays.asList("http://localhost:8980/myapp/*"), client.getRedirectUris());
+        }
+    }
+}
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegConfigTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegConfigTest.java
index b91296c..1bf7b38 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegConfigTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegConfigTest.java
@@ -15,7 +15,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
 /**
  * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
  */
-public class KcRegConfigTest extends AbstractCliTest {
+public class KcRegConfigTest extends AbstractRegCliTest {
 
     @Test
     public void testRegistrationToken() throws IOException {
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegCreateTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegCreateTest.java
index 5db5d86..d916a9b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegCreateTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegCreateTest.java
@@ -19,7 +19,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
 /**
  * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
  */
-public class KcRegCreateTest extends AbstractCliTest {
+public class KcRegCreateTest extends AbstractRegCliTest {
 
     @Test
     public void testCreateWithRealmOverride() throws IOException {
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTest.java
index 5647ec8..872ed32 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTest.java
@@ -23,7 +23,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
 /**
  * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
  */
-public class KcRegTest extends AbstractCliTest {
+public class KcRegTest extends AbstractRegCliTest {
 
     @Test
     public void testNoArgs() {
@@ -68,7 +68,7 @@ public class KcRegTest extends AbstractCliTest {
         exe = execute("config truststore");
         assertExitCodeAndStdErrSize(exe, 1, 0);
         Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
-        Assert.assertEquals("help message", "Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWOD] [ARGUMENTS]", exe.stdoutLines().get(0));
+        Assert.assertEquals("help message", "Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWORD] [ARGUMENTS]", exe.stdoutLines().get(0));
 
         exe = execute("create");
         assertExitCodeAndStdErrSize(exe, 1, 0);
@@ -172,7 +172,7 @@ public class KcRegTest extends AbstractCliTest {
         exe = execute("config truststore --help");
         assertExitCodeAndStdErrSize(exe, 0, 0);
         Assert.assertEquals("stdout first line",
-                "Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWOD] [ARGUMENTS]",
+                "Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWORD] [ARGUMENTS]",
                 exe.stdoutLines().get(0));
 
     }
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTruststoreTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTruststoreTest.java
index e3f7729..34c9584 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTruststoreTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTruststoreTest.java
@@ -18,7 +18,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
 /**
  * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
  */
-public class KcRegTruststoreTest extends AbstractCliTest {
+public class KcRegTruststoreTest extends AbstractRegCliTest {
 
     @Test
     public void testTruststore() throws IOException {
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTest.java
index 79e6707..419d754 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTest.java
@@ -18,7 +18,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
 /**
  * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
  */
-public class KcRegUpdateTest extends AbstractCliTest {
+public class KcRegUpdateTest extends AbstractRegCliTest {
 
 
     @Test
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTokenTest.java
index b2aebf7..a4b6c2c 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTokenTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTokenTest.java
@@ -18,7 +18,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
 /**
  * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
  */
-public class KcRegUpdateTokenTest extends AbstractCliTest {
+public class KcRegUpdateTokenTest extends AbstractRegCliTest {
 
     @Test
     public void testUpdateToken() throws IOException {
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/cli/kcadm/admin-cli-keystore.jks b/testsuite/integration-arquillian/tests/base/src/test/resources/cli/kcadm/admin-cli-keystore.jks
new file mode 100644
index 0000000..5c789f3
Binary files /dev/null and b/testsuite/integration-arquillian/tests/base/src/test/resources/cli/kcadm/admin-cli-keystore.jks differ