keycloak-aplcache

Changes

Details

diff --git a/integration/client-cli/admin-cli/src/main/bin/kcadm.sh b/integration/client-cli/admin-cli/src/main/bin/kcadm.sh
index 26df7e1..0c905b8 100755
--- a/integration/client-cli/admin-cli/src/main/bin/kcadm.sh
+++ b/integration/client-cli/admin-cli/src/main/bin/kcadm.sh
@@ -20,4 +20,14 @@ if [ "x$RESOLVED_NAME" = "x" ]; then
 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
+
+
+# Uncomment out these lines if you are integrating with `kcinit`
+#if [ "$1" = "config" ]; then
+#    java $KC_OPTS -cp $DIRNAME/client/keycloak-admin-cli-${project.version}.jar org.keycloak.client.admin.cli.KcAdmMain "$@"
+#else
+#    java $KC_OPTS -cp $DIRNAME/client/keycloak-admin-cli-${project.version}.jar org.keycloak.client.admin.cli.KcAdmMain "$@" --noconfig --token $(kcinit token admin-cli) --server $(kcinit show server)
+#fi
+# Remove the next line if you have enabled kcinit
+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/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
index d8d6b56..5c18ccb 100644
--- 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
@@ -89,6 +89,9 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
     @Option(name = "trustpass", description = "Truststore password (prompted for if not specified and --truststore is used)")
     String trustPass;
 
+    @Option(name = "token", description = "Token to use for invocations.  With this option set, every other authentication option is ignored")
+    String externalToken;
+
 
     protected void initFromParent(AbstractAuthOptionsCmd parent) {
 
@@ -108,6 +111,7 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
         alias = parent.alias;
         trustStore = parent.trustStore;
         trustPass = parent.trustPass;
+        externalToken = parent.externalToken;
     }
 
     protected void applyDefaultOptionValues() {
@@ -117,7 +121,7 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
     }
 
     protected boolean noOptions() {
-        return server == null && realm == null && clientId == null && secret == null &&
+        return externalToken == null && 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);
@@ -215,8 +219,8 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
     }
 
     protected boolean requiresLogin() {
-        return user != null || password != null || secret != null || keystore != null
-                || keyPass != null || storePass != null || alias != null;
+        return externalToken == null && (user != null || password != null || secret != null || keystore != null
+                || keyPass != null || storePass != null || alias != null);
     }
 
     protected ConfigData copyWithServerInfo(ConfigData config) {
@@ -229,6 +233,9 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
         if (realm != null) {
             result.setRealm(realm);
         }
+        if (externalToken != null) {
+            result.setExternalToken(externalToken);
+        }
 
         checkServerInfo(result);
         return result;
@@ -241,6 +248,9 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
             data.setRealm(realm);
         if (trustStore != null)
             data.setTruststore(trustStore);
+        if (externalToken != null) {
+            data.setExternalToken(externalToken);
+        }
 
         RealmConfigData rdata = data.sessionRealmConfigData();
         if (clientId != null)
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AddRolesCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AddRolesCmd.java
index 549a6c7..fccf5e8 100644
--- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AddRolesCmd.java
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AddRolesCmd.java
@@ -339,6 +339,7 @@ public class AddRolesCmd extends AbstractAuthOptionsCmd {
         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("    --no-config           Don't use config file - no authentication info is loaded or saved");
+        out.println("    --token               Token to use to invoke on Keycloak.  Other credential may be ignored if this flag is set.");
         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");
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/CreateCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/CreateCmd.java
index 6bdc7ec..c45f0da 100644
--- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/CreateCmd.java
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/CreateCmd.java
@@ -100,6 +100,7 @@ public class CreateCmd extends AbstractRequestCmd {
         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("    --no-config           Don't use config file - no authentication info is loaded or saved");
+        out.println("    --token               Token to use to invoke on Keycloak.  Other credential may be ignored if this flag is set.");
         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");
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/DeleteCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/DeleteCmd.java
index 1b43489..d9e2b95 100644
--- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/DeleteCmd.java
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/DeleteCmd.java
@@ -67,6 +67,7 @@ public class DeleteCmd extends CreateCmd {
         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("    --no-config           Don't use config file - no authentication info is loaded or saved");
+        out.println("    --token               Token to use to invoke on Keycloak.  Other credential may be ignored if this flag is set.");
         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");
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
index 4667db0..9c8cd78 100644
--- 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
@@ -99,6 +99,7 @@ public class GetCmd extends  AbstractRequestCmd {
         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("    --no-config           Don't use config file - no authentication info is loaded or saved");
+        out.println("    --token               Token to use to invoke on Keycloak.  Other credential may be ignored if this flag is set.");
         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");
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetRolesCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetRolesCmd.java
index 96f79c4..91637cb 100644
--- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetRolesCmd.java
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetRolesCmd.java
@@ -343,6 +343,7 @@ public class GetRolesCmd extends GetCmd {
         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("    --no-config           Don't use config file - no authentication info is loaded or saved");
+        out.println("    --token               Token to use to invoke on Keycloak.  Other credential may be ignored if this flag is set.");
         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");
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/RemoveRolesCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/RemoveRolesCmd.java
index 4f9c496..ffc600d 100644
--- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/RemoveRolesCmd.java
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/RemoveRolesCmd.java
@@ -339,6 +339,7 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd {
         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("    --no-config           Don't use config file - no authentication info is loaded or saved");
+        out.println("    --token               Token to use to invoke on Keycloak.  Other credential may be ignored if this flag is set.");
         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");
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
index c0317ad..cbf6758 100644
--- 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
@@ -151,6 +151,7 @@ public class SetPasswordCmd extends AbstractAuthOptionsCmd {
         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("    --no-config           Don't use config file - no authentication info is loaded or saved");
+        out.println("    --token               Token to use to invoke on Keycloak.  Other credential may be ignored if this flag is set.");
         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");
diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/UpdateCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/UpdateCmd.java
index 2c0f404..b6b17da 100644
--- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/UpdateCmd.java
+++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/UpdateCmd.java
@@ -108,6 +108,7 @@ public class UpdateCmd extends AbstractRequestCmd {
         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("    --no-config           Don't use config file - no authentication info is loaded or saved");
+        out.println("    --token               Token to use to invoke on Keycloak.  Other credential may be ignored if this flag is set.");
         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");
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
index e327335..9cdec9f 100644
--- 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
@@ -16,6 +16,7 @@
  */
 package org.keycloak.client.admin.cli.config;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import org.keycloak.util.JsonSerialization;
 
 import java.io.IOException;
@@ -27,6 +28,9 @@ import java.util.Map;
  */
 public class ConfigData {
 
+    @JsonIgnore
+    private String externalToken;
+
     private String serverUrl;
 
     private String realm;
@@ -46,6 +50,16 @@ public class ConfigData {
         this.serverUrl = serverUrl;
     }
 
+    @JsonIgnore
+    public String getExternalToken() {
+        return externalToken;
+    }
+
+    @JsonIgnore
+    public void setExternalToken(String externalToken) {
+        this.externalToken = externalToken;
+    }
+
     public String getRealm() {
         return realm;
     }
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
index ddfca0c..c237571 100644
--- 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
@@ -46,6 +46,9 @@ import static org.keycloak.client.admin.cli.util.HttpUtil.urlencode;
 public class AuthUtil {
 
     public static String ensureToken(ConfigData config) {
+        if (config.getExternalToken() != null) {
+            return config.getExternalToken();
+        }
 
         checkAuthInfo(config);
 
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
index 3699048..af4d60b 100644
--- 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
@@ -63,8 +63,11 @@ public class ConfigUtil {
     }
 
     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'.");
+        if (config.getServerUrl() == null) {
+            throw new RuntimeException("No server specified. Use --server, or '" + OsUtil.CMD + " config credentials or connection'.");
+        }
+        if (config.getRealm() == null && config.getExternalToken() == null) {
+            throw new RuntimeException("No realm or token specified. Use --realm, --token, or '" + OsUtil.CMD + " config credentials'.");
         }
     }
 
@@ -73,8 +76,8 @@ public class ConfigUtil {
     }
 
     public static boolean credentialsAvailable(ConfigData config) {
-        return config.getServerUrl() != null && config.getRealm() != null
-                && config.sessionRealmConfigData() != null && config.sessionRealmConfigData().getRefreshToken() != null;
+        return config.getServerUrl() != null && (config.getExternalToken() != null || (config.getRealm() != null
+                && config.sessionRealmConfigData() != null && config.sessionRealmConfigData().getRefreshToken() != null));
     }
 
     public static ConfigData loadConfig() {
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java
index 8994718..9b5e63c 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java
@@ -86,21 +86,19 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage
                 && !auth.getRealm().equals(new RealmManager(session).getKeycloakAdminstrationRealm())) {
             throw new ForbiddenException();
         }
-        if (auth.getClient().getClientId().equals(Constants.ADMIN_CLI_CLIENT_ID)
-                || auth.getClient().getClientId().equals(Constants.ADMIN_CONSOLE_CLIENT_ID)) {
-            this.identity = new UserModelIdentity(auth.getRealm(), auth.getUser());
-
-        } else {
-            this.identity = new KeycloakIdentity(auth.getToken(), session);
-        }
+        initIdentity(session, auth);
     }
     MgmtPermissions(KeycloakSession session, AdminAuth auth) {
         this.session = session;
         this.auth = auth;
         this.admin = auth.getUser();
         this.adminsRealm = auth.getRealm();
-        if (auth.getClient().getClientId().equals(Constants.ADMIN_CLI_CLIENT_ID)
-                || auth.getClient().getClientId().equals(Constants.ADMIN_CONSOLE_CLIENT_ID)) {
+        initIdentity(session, auth);
+    }
+
+    private void initIdentity(KeycloakSession session, AdminAuth auth) {
+        if (auth.getToken().hasAudience(Constants.ADMIN_CLI_CLIENT_ID)
+                || auth.getToken().hasAudience(Constants.ADMIN_CONSOLE_CLIENT_ID)) {
             this.identity = new UserModelIdentity(auth.getRealm(), auth.getUser());
 
         } else {
diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml
index 754c6da..7bffe00 100644
--- a/testsuite/integration-arquillian/tests/base/pom.xml
+++ b/testsuite/integration-arquillian/tests/base/pom.xml
@@ -264,7 +264,7 @@
                                 <package>github.com/keycloak/kcinit</package>
                             </packages>
                             <goPath>${project.build.directory}/gopath</goPath>
-                            <tag>0.4</tag>
+                            <tag>0.5</tag>
                         </configuration>
                     </execution>
                   </executions>
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java
index 1df6612..825e681 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java
@@ -23,10 +23,11 @@ import org.junit.Assert;
 import org.junit.Test;
 import org.keycloak.admin.client.Keycloak;
 import org.keycloak.authorization.model.Resource;
-import org.keycloak.models.ClientTemplateModel;
-import org.keycloak.models.GroupModel;
+import org.keycloak.client.admin.cli.util.ConfigUtil;
+import org.keycloak.models.*;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.representations.idm.ClientTemplateRepresentation;
+import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
 import org.keycloak.representations.idm.authorization.Logic;
 import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
 import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
@@ -34,20 +35,14 @@ import org.keycloak.services.resources.admin.permissions.AdminPermissionManageme
 import org.keycloak.services.resources.admin.permissions.AdminPermissions;
 import org.keycloak.authorization.model.Policy;
 import org.keycloak.authorization.model.ResourceServer;
-import org.keycloak.models.AdminRoles;
-import org.keycloak.models.ClientModel;
-import org.keycloak.models.Constants;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.RealmModel;
-import org.keycloak.models.RoleModel;
-import org.keycloak.models.UserCredentialModel;
-import org.keycloak.models.UserModel;
 import org.keycloak.representations.idm.ClientRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.representations.idm.RoleRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.representations.idm.authorization.DecisionStrategy;
 import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
+import org.keycloak.testsuite.auth.page.AuthRealm;
 import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
 import org.keycloak.testsuite.util.AdminClientUtil;
 
@@ -856,6 +851,52 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest {
         }
     }
 
+    /**
+     * KEYCLOAK-7406
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWithTokenExchange() throws Exception {
+        testingClient.server().run(session -> {
+            RealmModel realm = session.realms().getRealmByName("master");
+            ClientModel client = session.realms().getClientByClientId("kcinit", realm);
+            if (client != null) {
+                return;
+            }
+
+            ClientModel kcinit = realm.addClient("kcinit");
+            kcinit.setEnabled(true);
+            kcinit.addRedirectUri("http://localhost:*");
+            kcinit.setPublicClient(false);
+            kcinit.setSecret("password");
+            kcinit.setDirectAccessGrantsEnabled(true);
+
+            // permission for client to client exchange to "target" client
+            ClientModel adminCli = realm.getClientByClientId(ConfigUtil.DEFAULT_CLIENT);
+            AdminPermissionManagement management = AdminPermissions.management(session, realm);
+            management.clients().setPermissionsEnabled(adminCli, true);
+            ClientPolicyRepresentation clientRep = new ClientPolicyRepresentation();
+            clientRep.setName("to");
+            clientRep.addClient(kcinit.getId());
+            ResourceServer server = management.realmResourceServer();
+            Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientRep, server);
+            management.clients().exchangeToPermission(adminCli).addAssociatedPolicy(clientPolicy);
+        });
+
+        oauth.realm("master");
+        oauth.clientId("kcinit");
+        String token = oauth.doGrantAccessTokenRequest("password", "admin", "admin").getAccessToken();
+        Assert.assertNotNull(token);
+        String exchanged = oauth.doTokenExchange("master", token, "admin-cli", "kcinit", "password").getAccessToken();
+        Assert.assertNotNull(exchanged);
+
+        Keycloak client = Keycloak.getInstance(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth",
+                AuthRealm.MASTER, Constants.ADMIN_CLI_CLIENT_ID, exchanged);
+
+        Assert.assertNotNull(client.realm("master").roles().get("offline_access"));
+    }
+
 
 
 }
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
index d86ddf1..f7d15c4 100644
--- 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
@@ -320,7 +320,8 @@ public abstract class AbstractAdmCliTest extends AbstractCliTest {
 
         exe = execute("delete clients/" + client.getId() + " --no-config --server " + serverUrl + " --realm test " + credentials + " " + extraOptions);
 
-        assertExitCodeAndStreamSizes(exe, 0, 0, 1);
+        int linecountOffset = loginMessage.equals("") ? 1 : 0; // if there is no login, then there is one less stdErrLinecount
+        assertExitCodeAndStreamSizes(exe, 0, 0, 1 - linecountOffset);
 
         lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
         Assert.assertEquals("config file not modified", lastModified, lastModified2);
@@ -331,9 +332,9 @@ public abstract class AbstractAdmCliTest extends AbstractCliTest {
         // subsequent delete should fail
         exe = execute("delete clients/" + client.getId() + " --no-config --server " + serverUrl + " --realm test " + credentials + " " + extraOptions);
 
-        assertExitCodeAndStreamSizes(exe, 1, 0, 2);
+        assertExitCodeAndStreamSizes(exe, 1, 0, 2 - linecountOffset);
         String resourceUri = serverUrl + "/admin/realms/test/clients/" + client.getId();
-        Assert.assertEquals("error message", "Resource not found for url: " + resourceUri, exe.stderrLines().get(1));
+        Assert.assertEquals("error message", "Resource not found for url: " + resourceUri, exe.stderrLines().get(1 - linecountOffset));
 
         lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
         Assert.assertEquals("config file not modified", lastModified, lastModified2);
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
index 077e427..9c58600 100644
--- 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
@@ -570,4 +570,20 @@ public class KcAdmTest extends AbstractAdmCliTest {
                 "--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");
     }
+
+    @Test
+    public void testCRUDWithToken() throws Exception {
+        /*
+         *  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.
+         */
+        oauth.realm("master");
+        oauth.clientId("admin-cli");
+        String token = oauth.doGrantAccessTokenRequest("", "admin", "admin").getAccessToken();
+        testCRUDWithOnTheFlyAuth(serverUrl, " --token " + token, "",
+                "");
+
+    }
+
+
 }