Merge pull request #5123 from patriot1burke/kcadm-token
KEYCLOAK-7044 KEYCLOAK-7046
This commit is contained in:
commit
ffd9d957f4
18 changed files with 135 additions and 31 deletions
|
@ -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 "$@"
|
||||
|
||||
|
||||
# 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 "$@"
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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, "",
|
||||
"");
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue