task: refactor overlap between cli clients

also repackaging to more clearly delineate code roles

closes: #28329

Signed-off-by: Steve Hawkins <shawkins@redhat.com>
This commit is contained in:
Steve Hawkins 2024-03-29 11:57:13 -04:00 committed by Pedro Igor
parent 808926b63e
commit 0be34d64e7
121 changed files with 1569 additions and 3927 deletions

View file

@ -14,38 +14,46 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.admin.cli;
import com.fasterxml.jackson.core.JsonParseException; import org.keycloak.client.cli.common.AttributeOperation;
import com.fasterxml.jackson.databind.JsonNode; import org.keycloak.client.cli.util.AttributeException;
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 org.keycloak.util.JsonSerialization;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import static org.keycloak.client.admin.cli.util.IoUtil.readFileOrStdin; import com.fasterxml.jackson.core.JsonParseException;
import static org.keycloak.client.admin.cli.util.ReflectionUtil.setAttributes; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import static org.keycloak.client.admin.cli.ReflectionUtil.setAttributes;
import static org.keycloak.client.cli.util.IoUtil.readFileOrStdin;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/ */
public class ParseUtil { public class CmdStdinContext<T> {
public static String[] parseKeyVal(String keyval) { private T result;
// we expect = as a separator private String content;
int pos = keyval.indexOf("=");
if (pos <= 0) { public CmdStdinContext() {}
throw new IllegalArgumentException("Invalid key=value parameter: [" + keyval + "]");
public T getResult() {
return result;
} }
String [] parsed = new String[2]; public void setResult(T result) {
parsed[0] = keyval.substring(0, pos); this.result = result;
parsed[1] = keyval.substring(pos+1); }
return parsed; public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
} }
public static CmdStdinContext<JsonNode> parseFileOrStdin(String file) { public static CmdStdinContext<JsonNode> parseFileOrStdin(String file) {

View file

@ -17,45 +17,37 @@
package org.keycloak.client.admin.cli; package org.keycloak.client.admin.cli;
import org.keycloak.client.admin.cli.commands.KcAdmCmd; import org.keycloak.client.admin.cli.commands.KcAdmCmd;
import org.keycloak.client.admin.cli.util.ClassLoaderUtil; import org.keycloak.client.cli.common.CommandState;
import org.keycloak.client.admin.cli.util.OsUtil; import org.keycloak.client.cli.common.Globals;
import org.keycloak.common.crypto.CryptoIntegration; import org.keycloak.client.cli.util.OsUtil;
import java.io.PrintWriter;
import picocli.CommandLine;
import picocli.CommandLine.Model.CommandSpec;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/ */
public class KcAdmMain { public class KcAdmMain {
public static final String DEFAULT_CONFIG_FILE_PATH = System.getProperty("user.home") + "/.keycloak/kcadm.config";
public static final String DEFAULT_CONFIG_FILE_STRING = OsUtil.OS_ARCH.isWindows() ? "%HOMEDRIVE%%HOMEPATH%\\.keycloak\\kcadm.config" : "~/.keycloak/kcadm.config";
public static final String CMD = OsUtil.OS_ARCH.isWindows() ? "kcadm.bat" : "kcadm.sh";
public static final CommandState COMMAND_STATE = new CommandState() {
@Override
public String getCommand() {
return CMD;
}
@Override
public String getDefaultConfigFilePath() {
return DEFAULT_CONFIG_FILE_PATH;
}
};
public static void main(String [] args) { public static void main(String [] args) {
String libDir = System.getProperty("kc.lib.dir"); Globals.main(args, new KcAdmCmd(), CMD, DEFAULT_CONFIG_FILE_STRING);
if (libDir == null) {
throw new RuntimeException("System property kc.lib.dir needs to be set");
}
ClassLoader cl = ClassLoaderUtil.resolveClassLoader(libDir);
Thread.currentThread().setContextClassLoader(cl);
CryptoIntegration.init(cl);
CommandLine cli = createCommandLine();
int exitCode = cli.execute(args);
System.exit(exitCode);
}
public static CommandLine createCommandLine() {
CommandSpec spec = CommandSpec.forAnnotatedObject(new KcAdmCmd()).name(OsUtil.CMD);
CommandLine cmd = new CommandLine(spec);
cmd.setExecutionExceptionHandler(new ExecutionExceptionHandler());
cmd.setParameterExceptionHandler(new ShortErrorMessageHandler());
cmd.setErr(new PrintWriter(System.err, true));
return cmd;
} }
} }

View file

@ -14,22 +14,23 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.admin.cli;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode; import com.fasterxml.jackson.databind.node.TextNode;
import org.keycloak.client.admin.cli.common.AttributeKey;
import org.keycloak.client.admin.cli.common.AttributeOperation; import org.keycloak.client.cli.common.AttributeKey;
import org.keycloak.client.cli.common.AttributeOperation;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.SET; import static org.keycloak.client.cli.common.AttributeOperation.Type.SET;
import static org.keycloak.client.admin.cli.util.OutputUtil.MAPPER; import static org.keycloak.client.cli.util.OutputUtil.MAPPER;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>

View file

@ -16,252 +16,35 @@
*/ */
package org.keycloak.client.admin.cli.commands; package org.keycloak.client.admin.cli.commands;
import org.keycloak.OAuth2Constants; import org.keycloak.client.admin.cli.KcAdmMain;
import org.keycloak.client.admin.cli.config.ConfigData; import org.keycloak.client.cli.common.BaseAuthOptionsCmd;
import org.keycloak.client.admin.cli.config.ConfigHandler; import org.keycloak.client.cli.config.ConfigData;
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 picocli.CommandLine.Option; import picocli.CommandLine.Option;
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> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/ */
public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd { public abstract class AbstractAuthOptionsCmd extends BaseAuthOptionsCmd implements GlobalOptionsCmdHelper {
@Option(names = {"-a", "--admin-root"}, description = "URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/admin") @Option(names = {"-a", "--admin-root"}, description = "URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/admin")
String adminRestRoot; String adminRestRoot;
@Option(names = "--config", description = "Path to the config file (~/.keycloak/kcadm.config by default)")
String config;
@Option(names = "--no-config", description = "Don't use config file - no authentication info is loaded or saved")
boolean noconfig;
@Option(names = "--server", description = "Server endpoint url (e.g. 'http://localhost:8080')")
String server;
@Option(names = {"-r", "--target-realm"}, description = "Realm to target - when it's different than the realm we authenticate against") @Option(names = {"-r", "--target-realm"}, description = "Realm to target - when it's different than the realm we authenticate against")
String targetRealm; String targetRealm;
@Option(names = "--realm", description = "Realm name to authenticate against")
String realm;
@Option(names = "--client", description = "Realm name to authenticate against")
String clientId;
@Option(names = "--user", description = "Username to login with")
String user;
@Option(names = "--password", description = "Password to login with (prompted for if not specified and --user is used)")
String password;
@Option(names = "--secret", description = "Secret to authenticate the client (prompted for if no --user or --keystore is specified)")
String secret;
@Option(names = "--keystore", description = "Path to a keystore containing private key")
String keystore;
@Option(names = "--storepass", description = "Keystore password (prompted for if not specified and --keystore is used)")
String storePass;
@Option(names = "--keypass", description = "Key password (prompted for if not specified and --keystore is used without --storepass, \n otherwise defaults to keystore password)")
String keyPass;
@Option(names = "--alias", description = "Alias of the key inside a keystore (defaults to the value of ClientId)")
String alias;
@Option(names = "--truststore", description = "Path to a truststore")
String trustStore;
@Option(names = "--trustpass", description = "Truststore password (prompted for if not specified and --truststore is used)")
String trustPass;
@Option(names = "--insecure", description = "Turns off TLS validation")
boolean insecure;
@Option(names = "--token", description = "Token to use for invocations. With this option set, every other authentication option is ignored") @Option(names = "--token", description = "Token to use for invocations. With this option set, every other authentication option is ignored")
String externalToken; public void setToken(String token) {
this.externalToken = token;
protected void initFromParent(AbstractAuthOptionsCmd 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;
externalToken = parent.externalToken;
} }
protected void applyDefaultOptionValues() { public AbstractAuthOptionsCmd() {
if (clientId == null) { super(KcAdmMain.COMMAND_STATE);
clientId = DEFAULT_CLIENT;
} }
}
@Override
protected boolean nothingToDo() {
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;
}
protected String getTargetRealm(ConfigData config) { protected String getTargetRealm(ConfigData config) {
return targetRealm != null ? targetRealm : config.getRealm(); return targetRealm != null ? targetRealm : config.getRealm();
} }
@Override
protected void processOptions() {
if (config != null && noconfig) {
throw new IllegalArgumentException("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) {
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: ");
}
try {
HttpUtil.setTruststore(new File(truststore), pass);
} catch (Exception e) {
throw new RuntimeException("Failed to load truststore: " + truststore, e);
}
}
if (insecure) {
HttpUtil.setSkipCertificateValidation();
}
}
protected ConfigData ensureAuthInfo(ConfigData config) {
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();
// 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 externalToken == null && (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);
}
if (externalToken != null) {
result.setExternalToken(externalToken);
}
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);
if (externalToken != null) {
data.setExternalToken(externalToken);
}
RealmConfigData rdata = data.sessionRealmConfigData();
if (clientId != null)
rdata.setClientId(clientId);
if (secret != null)
rdata.setSecret(secret);
String grantTypeForAuthentication = user == null ? OAuth2Constants.CLIENT_CREDENTIALS : OAuth2Constants.PASSWORD;
rdata.setGrantTypeForAuthentication(grantTypeForAuthentication);
}
} }

View file

@ -1,136 +0,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.
*/
package org.keycloak.client.admin.cli.commands;
import org.keycloak.client.admin.cli.Globals;
import org.keycloak.client.admin.cli.util.FilterUtil;
import org.keycloak.client.admin.cli.util.ReturnFields;
import java.io.IOException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import picocli.CommandLine;
import picocli.CommandLine.Option;
import static org.keycloak.client.admin.cli.util.HttpUtil.normalize;
import static org.keycloak.client.admin.cli.util.IoUtil.printOut;
public abstract class AbstractGlobalOptionsCmd implements Runnable {
@Option(names = "--help",
description = "Print command specific help")
public void setHelp(boolean help) {
Globals.help = help;
}
@Option(names = "-x",
description = "Print full stack trace when exiting with error")
public void setDumpTrace(boolean dumpTrace) {
Globals.dumpTrace = dumpTrace;
}
protected void printHelpIfNeeded() {
if (Globals.help) {
printOut(help());
System.exit(CommandLine.ExitCode.OK);
} else if (nothingToDo()) {
printOut(help());
System.exit(CommandLine.ExitCode.USAGE);
}
}
protected boolean nothingToDo() {
return false;
}
protected String help() {
return KcAdmCmd.usage();
}
protected String composeAdminRoot(String server) {
return normalize(server) + "admin";
}
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);
}
}
@Override
public void run() {
printHelpIfNeeded();
checkUnsupportedOptions(getUnsupportedOptions());
processOptions();
process();
}
protected String[] getUnsupportedOptions() {
return new String[0];
}
protected void processOptions() {
}
protected void process() {
}
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 IllegalArgumentException("Unsupported option: " + name);
}
}
}
protected static String booleanOptionForCheck(boolean value) {
return value ? "true" : null;
}
}

View file

@ -17,18 +17,18 @@
package org.keycloak.client.admin.cli.commands; package org.keycloak.client.admin.cli.commands;
import org.apache.http.entity.ContentType; import org.apache.http.entity.ContentType;
import org.keycloak.client.admin.cli.common.AttributeOperation; import org.keycloak.client.admin.cli.CmdStdinContext;
import org.keycloak.client.admin.cli.common.CmdStdinContext; import org.keycloak.client.admin.cli.ReflectionUtil;
import org.keycloak.client.admin.cli.config.ConfigData; import org.keycloak.client.cli.common.AttributeOperation;
import org.keycloak.client.admin.cli.util.AccessibleBufferOutputStream; import org.keycloak.client.cli.config.ConfigData;
import org.keycloak.client.admin.cli.util.Header; import org.keycloak.client.cli.util.AccessibleBufferOutputStream;
import org.keycloak.client.admin.cli.util.Headers; import org.keycloak.client.cli.util.Header;
import org.keycloak.client.admin.cli.util.HeadersBody; import org.keycloak.client.cli.util.Headers;
import org.keycloak.client.admin.cli.util.HeadersBodyStatus; import org.keycloak.client.cli.util.HeadersBody;
import org.keycloak.client.admin.cli.util.HttpUtil; import org.keycloak.client.cli.util.HeadersBodyStatus;
import org.keycloak.client.admin.cli.util.OutputFormat; import org.keycloak.client.cli.util.HttpUtil;
import org.keycloak.client.admin.cli.util.ReflectionUtil; import org.keycloak.client.cli.util.OutputFormat;
import org.keycloak.client.admin.cli.util.ReturnFields; import org.keycloak.client.cli.util.ReturnFields;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@ -52,22 +52,19 @@ import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Option; import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters; import picocli.CommandLine.Parameters;
import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.DELETE; import static org.keycloak.client.cli.common.AttributeOperation.Type.DELETE;
import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.SET; import static org.keycloak.client.cli.common.AttributeOperation.Type.SET;
import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken; import static org.keycloak.client.cli.util.ConfigUtil.credentialsAvailable;
import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable; import static org.keycloak.client.cli.util.ConfigUtil.loadConfig;
import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig; import static org.keycloak.client.cli.util.HttpUtil.checkSuccess;
import static org.keycloak.client.admin.cli.util.HttpUtil.checkSuccess; import static org.keycloak.client.cli.util.HttpUtil.composeResourceUrl;
import static org.keycloak.client.admin.cli.util.HttpUtil.composeResourceUrl; import static org.keycloak.client.cli.util.HttpUtil.doGet;
import static org.keycloak.client.admin.cli.util.HttpUtil.doGet; import static org.keycloak.client.cli.util.IoUtil.copyStream;
import static org.keycloak.client.admin.cli.util.IoUtil.copyStream; import static org.keycloak.client.cli.util.IoUtil.printErr;
import static org.keycloak.client.admin.cli.util.IoUtil.printErr; import static org.keycloak.client.cli.util.IoUtil.printOut;
import static org.keycloak.client.admin.cli.util.IoUtil.printOut; import static org.keycloak.client.cli.util.OutputUtil.MAPPER;
import static org.keycloak.client.admin.cli.util.OutputUtil.MAPPER; import static org.keycloak.client.cli.util.OutputUtil.printAsCsv;
import static org.keycloak.client.admin.cli.util.OutputUtil.printAsCsv; import static org.keycloak.client.cli.util.ParseUtil.parseKeyVal;
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> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -208,7 +205,7 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd {
} }
} }
} else { } else {
ctx = parseFileOrStdin(file); ctx = CmdStdinContext.parseFileOrStdin(file);
} }
} else if (body != null) { } else if (body != null) {
content = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)); content = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
@ -280,7 +277,7 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd {
throw new RuntimeException("Can't set attributes on content of type other than application/json"); throw new RuntimeException("Can't set attributes on content of type other than application/json");
} }
ctx = mergeAttributes(ctx, MAPPER.createObjectNode(), attrs); ctx = CmdStdinContext.mergeAttributes(ctx, MAPPER.createObjectNode(), attrs);
} }
if (content == null && ctx.getContent() != null) { if (content == null && ctx.getContent() != null) {

View file

@ -21,12 +21,13 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
import picocli.CommandLine.Option; import picocli.CommandLine.Option;
import org.keycloak.client.admin.cli.config.ConfigData; import org.keycloak.client.admin.cli.KcAdmMain;
import org.keycloak.client.admin.cli.operations.ClientOperations; import org.keycloak.client.admin.cli.operations.ClientOperations;
import org.keycloak.client.admin.cli.operations.GroupOperations; import org.keycloak.client.admin.cli.operations.GroupOperations;
import org.keycloak.client.admin.cli.operations.RoleOperations; import org.keycloak.client.admin.cli.operations.RoleOperations;
import org.keycloak.client.admin.cli.operations.LocalSearch; import org.keycloak.client.admin.cli.operations.LocalSearch;
import org.keycloak.client.admin.cli.operations.UserOperations; import org.keycloak.client.admin.cli.operations.UserOperations;
import org.keycloak.client.cli.config.ConfigData;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
@ -35,12 +36,10 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken; import static org.keycloak.client.admin.cli.KcAdmMain.CMD;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.cli.util.ConfigUtil.credentialsAvailable;
import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable; import static org.keycloak.client.cli.util.ConfigUtil.loadConfig;
import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig; import static org.keycloak.client.cli.util.OsUtil.PROMPT;
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> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -290,7 +289,7 @@ public class AddRolesCmd extends AbstractAuthOptionsCmd {
out.println(); out.println();
out.println(" Global options:"); out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error"); 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(" --config Path to the config file (" + KcAdmMain.DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --no-config Don't use config file - no authentication info is loaded or saved"); 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(" --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(" --truststore PATH Path to a truststore containing trusted certificates");

View file

@ -21,7 +21,7 @@ import java.io.StringWriter;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
import static org.keycloak.client.admin.cli.util.OsUtil.CMD; import static org.keycloak.client.admin.cli.KcAdmMain.CMD;
/** /**

View file

@ -16,237 +16,20 @@
*/ */
package org.keycloak.client.admin.cli.commands; package org.keycloak.client.admin.cli.commands;
import org.keycloak.OAuth2Constants; import org.keycloak.client.admin.cli.KcAdmMain;
import org.keycloak.client.admin.cli.config.ConfigData; import org.keycloak.client.cli.common.BaseConfigCredentialsCmd;
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 picocli.CommandLine.Command; import picocli.CommandLine.Command;
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.OS_ARCH;
import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/ */
@Command(name = "credentials", description = "--server SERVER_URL --realm REALM [ARGUMENTS]") @Command(name = "credentials", description = "--server SERVER_URL --realm REALM [ARGUMENTS]")
public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd { public class ConfigCredentialsCmd extends BaseConfigCredentialsCmd {
private int sigLifetime = 600; public ConfigCredentialsCmd() {
super(KcAdmMain.COMMAND_STATE);
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
protected String[] getUnsupportedOptions() {
return new String[] {"--no-config", booleanOptionForCheck(noconfig)};
}
@Override
public void process() {
// 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();
String grantTypeForAuthentication = null;
if (user != null) {
grantTypeForAuthentication = OAuth2Constants.PASSWORD;
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: ");
}
// if secret was set to be read from stdin, then ask for it
if ("-".equals(secret) && keystore == null) {
secret = readSecret("Enter client secret: ");
}
} else if (keystore != null || secret != null || clientSet) {
grantTypeForAuthentication = OAuth2Constants.CLIENT_CREDENTIALS;
printErr("Logging into " + server + " as " + "service-account-" + clientId + " of realm " + realm);
if (keystore == null) {
if (secret == null) {
secret = readSecret("Enter client secret: ");
}
}
}
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: ");
keyPass = readSecret("Enter key password: ");
}
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;
}
setupTruststore(copyWithServerInfo(loadConfig()));
// 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, grantTypeForAuthentication);
}
@Override
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')");
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 --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 --realm master --user admin");
} else {
out.println(" " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080 --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 --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 --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 --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 --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();
}
} }

View file

@ -16,126 +16,19 @@
*/ */
package org.keycloak.client.admin.cli.commands; package org.keycloak.client.admin.cli.commands;
import java.io.File; import org.keycloak.client.admin.cli.KcAdmMain;
import java.io.PrintWriter; import org.keycloak.client.cli.common.BaseConfigTruststoreCmd;
import java.io.StringWriter;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
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.OS_ARCH;
import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/ */
@Command(name = "truststore", description = "PATH [ARGUMENTS]") @Command(name = "truststore", description = "PATH [ARGUMENTS]")
public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd { public class ConfigTruststoreCmd extends BaseConfigTruststoreCmd {
@Parameters(arity = "0..1") public ConfigTruststoreCmd() {
private String store; super(KcAdmMain.COMMAND_STATE);
@Option(names = {"-d", "--delete"}, description = "Remove truststore configuration")
private boolean delete;
@Override
protected boolean nothingToDo() {
return super.nothingToDo() && store == null && !delete;
} }
@Override
protected String[] getUnsupportedOptions() {
return new String[] {"--server", server,
"--realm", realm,
"--client", clientId,
"--user", user,
"--password", password,
"--secret", secret,
"--truststore", trustStore,
"--keystore", keystore,
"--keypass", keyPass,
"--alias", alias,
"--no-config", booleanOptionForCheck(noconfig)};
}
@Override
protected void process() {
String pass;
if (!delete) {
if (store == null) {
throw new IllegalArgumentException("No truststore specified");
}
if (!new File(store).isFile()) {
throw new RuntimeException("Truststore file not found: " + store);
}
if ("-".equals(trustPass)) {
trustPass = readSecret("Enter truststore password: ");
}
pass = trustPass;
} else {
if (store != 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");
}
pass = null;
}
saveMergeConfig(config -> {
config.setTruststore(store);
config.setTrustpass(pass);
});
}
@Override
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 --trustpass " + 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();
}
} }

View file

@ -16,16 +16,17 @@
*/ */
package org.keycloak.client.admin.cli.commands; package org.keycloak.client.admin.cli.commands;
import org.keycloak.client.admin.cli.KcAdmMain;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
import picocli.CommandLine.Option; import picocli.CommandLine.Option;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.admin.cli.KcAdmMain.CMD;
import static org.keycloak.client.admin.cli.util.OsUtil.CMD; import static org.keycloak.client.cli.util.OsUtil.OS_ARCH;
import static org.keycloak.client.admin.cli.util.OsUtil.OS_ARCH; import static org.keycloak.client.cli.util.OsUtil.PROMPT;
import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -91,7 +92,7 @@ public class CreateCmd extends AbstractRequestCmd {
out.println(); out.println();
out.println(" Global options:"); out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error"); 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(" --config Path to the config file (" + KcAdmMain.DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --no-config Don't use config file - no authentication info is loaded or saved"); 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(" --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(" --truststore PATH Path to a truststore containing trusted certificates");

View file

@ -16,14 +16,15 @@
*/ */
package org.keycloak.client.admin.cli.commands; package org.keycloak.client.admin.cli.commands;
import org.keycloak.client.admin.cli.KcAdmMain;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.admin.cli.KcAdmMain.CMD;
import static org.keycloak.client.admin.cli.util.OsUtil.CMD; import static org.keycloak.client.cli.util.OsUtil.PROMPT;
import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
/** /**
@ -55,7 +56,7 @@ public class DeleteCmd extends CreateCmd {
out.println(); out.println();
out.println(" Global options:"); out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error"); 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(" --config Path to the config file (" + KcAdmMain.DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --no-config Don't use config file - no authentication info is loaded or saved"); 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(" --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(" --truststore PATH Path to a truststore containing trusted certificates");

View file

@ -16,15 +16,16 @@
*/ */
package org.keycloak.client.admin.cli.commands; package org.keycloak.client.admin.cli.commands;
import org.keycloak.client.admin.cli.KcAdmMain;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
import picocli.CommandLine.Option; import picocli.CommandLine.Option;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.admin.cli.KcAdmMain.CMD;
import static org.keycloak.client.admin.cli.util.OsUtil.CMD; import static org.keycloak.client.cli.util.OsUtil.PROMPT;
import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -91,7 +92,7 @@ public class GetCmd extends AbstractRequestCmd {
out.println(); out.println();
out.println(" Global options:"); out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error"); 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(" --config Path to the config file (" + KcAdmMain.DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --no-config Don't use config file - no authentication info is loaded or saved"); 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(" --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(" --truststore PATH Path to a truststore containing trusted certificates");

View file

@ -16,11 +16,12 @@
*/ */
package org.keycloak.client.admin.cli.commands; package org.keycloak.client.admin.cli.commands;
import org.keycloak.client.admin.cli.config.ConfigData; import org.keycloak.client.admin.cli.KcAdmMain;
import org.keycloak.client.admin.cli.operations.ClientOperations; import org.keycloak.client.admin.cli.operations.ClientOperations;
import org.keycloak.client.admin.cli.operations.GroupOperations; import org.keycloak.client.admin.cli.operations.GroupOperations;
import org.keycloak.client.admin.cli.operations.RoleOperations; import org.keycloak.client.admin.cli.operations.RoleOperations;
import org.keycloak.client.admin.cli.operations.UserOperations; import org.keycloak.client.admin.cli.operations.UserOperations;
import org.keycloak.client.cli.config.ConfigData;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
@ -28,13 +29,11 @@ import java.io.StringWriter;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
import picocli.CommandLine.Option; import picocli.CommandLine.Option;
import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken; import static org.keycloak.client.admin.cli.KcAdmMain.CMD;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.cli.util.ConfigUtil.credentialsAvailable;
import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable; import static org.keycloak.client.cli.util.ConfigUtil.loadConfig;
import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig; import static org.keycloak.client.cli.util.HttpUtil.composeResourceUrl;
import static org.keycloak.client.admin.cli.util.HttpUtil.composeResourceUrl; import static org.keycloak.client.cli.util.OsUtil.PROMPT;
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> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -325,7 +324,7 @@ public class GetRolesCmd extends GetCmd {
out.println(); out.println();
out.println(" Global options:"); out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error"); 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(" --config Path to the config file (" + KcAdmMain.DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --no-config Don't use config file - no authentication info is loaded or saved"); 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(" --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(" --truststore PATH Path to a truststore containing trusted certificates");

View file

@ -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.commands;
import org.keycloak.client.cli.util.FilterUtil;
import org.keycloak.client.cli.util.ReturnFields;
import java.io.IOException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import static org.keycloak.client.cli.util.HttpUtil.normalize;
public interface GlobalOptionsCmdHelper {
default String composeAdminRoot(String server) {
return normalize(server) + "admin";
}
default String extractTypeNameFromUri(String resourceUrl) {
String type = extractLastComponentOfUri(resourceUrl);
if (type.endsWith("s")) {
type = type.substring(0, type.length()-1);
}
return type;
}
default 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);
}
default 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);
}
}
}

View file

@ -21,7 +21,7 @@ import java.util.List;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters; import picocli.CommandLine.Parameters;
import static org.keycloak.client.admin.cli.util.IoUtil.printOut; import static org.keycloak.client.cli.util.IoUtil.printOut;
@Command(name = "help", description = "This Help") @Command(name = "help", description = "This Help")
public class HelpCmd implements Runnable { public class HelpCmd implements Runnable {
@ -39,11 +39,11 @@ public class HelpCmd implements Runnable {
if (args.size() > 1) { if (args.size() > 1) {
switch (args.get(1)) { switch (args.get(1)) {
case "credentials": { case "credentials": {
printOut(ConfigCredentialsCmd.usage()); printOut(new ConfigCredentialsCmd().help());
break outer; break outer;
} }
case "truststore": { case "truststore": {
printOut(ConfigTruststoreCmd.usage()); printOut(new ConfigTruststoreCmd().help());
break outer; break outer;
} }
} }

View file

@ -16,14 +16,16 @@
*/ */
package org.keycloak.client.admin.cli.commands; package org.keycloak.client.admin.cli.commands;
import org.keycloak.client.admin.cli.KcAdmMain;
import org.keycloak.client.cli.common.BaseGlobalOptionsCmd;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.admin.cli.KcAdmMain.CMD;
import static org.keycloak.client.admin.cli.util.OsUtil.CMD; import static org.keycloak.client.cli.util.OsUtil.PROMPT;
import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
@Command(name = "kcadm", @Command(name = "kcadm",
header = { header = {
@ -47,13 +49,18 @@ subcommands = {
GetRolesCmd.class, GetRolesCmd.class,
SetPasswordCmd.class SetPasswordCmd.class
}) })
public class KcAdmCmd extends AbstractGlobalOptionsCmd { public class KcAdmCmd extends BaseGlobalOptionsCmd {
@Override @Override
protected boolean nothingToDo() { protected boolean nothingToDo() {
return true; return true;
} }
@Override
protected String help() {
return usage();
}
public static String usage() { public static String usage() {
StringWriter sb = new StringWriter(); StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb); PrintWriter out = new PrintWriter(sb);
@ -76,7 +83,7 @@ public class KcAdmCmd extends AbstractGlobalOptionsCmd {
out.println("Global options:"); out.println("Global options:");
out.println(" -x Print full stack trace when exiting with error"); out.println(" -x Print full stack trace when exiting with error");
out.println(" --help Print help for specific command"); out.println(" --help Print help for specific command");
out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)"); out.println(" --config Path to the config file (" + KcAdmMain.DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(); out.println();
out.println("Commands: "); out.println("Commands: ");
out.println(" config Set up credentials, and other configuration settings using the config file"); out.println(" config Set up credentials, and other configuration settings using the config file");

View file

@ -21,9 +21,10 @@ import com.fasterxml.jackson.databind.JsonNode;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
import picocli.CommandLine.Option; import picocli.CommandLine.Option;
import org.keycloak.client.admin.cli.common.AttributeOperation; import org.keycloak.client.admin.cli.CmdStdinContext;
import org.keycloak.client.admin.cli.common.CmdStdinContext; import org.keycloak.client.cli.common.AttributeOperation;
import org.keycloak.client.admin.cli.util.AccessibleBufferOutputStream; import org.keycloak.client.cli.common.BaseGlobalOptionsCmd;
import org.keycloak.client.cli.util.AccessibleBufferOutputStream;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -35,22 +36,20 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.SET; import static org.keycloak.client.cli.common.AttributeOperation.Type.SET;
import static org.keycloak.client.admin.cli.util.IoUtil.copyStream; import static org.keycloak.client.cli.util.IoUtil.copyStream;
import static org.keycloak.client.admin.cli.util.IoUtil.printErr; import static org.keycloak.client.cli.util.IoUtil.printErr;
import static org.keycloak.client.admin.cli.util.OsUtil.CMD; import static org.keycloak.client.cli.util.OsUtil.OS_ARCH;
import static org.keycloak.client.admin.cli.util.OsUtil.OS_ARCH; import static org.keycloak.client.cli.util.OsUtil.PROMPT;
import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT; import static org.keycloak.client.cli.util.OutputUtil.MAPPER;
import static org.keycloak.client.admin.cli.util.OutputUtil.MAPPER; import static org.keycloak.client.cli.util.ParseUtil.parseKeyVal;
import static org.keycloak.client.admin.cli.util.ParseUtil.mergeAttributes; import static org.keycloak.client.admin.cli.KcAdmMain.CMD;
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> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/ */
@Command(name = "new-object", description = "Command to create new JSON objects locally") @Command(name = "new-object", description = "Command to create new JSON objects locally")
public class NewObjectCmd extends AbstractGlobalOptionsCmd { public class NewObjectCmd extends BaseGlobalOptionsCmd implements GlobalOptionsCmdHelper {
@Option(names = {"-f", "--file"}, description = "Read object from file or standard input if FILENAME is set to '-'") @Option(names = {"-f", "--file"}, description = "Read object from file or standard input if FILENAME is set to '-'")
String file; String file;
@ -73,11 +72,11 @@ public class NewObjectCmd extends AbstractGlobalOptionsCmd {
CmdStdinContext<JsonNode> ctx = new CmdStdinContext<>(); CmdStdinContext<JsonNode> ctx = new CmdStdinContext<>();
if (file != null) { if (file != null) {
ctx = parseFileOrStdin(file); ctx = CmdStdinContext.parseFileOrStdin(file);
} }
if (attrs.size() > 0) { if (attrs.size() > 0) {
ctx = mergeAttributes(ctx, MAPPER.createObjectNode(), attrs); ctx = CmdStdinContext.mergeAttributes(ctx, MAPPER.createObjectNode(), attrs);
} }
if (body == null && ctx.getContent() != null) { if (body == null && ctx.getContent() != null) {

View file

@ -16,12 +16,13 @@
*/ */
package org.keycloak.client.admin.cli.commands; package org.keycloak.client.admin.cli.commands;
import org.keycloak.client.admin.cli.config.ConfigData; import org.keycloak.client.admin.cli.KcAdmMain;
import org.keycloak.client.admin.cli.operations.ClientOperations; import org.keycloak.client.admin.cli.operations.ClientOperations;
import org.keycloak.client.admin.cli.operations.GroupOperations; import org.keycloak.client.admin.cli.operations.GroupOperations;
import org.keycloak.client.admin.cli.operations.LocalSearch; import org.keycloak.client.admin.cli.operations.LocalSearch;
import org.keycloak.client.admin.cli.operations.RoleOperations; import org.keycloak.client.admin.cli.operations.RoleOperations;
import org.keycloak.client.admin.cli.operations.UserOperations; import org.keycloak.client.admin.cli.operations.UserOperations;
import org.keycloak.client.cli.config.ConfigData;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
@ -35,12 +36,10 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
import picocli.CommandLine.Option; import picocli.CommandLine.Option;
import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken; import static org.keycloak.client.admin.cli.KcAdmMain.CMD;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.cli.util.ConfigUtil.credentialsAvailable;
import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable; import static org.keycloak.client.cli.util.ConfigUtil.loadConfig;
import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig; import static org.keycloak.client.cli.util.OsUtil.PROMPT;
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> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -297,7 +296,7 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd {
out.println(); out.println();
out.println(" Global options:"); out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error"); 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(" --config Path to the config file (" + KcAdmMain.DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --no-config Don't use config file - no authentication info is loaded or saved"); 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(" --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(" --truststore PATH Path to a truststore containing trusted certificates");

View file

@ -16,7 +16,8 @@
*/ */
package org.keycloak.client.admin.cli.commands; package org.keycloak.client.admin.cli.commands;
import org.keycloak.client.admin.cli.config.ConfigData; import org.keycloak.client.admin.cli.KcAdmMain;
import org.keycloak.client.cli.config.ConfigData;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
@ -26,13 +27,11 @@ import picocli.CommandLine.Option;
import static org.keycloak.client.admin.cli.operations.UserOperations.getIdFromUsername; 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.operations.UserOperations.resetUserPassword;
import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken; import static org.keycloak.client.cli.util.ConfigUtil.credentialsAvailable;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.cli.util.ConfigUtil.loadConfig;
import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable; import static org.keycloak.client.cli.util.IoUtil.readSecret;
import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig; import static org.keycloak.client.cli.util.OsUtil.PROMPT;
import static org.keycloak.client.admin.cli.util.IoUtil.readSecret; import static org.keycloak.client.admin.cli.KcAdmMain.CMD;
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> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -117,7 +116,7 @@ public class SetPasswordCmd extends AbstractAuthOptionsCmd {
out.println(); out.println();
out.println(" Global options:"); out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error"); 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(" --config Path to the config file (" + KcAdmMain.DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --no-config Don't use config file - no authentication info is loaded or saved"); 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(" --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(" --truststore PATH Path to a truststore containing trusted certificates");

View file

@ -17,16 +17,17 @@
package org.keycloak.client.admin.cli.commands; package org.keycloak.client.admin.cli.commands;
import org.keycloak.client.admin.cli.KcAdmMain;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
import picocli.CommandLine.Option; import picocli.CommandLine.Option;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.admin.cli.KcAdmMain.CMD;
import static org.keycloak.client.admin.cli.util.OsUtil.CMD; import static org.keycloak.client.cli.util.OsUtil.OS_ARCH;
import static org.keycloak.client.admin.cli.util.OsUtil.OS_ARCH; import static org.keycloak.client.cli.util.OsUtil.PROMPT;
import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -97,7 +98,7 @@ public class UpdateCmd extends AbstractRequestCmd {
out.println(); out.println();
out.println(" Global options:"); out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error"); 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(" --config Path to the config file (" + KcAdmMain.DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --no-config Don't use config file - no authentication info is loaded or saved"); 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(" --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(" --truststore PATH Path to a truststore containing trusted certificates");

View file

@ -1,184 +0,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.
*/
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 String grantTypeForAuthentication;
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 String getGrantTypeForAuthentication() {
return grantTypeForAuthentication;
}
public void setGrantTypeForAuthentication(String grantTypeForAuthentication) {
this.grantTypeForAuthentication = grantTypeForAuthentication;
}
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;
grantTypeForAuthentication = source.grantTypeForAuthentication;
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.grantTypeForAuthentication = grantTypeForAuthentication;
data.expiresAt = expiresAt;
data.refreshExpiresAt = refreshExpiresAt;
data.sigExpiresAt = sigExpiresAt;
return data;
}
}

View file

@ -16,14 +16,12 @@
*/ */
package org.keycloak.client.admin.cli.operations; 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> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/ */
public class ClientOperations { public class ClientOperations {
public static String getIdFromClientId(String rootUrl, String realm, String auth, String clientId) { public static String getIdFromClientId(String rootUrl, String realm, String auth, String clientId) {
return getIdForType(rootUrl, realm, auth, "clients", "clientId", clientId, "clientId"); return OperationUtils.getIdForType(rootUrl, realm, auth, "clients", "clientId", clientId, "clientId");
} }
} }

View file

@ -18,10 +18,9 @@ package org.keycloak.client.admin.cli.operations;
import java.util.List; import java.util.List;
import static org.keycloak.client.admin.cli.util.HttpUtil.composeResourceUrl; import static org.keycloak.client.cli.util.HttpUtil.composeResourceUrl;
import static org.keycloak.client.admin.cli.util.HttpUtil.doDeleteJSON; import static org.keycloak.client.cli.util.HttpUtil.doDeleteJSON;
import static org.keycloak.client.admin.cli.util.HttpUtil.doPostJSON; import static org.keycloak.client.cli.util.HttpUtil.doPostJSON;
import static org.keycloak.client.admin.cli.util.HttpUtil.getIdForType;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -29,11 +28,11 @@ import static org.keycloak.client.admin.cli.util.HttpUtil.getIdForType;
public class GroupOperations { public class GroupOperations {
public static String getIdFromName(String rootUrl, String realm, String auth, String groupname) { public static String getIdFromName(String rootUrl, String realm, String auth, String groupname) {
return getIdForType(rootUrl, realm, auth, "groups", "search", groupname, "name", () -> new String[] { "exact", "true" }); return OperationUtils.getIdForType(rootUrl, realm, auth, "groups", "search", groupname, "name", () -> new String[] { "exact", "true" });
} }
public static String getIdFromPath(String rootUrl, String realm, String auth, String path) { public static String getIdFromPath(String rootUrl, String realm, String auth, String path) {
return getIdForType(rootUrl, realm, auth, "groups", "path", path, "path"); return OperationUtils.getIdForType(rootUrl, realm, auth, "groups", "path", path, "path");
} }
public static void addRealmRoles(String rootUrl, String realm, String auth, String groupid, List<?> roles) { public static void addRealmRoles(String rootUrl, String realm, String auth, String groupid, List<?> roles) {

View file

@ -0,0 +1,84 @@
/*
* Copyright 2024 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.cli.util.HttpUtil;
import java.util.List;
import java.util.function.Supplier;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import static org.keycloak.common.util.ObjectUtil.capitalize;
public class OperationUtils {
private static final String[] DEFAULT_QUERY_PARAMS = { "first", "0", "max", "2" };
public static String getIdForType(String rootUrl, String realm, String auth, String resourceEndpoint, String attrName, String attrValue, String inputAttrName) {
return getAttrForType(rootUrl, realm, auth, resourceEndpoint, attrName, attrValue, inputAttrName, "id", null);
}
public static String getIdForType(String rootUrl, String realm, String auth, String resourceEndpoint, String attrName, String attrValue, String inputAttrName, Supplier<String[]> endpointParams) {
return getAttrForType(rootUrl, realm, auth, resourceEndpoint, attrName, attrValue, inputAttrName, "id", endpointParams);
}
public static String getAttrForType(String rootUrl, String realm, String auth, String resourceEndpoint, String attrName, String attrValue, String inputAttrName, String returnAttrName) {
return getAttrForType(rootUrl, realm, auth, resourceEndpoint, attrName, attrValue, inputAttrName, returnAttrName, null);
}
public static String getAttrForType(String rootUrl, String realm, String auth, String resourceEndpoint, String attrName, String attrValue, String inputAttrName, String returnAttrName, Supplier<String[]> endpointParams) {
String resourceUrl = HttpUtil.composeResourceUrl(rootUrl, realm, resourceEndpoint);
String[] defaultParams;
if (endpointParams == null) {
defaultParams = DEFAULT_QUERY_PARAMS;
} else {
defaultParams = endpointParams.get();
}
resourceUrl = HttpUtil.addQueryParamsToUri(resourceUrl, attrName, attrValue);
resourceUrl = HttpUtil.addQueryParamsToUri(resourceUrl, defaultParams);
List<ObjectNode> results = HttpUtil.doGetJSON(RoleOperations.LIST_OF_NODES.class, resourceUrl, auth);
ObjectNode match;
try {
match = new LocalSearch(results).exactMatchOne(attrValue, inputAttrName);
} catch (Exception e) {
throw new RuntimeException("Multiple " + resourceEndpoint + " found for " + inputAttrName + ": " + attrValue, e);
}
String typeName = HttpUtil.singularize(resourceEndpoint);
if (match == null) {
if (results.size() > 1) {
throw new RuntimeException("Some matches, but not an exact match, found for " + capitalize(typeName) + " with " + inputAttrName + ": " + attrValue + ". Try using a more unique search, such as an id.");
}
throw new RuntimeException(capitalize(typeName) + " not found for " + inputAttrName + ": " + attrValue);
}
JsonNode attr = match.get(returnAttrName);
if (attr == null) {
throw new RuntimeException("Returned " + typeName + " info has no '" + returnAttrName + "' attribute");
}
return attr.asText();
}
}

View file

@ -17,18 +17,17 @@
package org.keycloak.client.admin.cli.operations; package org.keycloak.client.admin.cli.operations;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import static org.keycloak.client.cli.util.HttpUtil.composeResourceUrl;
import static org.keycloak.client.cli.util.HttpUtil.doDeleteJSON;
import static org.keycloak.client.cli.util.HttpUtil.doGetJSON;
import static org.keycloak.client.cli.util.HttpUtil.doPostJSON;
import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RoleRepresentation;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import static org.keycloak.client.admin.cli.util.HttpUtil.composeResourceUrl;
import static org.keycloak.client.admin.cli.util.HttpUtil.doDeleteJSON;
import static org.keycloak.client.admin.cli.util.HttpUtil.doGetJSON;
import static org.keycloak.client.admin.cli.util.HttpUtil.doPostJSON;
import static org.keycloak.client.admin.cli.util.HttpUtil.getAttrForType;
import static org.keycloak.client.admin.cli.util.HttpUtil.getIdForType;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/ */
@ -38,7 +37,7 @@ public class RoleOperations {
public static class LIST_OF_NODES extends ArrayList<ObjectNode>{}; public static class LIST_OF_NODES extends ArrayList<ObjectNode>{};
public static String getIdFromRoleName(String adminRoot, String realm, String auth, String rname) { public static String getIdFromRoleName(String adminRoot, String realm, String auth, String rname) {
return getIdForType(adminRoot, realm, auth, "roles", "search", rname, "name"); return OperationUtils.getIdForType(adminRoot, realm, auth, "roles", "search", rname, "name");
} }
public static void addRealmRoles(String rootUrl, String realm, String auth, String roleid, List<?> roles) { public static void addRealmRoles(String rootUrl, String realm, String auth, String roleid, List<?> roles) {
@ -60,11 +59,11 @@ public class RoleOperations {
} }
public static String getRoleNameFromId(String adminRoot, String realm, String auth, String rid) { public static String getRoleNameFromId(String adminRoot, String realm, String auth, String rid) {
return getAttrForType(adminRoot, realm, auth, "roles", "id", rid, "id","name"); return OperationUtils.getAttrForType(adminRoot, realm, auth, "roles", "id", rid, "id","name");
} }
public static String getClientRoleNameFromId(String adminRoot, String realm, String auth, String cid, String rid) { public static String getClientRoleNameFromId(String adminRoot, String realm, String auth, String cid, String rid) {
return getAttrForType(adminRoot, realm, auth, "clients/" + cid + "/roles", "id", rid, "id", "name"); return OperationUtils.getAttrForType(adminRoot, realm, auth, "clients/" + cid + "/roles", "id", rid, "id", "name");
} }
public static List<RoleRepresentation> getRealmRoles(String rootUrl, String realm, String auth) { public static List<RoleRepresentation> getRealmRoles(String rootUrl, String realm, String auth) {

View file

@ -16,10 +16,10 @@
*/ */
package org.keycloak.client.admin.cli.operations; package org.keycloak.client.admin.cli.operations;
import org.keycloak.client.admin.cli.util.Headers; import org.keycloak.client.cli.util.Headers;
import org.keycloak.client.admin.cli.util.HeadersBody; import org.keycloak.client.cli.util.HeadersBody;
import org.keycloak.client.admin.cli.util.HeadersBodyStatus; import org.keycloak.client.cli.util.HeadersBodyStatus;
import org.keycloak.client.admin.cli.util.HttpUtil; import org.keycloak.client.cli.util.HttpUtil;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
@ -27,10 +27,9 @@ import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import static org.keycloak.client.admin.cli.util.HttpUtil.composeResourceUrl; import static org.keycloak.client.cli.util.HttpUtil.composeResourceUrl;
import static org.keycloak.client.admin.cli.util.HttpUtil.doDeleteJSON; import static org.keycloak.client.cli.util.HttpUtil.doDeleteJSON;
import static org.keycloak.client.admin.cli.util.HttpUtil.doPostJSON; import static org.keycloak.client.cli.util.HttpUtil.doPostJSON;
import static org.keycloak.client.admin.cli.util.HttpUtil.getIdForType;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -91,7 +90,7 @@ public class UserOperations {
} }
public static String getIdFromUsername(String rootUrl, String realm, String auth, String username) { public static String getIdFromUsername(String rootUrl, String realm, String auth, String username) {
return getIdForType(rootUrl, realm, auth, "users", "username", username, "username", return OperationUtils.getIdForType(rootUrl, realm, auth, "users", "username", username, "username",
() -> new String[] {"exact", "true"}); () -> new String[] {"exact", "true"});
} }
} }

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.common; package org.keycloak.client.cli.common;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.common; package org.keycloak.client.cli.common;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>

View file

@ -0,0 +1,276 @@
/*
* 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.cli.common;
import org.keycloak.OAuth2Constants;
import org.keycloak.client.cli.config.ConfigData;
import org.keycloak.client.cli.config.ConfigHandler;
import org.keycloak.client.cli.config.FileConfigHandler;
import org.keycloak.client.cli.config.InMemoryConfigHandler;
import org.keycloak.client.cli.config.RealmConfigData;
import org.keycloak.client.cli.util.AuthUtil;
import org.keycloak.client.cli.util.ConfigUtil;
import org.keycloak.client.cli.util.HttpUtil;
import org.keycloak.client.cli.util.IoUtil;
import java.io.File;
import picocli.CommandLine.Option;
import static org.keycloak.client.cli.config.FileConfigHandler.setConfigFile;
import static org.keycloak.client.cli.util.ConfigUtil.DEFAULT_CLIENT;
import static org.keycloak.client.cli.util.ConfigUtil.checkServerInfo;
import static org.keycloak.client.cli.util.ConfigUtil.loadConfig;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public abstract class BaseAuthOptionsCmd extends BaseGlobalOptionsCmd {
public static final String DEFAULT_CONFIG_PATH_STRING_KEY = "default.config.path.string";
@Option(names = "--config", description = "Path to the config file (${sys:"+DEFAULT_CONFIG_PATH_STRING_KEY+"} by default)")
protected String config;
@Option(names = "--no-config", description = "Don't use config file - no authentication info is loaded or saved")
protected boolean noconfig;
@Option(names = "--server", description = "Server endpoint url (e.g. 'http://localhost:8080')")
protected String server;
@Option(names = "--realm", description = "Realm name to authenticate against")
protected String realm;
@Option(names = "--client", description = "Realm name to authenticate against")
protected String clientId;
@Option(names = "--user", description = "Username to login with")
protected String user;
@Option(names = "--password", description = "Password to login with (prompted for if not specified and --user is used)")
protected String password;
@Option(names = "--secret", description = "Secret to authenticate the client (prompted for if no --user or --keystore is specified)")
protected String secret;
@Option(names = "--keystore", description = "Path to a keystore containing private key")
protected String keystore;
@Option(names = "--storepass", description = "Keystore password (prompted for if not specified and --keystore is used)")
protected String storePass;
@Option(names = "--keypass", description = "Key password (prompted for if not specified and --keystore is used without --storepass, \n otherwise defaults to keystore password)")
protected String keyPass;
@Option(names = "--alias", description = "Alias of the key inside a keystore (defaults to the value of ClientId)")
protected String alias;
@Option(names = "--truststore", description = "Path to a truststore")
protected String trustStore;
@Option(names = "--trustpass", description = "Truststore password (prompted for if not specified and --truststore is used)")
protected String trustPass;
@Option(names = "--insecure", description = "Turns off TLS validation")
protected boolean insecure;
// subclasses unfortunately use different options for this, so they must be declared elsewhere
protected String externalToken;
protected CommandState commandState;
public BaseAuthOptionsCmd(CommandState state) {
this.commandState = state;
}
protected String getCommand() {
return commandState.getCommand();
}
protected String getDefaultConfigFilePath() {
return commandState.getDefaultConfigFilePath();
}
protected void initFromParent(BaseAuthOptionsCmd 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;
externalToken = parent.externalToken;
}
protected void applyDefaultOptionValues() {
if (clientId == null) {
clientId = DEFAULT_CLIENT;
}
}
@Override
protected boolean nothingToDo() {
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;
}
@Override
protected void processOptions() {
if (config != null && noconfig) {
throw new IllegalArgumentException("Options --config and --no-config are mutually exclusive");
}
if (!noconfig) {
setConfigFile(config != null ? config : getDefaultConfigFilePath());
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) {
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: ");
}
try {
HttpUtil.setTruststore(new File(truststore), pass);
} catch (Exception e) {
throw new RuntimeException("Failed to load truststore: " + truststore, e);
}
}
if (insecure) {
HttpUtil.setSkipCertificateValidation();
}
}
protected ConfigData ensureAuthInfo(ConfigData config) {
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);
BaseConfigCredentialsCmd login = new BaseConfigCredentialsCmd(commandState);
login.initFromParent(this);
login.init(config);
login.process();
// 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 {
checkServerInfo(config, getCommand());
// make sure all defaults are initialized after this point
applyDefaultOptionValues();
return loadConfig();
}
}
protected boolean requiresLogin() {
return externalToken == null && (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);
}
if (externalToken != null) {
result.setExternalToken(externalToken);
}
checkServerInfo(result, getCommand());
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);
if (externalToken != null) {
data.setExternalToken(externalToken);
}
RealmConfigData rdata = data.sessionRealmConfigData();
if (clientId != null)
rdata.setClientId(clientId);
if (secret != null)
rdata.setSecret(secret);
String grantTypeForAuthentication = user == null ? OAuth2Constants.CLIENT_CREDENTIALS : OAuth2Constants.PASSWORD;
rdata.setGrantTypeForAuthentication(grantTypeForAuthentication);
}
protected String ensureToken(ConfigData config) {
return AuthUtil.ensureToken(config, getCommand());
}
}

View file

@ -0,0 +1,248 @@
/*
* 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.cli.common;
import org.keycloak.OAuth2Constants;
import org.keycloak.client.cli.config.ConfigData;
import org.keycloak.client.cli.config.RealmConfigData;
import org.keycloak.client.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.cli.util.AuthUtil.getAuthTokens;
import static org.keycloak.client.cli.util.AuthUtil.getAuthTokensByJWT;
import static org.keycloak.client.cli.util.AuthUtil.getAuthTokensBySecret;
import static org.keycloak.client.cli.util.ConfigUtil.getHandler;
import static org.keycloak.client.cli.util.ConfigUtil.loadConfig;
import static org.keycloak.client.cli.util.ConfigUtil.saveTokens;
import static org.keycloak.client.cli.util.IoUtil.printErr;
import static org.keycloak.client.cli.util.IoUtil.readSecret;
import static org.keycloak.client.cli.util.OsUtil.OS_ARCH;
import static org.keycloak.client.cli.util.OsUtil.PROMPT;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class BaseConfigCredentialsCmd extends BaseAuthOptionsCmd {
private int sigLifetime = 600;
public BaseConfigCredentialsCmd(CommandState commandState) {
super(commandState);
}
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
protected String[] getUnsupportedOptions() {
return new String[] {"--no-config", booleanOptionForCheck(noconfig)};
}
@Override
public void process() {
// 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();
String grantTypeForAuthentication = null;
if (user != null) {
grantTypeForAuthentication = OAuth2Constants.PASSWORD;
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: ");
}
// if secret was set to be read from stdin, then ask for it
if ("-".equals(secret) && keystore == null) {
secret = readSecret("Enter client secret: ");
}
} else if (keystore != null || secret != null || clientSet) {
grantTypeForAuthentication = OAuth2Constants.CLIENT_CREDENTIALS;
printErr("Logging into " + server + " as " + "service-account-" + clientId + " of realm " + realm);
if (keystore == null) {
if (secret == null) {
secret = readSecret("Enter client secret: ");
}
}
}
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: ");
keyPass = readSecret("Enter key password: ");
}
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;
}
setupTruststore(copyWithServerInfo(loadConfig()));
// 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, grantTypeForAuthentication);
}
@Override
public String help() {
StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
out.println("Usage: " + getCommand() + " config credentials --server SERVER_URL --realm REALM [ARGUMENTS]");
out.println(" " + getCommand() + " config credentials --server SERVER_URL --realm REALM --user USER [--password PASSWORD] [ARGUMENTS]");
out.println(" " + getCommand() + " config credentials --server SERVER_URL --realm REALM --client CLIENT_ID [--secret SECRET] [ARGUMENTS]");
out.println(" " + getCommand() + " 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 (" + getDefaultConfigFilePath() + " 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')");
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 + " " + getCommand() + " config credentials --server http://localhost:8080 --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 | " + getCommand() + " config credentials --server http://localhost:9080 --realm master --user admin");
} else {
out.println(" " + PROMPT + " " + getCommand() + " config credentials --server http://localhost:9080 --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 + " " + getCommand() + " config credentials --server http://localhost:9080 --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 + " " + getCommand() + " config credentials --server http://localhost:9080 --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 + " " + getCommand() + " config credentials --server http://localhost:9080 --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 + " " + getCommand() + " config credentials --server http://localhost:9080 --realm master --user user --client reg-cli --keystore " + OS_ARCH.path("~/.keycloak/keystore.jks"));
out.println();
out.println();
out.println("Use '" + getCommand() + " help' for general information and a list of commands");
return sb.toString();
}
}

View file

@ -0,0 +1,137 @@
/*
* 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.cli.common;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import static org.keycloak.client.cli.util.ConfigUtil.saveMergeConfig;
import static org.keycloak.client.cli.util.IoUtil.readSecret;
import static org.keycloak.client.cli.util.OsUtil.OS_ARCH;
import static org.keycloak.client.cli.util.OsUtil.PROMPT;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class BaseConfigTruststoreCmd extends BaseAuthOptionsCmd {
@Parameters(arity = "0..1")
private String store;
@Option(names = {"-d", "--delete"}, description = "Remove truststore configuration")
private boolean delete;
public BaseConfigTruststoreCmd(CommandState commandState) {
super(commandState);
}
@Override
protected boolean nothingToDo() {
return super.nothingToDo() && store == null && !delete;
}
@Override
protected String[] getUnsupportedOptions() {
return new String[] {"--server", server,
"--realm", realm,
"--client", clientId,
"--user", user,
"--password", password,
"--secret", secret,
"--truststore", trustStore,
"--keystore", keystore,
"--keypass", keyPass,
"--alias", alias,
"--no-config", booleanOptionForCheck(noconfig)};
}
@Override
protected void process() {
String pass;
if (!delete) {
if (store == null) {
throw new IllegalArgumentException("No truststore specified");
}
if (!new File(store).isFile()) {
throw new RuntimeException("Truststore file not found: " + store);
}
if ("-".equals(trustPass)) {
trustPass = readSecret("Enter truststore password: ");
}
pass = trustPass;
} else {
if (store != 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");
}
pass = null;
}
saveMergeConfig(config -> {
config.setTruststore(store);
config.setTrustpass(pass);
});
}
@Override
public String help() {
StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
out.println("Usage: " + getCommand() + " 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 (" + getDefaultConfigFilePath() + " 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 + " " + getCommand() + " 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 + " " + getCommand() + " config truststore --trustpass " + OS_ARCH.envVar("PASSWORD") + " " + OS_ARCH.path("~/.keycloak/truststore.jks"));
out.println();
out.println("Remove truststore configuration:");
out.println(" " + PROMPT + " " + getCommand() + " config truststore --delete");
out.println();
out.println();
out.println("Use '" + getCommand() + " help' for general information and a list of commands");
return sb.toString();
}
}

View file

@ -1,16 +1,27 @@
package org.keycloak.client.registration.cli.commands; /*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
import org.keycloak.client.registration.cli.Globals; * 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.cli.common;
import picocli.CommandLine; import picocli.CommandLine;
import picocli.CommandLine.Option; import picocli.CommandLine.Option;
import static org.keycloak.client.registration.cli.util.IoUtil.printOut; import static org.keycloak.client.cli.util.IoUtil.printOut;
/** public abstract class BaseGlobalOptionsCmd implements Runnable {
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public abstract class AbstractGlobalOptionsCmd implements Runnable {
@Option(names = "--help", @Option(names = "--help",
description = "Print command specific help") description = "Print command specific help")
@ -38,9 +49,7 @@ public abstract class AbstractGlobalOptionsCmd implements Runnable {
return false; return false;
} }
protected String help() { protected abstract String help();
return KcRegCmd.usage();
}
@Override @Override
public void run() { public void run() {

View file

@ -1,5 +1,5 @@
/* /*
* Copyright 2016 Red Hat, Inc. and/or its affiliates * Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags. * and other contributors as indicated by the @author tags.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -14,15 +14,13 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli;
/** package org.keycloak.client.cli.common;
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class Globals {
public static boolean dumpTrace = false; public interface CommandState {
public static boolean help = false; String getCommand();
String getDefaultConfigFilePath();
} }

View file

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli; package org.keycloak.client.cli.common;
import picocli.CommandLine; import picocli.CommandLine;
import picocli.CommandLine.ParseResult; import picocli.CommandLine.ParseResult;

View file

@ -0,0 +1,65 @@
/*
* 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.cli.common;
import org.keycloak.client.cli.util.ClassLoaderUtil;
import org.keycloak.common.crypto.CryptoIntegration;
import java.io.PrintWriter;
import picocli.CommandLine;
import picocli.CommandLine.Model.CommandSpec;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class Globals {
public static boolean dumpTrace = false;
public static boolean help = false;
public static void main(String [] args, BaseGlobalOptionsCmd rootCommand, String command, String defaultConfigFile) {
String libDir = System.getProperty("kc.lib.dir");
if (libDir == null) {
throw new RuntimeException("System property kc.lib.dir needs to be set");
}
ClassLoader cl = ClassLoaderUtil.resolveClassLoader(libDir);
Thread.currentThread().setContextClassLoader(cl);
CryptoIntegration.init(cl);
System.setProperty(BaseAuthOptionsCmd.DEFAULT_CONFIG_PATH_STRING_KEY, defaultConfigFile);
CommandLine cli = createCommandLine(rootCommand, command);
int exitCode = cli.execute(args);
System.exit(exitCode);
}
public static CommandLine createCommandLine(BaseGlobalOptionsCmd rootCommand, String command) {
CommandSpec spec = CommandSpec.forAnnotatedObject(rootCommand).name(command);
CommandLine cmd = new CommandLine(spec);
cmd.setExecutionExceptionHandler(new ExecutionExceptionHandler());
cmd.setParameterExceptionHandler(new ShortErrorMessageHandler());
cmd.setErr(new PrintWriter(System.err, true));
return cmd;
}
}

View file

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli; package org.keycloak.client.cli.common;
import java.io.PrintWriter; import java.io.PrintWriter;

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.config; package org.keycloak.client.cli.config;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.config; package org.keycloak.client.cli.config;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.config; package org.keycloak.client.cli.config;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>

View file

@ -14,9 +14,9 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.config; package org.keycloak.client.cli.config;
import org.keycloak.client.admin.cli.util.IoUtil; import org.keycloak.client.cli.util.IoUtil;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@ -30,7 +30,7 @@ import java.nio.channels.OverlappingFileLockException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import static org.keycloak.client.admin.cli.util.IoUtil.printErr; import static org.keycloak.client.cli.util.IoUtil.printErr;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.config; package org.keycloak.client.cli.config;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>

View file

@ -14,8 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.cli.config;
package org.keycloak.client.registration.cli.config;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
@ -23,6 +22,8 @@ import java.io.IOException;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import com.fasterxml.jackson.annotation.JsonInclude;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/ */
@ -50,11 +51,12 @@ public class RealmConfigData {
private Long sigExpiresAt; private Long sigExpiresAt;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String initialToken; private String initialToken;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private Map<String, String> clients = new LinkedHashMap<String, String>(); private Map<String, String> clients = new LinkedHashMap<String, String>();
public String serverUrl() { public String serverUrl() {
return serverUrl; return serverUrl;
} }
@ -111,6 +113,14 @@ public class RealmConfigData {
this.secret = secret; this.secret = secret;
} }
public String getGrantTypeForAuthentication() {
return grantTypeForAuthentication;
}
public void setGrantTypeForAuthentication(String grantTypeForAuthentication) {
this.grantTypeForAuthentication = grantTypeForAuthentication;
}
public Long getExpiresAt() { public Long getExpiresAt() {
return expiresAt; return expiresAt;
} }
@ -127,14 +137,6 @@ public class RealmConfigData {
this.refreshExpiresAt = refreshExpiresAt; this.refreshExpiresAt = refreshExpiresAt;
} }
public String getGrantTypeForAuthentication() {
return grantTypeForAuthentication;
}
public void setGrantTypeForAuthentication(String grantTypeForAuthentication) {
this.grantTypeForAuthentication = grantTypeForAuthentication;
}
public Long getSigExpiresAt() { public Long getSigExpiresAt() {
return sigExpiresAt; return sigExpiresAt;
} }
@ -194,13 +196,6 @@ public class RealmConfigData {
refreshToken = source.refreshToken; refreshToken = source.refreshToken;
expiresAt = source.expiresAt; expiresAt = source.expiresAt;
refreshExpiresAt = source.refreshExpiresAt; refreshExpiresAt = source.refreshExpiresAt;
mergeClients(source);
}
public void mergeRegistrationTokens(RealmConfigData source) {
initialToken = source.initialToken;
mergeClients(source);
} }
@Override @Override

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.cli.util;
import java.io.FilterOutputStream; import java.io.FilterOutputStream;
import java.io.IOException; import java.io.IOException;

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.cli.util;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>

View file

@ -14,10 +14,10 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.cli.util;
import org.keycloak.client.admin.cli.config.ConfigData; import org.keycloak.client.cli.config.ConfigData;
import org.keycloak.client.admin.cli.config.RealmConfigData; import org.keycloak.client.cli.config.RealmConfigData;
import org.keycloak.common.util.KeystoreUtil; import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.jose.jws.JWSBuilder;
@ -33,24 +33,24 @@ import java.security.KeyPair;
import java.util.UUID; import java.util.UUID;
import static java.lang.System.currentTimeMillis; import static java.lang.System.currentTimeMillis;
import static org.keycloak.client.admin.cli.util.ConfigUtil.checkAuthInfo; import static org.keycloak.client.cli.util.ConfigUtil.checkServerInfo;
import static org.keycloak.client.admin.cli.util.ConfigUtil.saveMergeConfig; import static org.keycloak.client.cli.util.ConfigUtil.saveMergeConfig;
import static org.keycloak.client.admin.cli.util.HttpUtil.APPLICATION_FORM_URL_ENCODED; import static org.keycloak.client.cli.util.HttpUtil.APPLICATION_FORM_URL_ENCODED;
import static org.keycloak.client.admin.cli.util.HttpUtil.APPLICATION_JSON; import static org.keycloak.client.cli.util.HttpUtil.APPLICATION_JSON;
import static org.keycloak.client.admin.cli.util.HttpUtil.doPost; import static org.keycloak.client.cli.util.HttpUtil.doPost;
import static org.keycloak.client.admin.cli.util.HttpUtil.urlencode; import static org.keycloak.client.cli.util.HttpUtil.urlencode;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/ */
public class AuthUtil { public class AuthUtil {
public static String ensureToken(ConfigData config) { public static String ensureToken(ConfigData config, String cmd) {
if (config.getExternalToken() != null) { if (config.getExternalToken() != null) {
return config.getExternalToken(); return config.getExternalToken();
} }
checkAuthInfo(config); checkServerInfo(config, cmd);
RealmConfigData realmConfig = config.sessionRealmConfigData(); RealmConfigData realmConfig = config.sessionRealmConfigData();
@ -63,11 +63,11 @@ public class AuthUtil {
// check refresh_token against expiry time // check refresh_token against expiry time
// if it's less than 5s to expiry, fail with credentials expired // if it's less than 5s to expiry, fail with credentials expired
if (realmConfig.getRefreshExpiresAt() != null && realmConfig.getRefreshExpiresAt() - now < 5000) { if (realmConfig.getRefreshExpiresAt() != null && realmConfig.getRefreshExpiresAt() - now < 5000) {
throw new RuntimeException("Session has expired. Login again with '" + OsUtil.CMD + " config credentials'"); throw new RuntimeException("Session has expired. Login again with '" + cmd + " config credentials'");
} }
if (realmConfig.getSigExpiresAt() != null && realmConfig.getSigExpiresAt() - now < 5000) { if (realmConfig.getSigExpiresAt() != null && realmConfig.getSigExpiresAt() - now < 5000) {
throw new RuntimeException("Session has expired. Login again with '" + OsUtil.CMD + " config credentials'"); throw new RuntimeException("Session has expired. Login again with '" + cmd + " config credentials'");
} }
try { try {

View file

@ -17,7 +17,7 @@
* *
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.cli.util;
import java.io.File; import java.io.File;
import java.net.MalformedURLException; import java.net.MalformedURLException;

View file

@ -14,14 +14,14 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.cli.util;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.client.admin.cli.config.ConfigData; import org.keycloak.client.cli.config.ConfigData;
import org.keycloak.client.admin.cli.config.ConfigHandler; import org.keycloak.client.cli.config.ConfigHandler;
import org.keycloak.client.admin.cli.config.ConfigUpdateOperation; import org.keycloak.client.cli.config.ConfigUpdateOperation;
import org.keycloak.client.admin.cli.config.InMemoryConfigHandler; import org.keycloak.client.cli.config.InMemoryConfigHandler;
import org.keycloak.client.admin.cli.config.RealmConfigData; import org.keycloak.client.cli.config.RealmConfigData;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
/** /**
@ -31,10 +31,6 @@ public class ConfigUtil {
public static final String DEFAULT_CLIENT = "admin-cli"; 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; private static ConfigHandler handler;
public static ConfigHandler getHandler() { public static ConfigHandler getHandler() {
@ -45,6 +41,15 @@ public class ConfigUtil {
ConfigUtil.handler = handler; ConfigUtil.handler = handler;
} }
public static String getRegistrationToken(RealmConfigData data, String clientId) {
String token = data.getClients().get(clientId);
return token == null || token.length() == 0 ? null : token;
}
public static void setRegistrationToken(RealmConfigData data, String clientId, String token) {
data.getClients().put(clientId, token == null ? "" : token);
}
public static void saveTokens(AccessTokenResponse tokens, String endpoint, String realm, String clientId, String signKey, Long sigExpiresAt, String secret, public static void saveTokens(AccessTokenResponse tokens, String endpoint, String realm, String clientId, String signKey, Long sigExpiresAt, String secret,
String grantTypeForAuthentication) { String grantTypeForAuthentication) {
handler.saveMergeConfig(config -> { handler.saveMergeConfig(config -> {
@ -67,19 +72,15 @@ public class ConfigUtil {
}); });
} }
public static void checkServerInfo(ConfigData config) { public static void checkServerInfo(ConfigData config, String cmd) {
if (config.getServerUrl() == null) { if (config.getServerUrl() == null) {
throw new RuntimeException("No server specified. Use --server, or '" + OsUtil.CMD + " config credentials'."); throw new RuntimeException("No server specified. Use --server, or '" + cmd + " config credentials'.");
} }
if (config.getRealm() == null && config.getExternalToken() == null) { if (config.getRealm() == null && config.getExternalToken() == null) {
throw new RuntimeException("No realm or token specified. Use --realm, --token, or '" + OsUtil.CMD + " config credentials'."); throw new RuntimeException("No realm or token specified. Use --realm, --token, or '" + cmd + " config credentials'.");
} }
} }
public static void checkAuthInfo(ConfigData config) {
checkServerInfo(config);
}
public static boolean credentialsAvailable(ConfigData config) { public static boolean credentialsAvailable(ConfigData config) {
// Just supporting "client_credentials" grant type for the case when refresh token is missing // Just supporting "client_credentials" grant type for the case when refresh token is missing
boolean credsAvailable = config.getServerUrl() != null && (config.getExternalToken() != null || (config.getRealm() != null boolean credsAvailable = config.getServerUrl() != null && (config.getExternalToken() != null || (config.getRealm() != null

View file

@ -14,18 +14,18 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.cli.util;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import static org.keycloak.client.cli.util.OutputUtil.MAPPER;
import static org.keycloak.client.cli.util.OutputUtil.convertToJsonNode;
import java.io.IOException; import java.io.IOException;
import java.util.Iterator; 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> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/ */

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.cli.util;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.cli.util;
import org.apache.http.entity.ContentType; import org.apache.http.entity.ContentType;

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.cli.util;
import org.apache.http.entity.ContentType; import org.apache.http.entity.ContentType;
@ -22,7 +22,7 @@ import java.io.ByteArrayOutputStream;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import static org.keycloak.client.admin.cli.util.IoUtil.copyStream; import static org.keycloak.client.cli.util.IoUtil.copyStream;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.cli.util;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.httpcomponents; package org.keycloak.client.cli.util;
import org.apache.http.annotation.Contract; import org.apache.http.annotation.Contract;
import org.apache.http.annotation.ThreadingBehavior; import org.apache.http.annotation.ThreadingBehavior;

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.cli.util;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>

View file

@ -14,10 +14,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.cli.util;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.http.HeaderIterator; import org.apache.http.HeaderIterator;
import org.apache.http.HttpHeaders; import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
@ -36,9 +34,6 @@ import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.ssl.SSLContextBuilder; import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.SSLContexts; 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 org.keycloak.util.JsonSerialization;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
@ -53,12 +48,8 @@ import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import static org.keycloak.common.util.ObjectUtil.capitalize;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -69,7 +60,6 @@ public class HttpUtil {
public static final String APPLICATION_JSON = "application/json"; 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 APPLICATION_FORM_URL_ENCODED = "application/x-www-form-urlencoded";
public static final String UTF_8 = "utf-8"; public static final String UTF_8 = "utf-8";
private static final String[] DEFAULT_QUERY_PARAMS = { "first", "0", "max", "2" };
private static HttpClient httpClient; private static HttpClient httpClient;
private static SSLConnectionSocketFactory sslsf; private static SSLConnectionSocketFactory sslsf;
@ -436,57 +426,6 @@ public class HttpUtil {
checkSuccess(resourceUrl, response); checkSuccess(resourceUrl, response);
} }
public static String getIdForType(String rootUrl, String realm, String auth, String resourceEndpoint, String attrName, String attrValue, String inputAttrName) {
return getAttrForType(rootUrl, realm, auth, resourceEndpoint, attrName, attrValue, inputAttrName, "id", null);
}
public static String getIdForType(String rootUrl, String realm, String auth, String resourceEndpoint, String attrName, String attrValue, String inputAttrName, Supplier<String[]> endpointParams) {
return getAttrForType(rootUrl, realm, auth, resourceEndpoint, attrName, attrValue, inputAttrName, "id", endpointParams);
}
public static String getAttrForType(String rootUrl, String realm, String auth, String resourceEndpoint, String attrName, String attrValue, String inputAttrName, String returnAttrName) {
return getAttrForType(rootUrl, realm, auth, resourceEndpoint, attrName, attrValue, inputAttrName, returnAttrName, null);
}
public static String getAttrForType(String rootUrl, String realm, String auth, String resourceEndpoint, String attrName, String attrValue, String inputAttrName, String returnAttrName, Supplier<String[]> endpointParams) {
String resourceUrl = composeResourceUrl(rootUrl, realm, resourceEndpoint);
String[] defaultParams;
if (endpointParams == null) {
defaultParams = DEFAULT_QUERY_PARAMS;
} else {
defaultParams = endpointParams.get();
}
resourceUrl = HttpUtil.addQueryParamsToUri(resourceUrl, attrName, attrValue);
resourceUrl = HttpUtil.addQueryParamsToUri(resourceUrl, defaultParams);
List<ObjectNode> results = doGetJSON(RoleOperations.LIST_OF_NODES.class, resourceUrl, auth);
ObjectNode match;
try {
match = new LocalSearch(results).exactMatchOne(attrValue, inputAttrName);
} catch (Exception e) {
throw new RuntimeException("Multiple " + resourceEndpoint + " found for " + inputAttrName + ": " + attrValue, e);
}
String typeName = singularize(resourceEndpoint);
if (match == null) {
if (results.size() > 1) {
throw new RuntimeException("Some matches, but not an exact match, found for " + capitalize(typeName) + " with " + inputAttrName + ": " + attrValue + ". Try using a more unique search, such as an id.");
}
throw new RuntimeException(capitalize(typeName) + " not found for " + inputAttrName + ": " + attrValue);
}
JsonNode attr = match.get(returnAttrName);
if (attr == null) {
throw new RuntimeException("Returned " + typeName + " info has no '" + returnAttrName + "' attribute");
}
return attr.asText();
}
public static String singularize(String value) { public static String singularize(String value) {
return value.substring(0, value.length()-1); return value.substring(0, value.length()-1);
} }

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.cli.util;
import java.io.Console; import java.io.Console;
import java.io.FileInputStream; import java.io.FileInputStream;

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.cli.util;
/** /**
* @author <a href="mailto:marko.strukelj@gmail.com">Marko Strukelj</a> * @author <a href="mailto:marko.strukelj@gmail.com">Marko Strukelj</a>

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.cli.util;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -22,8 +22,6 @@ package org.keycloak.client.admin.cli.util;
public class OsUtil { public class OsUtil {
public static final OsArch OS_ARCH = determineOSAndArch(); 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 PROMPT = OS_ARCH.isWindows() ? "c:\\>" : "$";

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.cli.util;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.cli.util;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.DeserializationFeature;

View file

@ -14,31 +14,24 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.common; package org.keycloak.client.cli.util;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/ */
public class CmdStdinContext<T> { public class ParseUtil {
private T result; public static String[] parseKeyVal(String keyval) {
private String content; // we expect = as a separator
int pos = keyval.indexOf("=");
public CmdStdinContext() {} if (pos <= 0) {
throw new IllegalArgumentException("Invalid key=value parameter: [" + keyval + "]");
public T getResult() {
return result;
} }
public void setResult(T result) { String [] parsed = new String[2];
this.result = result; parsed[0] = keyval.substring(0, pos);
} parsed[1] = keyval.substring(pos+1);
public String getContent() { return parsed;
return content;
}
public void setContent(String content) {
this.content = content;
} }
} }

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.cli.util;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;

View file

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.admin.cli;
import org.junit.Test; import org.junit.Test;

View file

@ -14,22 +14,22 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.cli.util;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Test; import org.junit.Test;
import org.keycloak.client.admin.cli.common.AttributeOperation; import org.keycloak.client.admin.cli.CmdStdinContext;
import org.keycloak.client.admin.cli.common.CmdStdinContext; import org.keycloak.client.admin.cli.ReflectionUtil;
import org.keycloak.client.cli.common.AttributeOperation;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.DELETE; import static org.keycloak.client.cli.common.AttributeOperation.Type.DELETE;
import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.SET; import static org.keycloak.client.cli.common.AttributeOperation.Type.SET;
import static org.keycloak.client.admin.cli.util.OutputUtil.MAPPER; import static org.keycloak.client.cli.util.OutputUtil.MAPPER;
import static org.keycloak.client.admin.cli.util.ParseUtil.mergeAttributes;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -71,7 +71,7 @@ public class MergeAttributesTest {
CmdStdinContext<JsonNode> ctx = new CmdStdinContext<>(); CmdStdinContext<JsonNode> ctx = new CmdStdinContext<>();
ctx.setResult(localNode); ctx.setResult(localNode);
ctx = mergeAttributes(ctx, MAPPER.createObjectNode(), attrs); ctx = CmdStdinContext.mergeAttributes(ctx, MAPPER.createObjectNode(), attrs);
System.out.println(ctx); System.out.println(ctx);
String remoteJSON = "{\n" + String remoteJSON = "{\n" +

View file

@ -15,9 +15,11 @@
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.cli.util;
import org.junit.Test; import org.junit.Test;
import org.keycloak.client.cli.util.OutputUtil;
import org.keycloak.client.cli.util.ReturnFields;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;

View file

@ -14,10 +14,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.admin.cli.util; package org.keycloak.client.cli.util;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.keycloak.client.cli.util.ReturnFields;
/** /**
* @author <a href="mailto:marko.strukelj@gmail.com">Marko Strukelj</a> * @author <a href="mailto:marko.strukelj@gmail.com">Marko Strukelj</a>

View file

@ -15,13 +15,10 @@
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.registration.cli.util; package org.keycloak.client.registration.cli;
import com.fasterxml.jackson.core.JsonParseException; import org.keycloak.client.cli.common.AttributeOperation;
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; import org.keycloak.client.cli.util.AttributeException;
import org.keycloak.client.registration.cli.common.AttributeOperation;
import org.keycloak.client.registration.cli.common.CmdStdinContext;
import org.keycloak.client.registration.cli.common.EndpointType;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation; import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
@ -29,38 +26,65 @@ import org.keycloak.util.JsonSerialization;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import static java.lang.System.arraycopy; import com.fasterxml.jackson.core.JsonParseException;
import static org.keycloak.client.registration.cli.util.IoUtil.readFileOrStdin; import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
import static org.keycloak.client.registration.cli.util.ReflectionUtil.setAttributes;
import static org.keycloak.client.cli.util.IoUtil.readFileOrStdin;
import static org.keycloak.client.registration.cli.ReflectionUtil.setAttributes;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/ */
public class ParseUtil { public class CmdStdinContext {
private EndpointType regType;
private ClientRepresentation client;
private OIDCClientRepresentation oidcClient;
private String content;
public static final String CLIENT_OPTION_WARN = "You're using what looks like an OPTION as CLIENT: %s"; public static final String CLIENT_OPTION_WARN = "You're using what looks like an OPTION as CLIENT: %s";
public static final String TOKEN_OPTION_WARN = "You're using what looks like an OPTION as TOKEN: %s"; public static final String TOKEN_OPTION_WARN = "You're using what looks like an OPTION as TOKEN: %s";
public static String[] shift(String[] args) { public CmdStdinContext() {}
if (args.length == 1)
return new String[0]; public EndpointType getEndpointType() {
String [] nu = new String [args.length-1]; return regType;
arraycopy(args, 1, nu, 0, args.length-1);
return nu;
} }
public static String[] parseKeyVal(String keyval) { public void setEndpointType(EndpointType regType) {
// we expect = as a separator this.regType = regType;
int pos = keyval.indexOf("=");
if (pos <= 0) {
throw new RuntimeException("Invalid key=value parameter: [" + keyval + "]");
} }
String [] parsed = new String[2]; public ClientRepresentation getClient() {
parsed[0] = keyval.substring(0, pos); return client;
parsed[1] = keyval.substring(pos+1); }
return parsed; public void setClient(ClientRepresentation client) {
this.client = client;
}
public OIDCClientRepresentation getOidcClient() {
return oidcClient;
}
public void setOidcClient(OIDCClientRepresentation oidcClient) {
this.oidcClient = oidcClient;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getRegistrationAccessToken() {
if (client != null) {
return client.getRegistrationAccessToken();
} else if (oidcClient != null) {
return oidcClient.getRegistrationAccessToken();
}
return null;
} }
public static CmdStdinContext parseFileOrStdin(String file, EndpointType type) { public static CmdStdinContext parseFileOrStdin(String file, EndpointType type) {

View file

@ -15,7 +15,9 @@
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.registration.cli.common; package org.keycloak.client.registration.cli;
import org.keycloak.client.cli.util.HttpUtil;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
@ -60,4 +62,16 @@ public enum EndpointType {
public String getName() { public String getName() {
return preferredName; return preferredName;
} }
public static String getExpectedContentType(EndpointType type) {
switch (type) {
case DEFAULT:
case OIDC:
return HttpUtil.APPLICATION_JSON;
case SAML2:
return HttpUtil.APPLICATION_XML;
default:
throw new RuntimeException("Unsupported endpoint type: " + type);
}
}
} }

View file

@ -1,7 +1,5 @@
package org.keycloak.client.registration.cli; package org.keycloak.client.registration.cli;
import org.keycloak.client.registration.cli.common.EndpointType;
import picocli.CommandLine.ITypeConverter; import picocli.CommandLine.ITypeConverter;
/** /**

View file

@ -1,12 +0,0 @@
package org.keycloak.client.registration.cli;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class Globals {
public static boolean dumpTrace = false;
public static boolean help = false;
}

View file

@ -1,46 +1,36 @@
package org.keycloak.client.registration.cli; package org.keycloak.client.registration.cli;
import org.keycloak.client.admin.cli.ExecutionExceptionHandler; import org.keycloak.client.cli.common.CommandState;
import org.keycloak.client.admin.cli.ShortErrorMessageHandler; import org.keycloak.client.cli.common.Globals;
import org.keycloak.client.cli.util.OsUtil;
import org.keycloak.client.registration.cli.commands.KcRegCmd; import org.keycloak.client.registration.cli.commands.KcRegCmd;
import org.keycloak.client.registration.cli.util.ClassLoaderUtil;
import org.keycloak.client.registration.cli.util.OsUtil;
import org.keycloak.common.crypto.CryptoIntegration;
import java.io.PrintWriter;
import picocli.CommandLine;
import picocli.CommandLine.Model.CommandSpec;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/ */
public class KcRegMain { public class KcRegMain {
public static final String DEFAULT_CONFIG_FILE_PATH = System.getProperty("user.home") + "/.keycloak/kcreg.config";
public static final String DEFAULT_CONFIG_FILE_STRING = OsUtil.OS_ARCH.isWindows() ? "%HOMEDRIVE%%HOMEPATH%\\.keycloak\\kcreg.config" : "~/.keycloak/kcreg.config";
public static final String CMD = OsUtil.OS_ARCH.isWindows() ? "kcreg.bat" : "kcreg.sh";
public static final CommandState COMMAND_STATE = new CommandState() {
@Override
public String getCommand() {
return CMD;
}
@Override
public String getDefaultConfigFilePath() {
return DEFAULT_CONFIG_FILE_PATH;
}
};
public static void main(String [] args) { public static void main(String [] args) {
String libDir = System.getProperty("kc.lib.dir"); Globals.main(args, new KcRegCmd(), CMD, DEFAULT_CONFIG_FILE_STRING);
if (libDir == null) {
throw new RuntimeException("System property kc.lib.dir needs to be set");
}
ClassLoader cl = ClassLoaderUtil.resolveClassLoader(libDir);
Thread.currentThread().setContextClassLoader(cl);
CryptoIntegration.init(cl);
CommandLine cli = createCommandLine();
int exitCode = cli.execute(args);
System.exit(exitCode);
}
public static CommandLine createCommandLine() {
CommandSpec spec = CommandSpec.forAnnotatedObject(new KcRegCmd()).name(OsUtil.CMD);
CommandLine cmd = new CommandLine(spec);
cmd.setExecutionExceptionHandler(new ExecutionExceptionHandler());
cmd.setParameterExceptionHandler(new ShortErrorMessageHandler());
cmd.setErr(new PrintWriter(System.err, true));
return cmd;
} }
} }

View file

@ -15,11 +15,13 @@
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.client.registration.cli.util; package org.keycloak.client.registration.cli;
import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonParseException;
import org.keycloak.client.registration.cli.common.AttributeKey;
import org.keycloak.client.registration.cli.common.AttributeOperation; import org.keycloak.client.cli.common.AttributeKey;
import org.keycloak.client.cli.common.AttributeOperation;
import org.keycloak.client.cli.util.AttributeException;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import java.io.IOException; import java.io.IOException;

View file

@ -1,255 +1,22 @@
package org.keycloak.client.registration.cli.commands; package org.keycloak.client.registration.cli.commands;
import org.keycloak.OAuth2Constants; import org.keycloak.client.cli.common.BaseAuthOptionsCmd;
import org.keycloak.client.registration.cli.config.ConfigData; import org.keycloak.client.registration.cli.KcRegMain;
import org.keycloak.client.registration.cli.config.ConfigHandler;
import org.keycloak.client.registration.cli.config.FileConfigHandler;
import org.keycloak.client.registration.cli.config.InMemoryConfigHandler;
import org.keycloak.client.registration.cli.config.RealmConfigData;
import org.keycloak.client.registration.cli.util.ConfigUtil;
import org.keycloak.client.registration.cli.util.HttpUtil;
import org.keycloak.client.registration.cli.util.IoUtil;
import java.io.File;
import picocli.CommandLine.Option; import picocli.CommandLine.Option;
import static org.keycloak.client.registration.cli.config.FileConfigHandler.setConfigFile;
import static org.keycloak.client.registration.cli.util.ConfigUtil.checkAuthInfo;
import static org.keycloak.client.registration.cli.util.ConfigUtil.checkServerInfo;
import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/ */
public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd { public abstract class AbstractAuthOptionsCmd extends BaseAuthOptionsCmd {
static final String DEFAULT_CLIENT = "admin-cli";
@Option(names = "--config", description = "Path to the config file (~/.keycloak/kcreg.config by default)")
protected String config;
@Option(names = "--no-config", description = "No configuration file should be used, no authentication info is loaded or saved")
protected boolean noconfig;
@Option(names = "--server", description = "Server endpoint url (e.g. 'http://localhost:8080')")
protected String server;
@Option(names = "--realm", description = "Realm name to authenticate against")
protected String realm;
@Option(names = "--client", description = "Realm name to authenticate against")
protected String clientId;
@Option(names = "--user", description = "Username to login with")
protected String user;
@Option(names = "--password", description = "Password to login with (prompted for if not specified and --user is used)")
protected String password;
@Option(names = "--secret", description = "Secret to authenticate the client (prompted for if no --user or --keystore is specified)")
protected String secret;
@Option(names = "--keystore", description = "Path to a keystore containing private key")
protected String keystore;
@Option(names = "--storepass", description = "Keystore password (prompted for if not specified and --keystore is used)")
protected String storePass;
@Option(names = "--keypass", description = "Key password (prompted for if not specified and --keystore is used without --storepass, \n otherwise defaults to keystore password)")
protected String keyPass;
@Option(names = "--alias", description = "Alias of the key inside a keystore (defaults to the value of ClientId)")
protected String alias;
@Option(names = "--truststore", description = "Path to a truststore")
protected String trustStore;
@Option(names = "--trustpass", description = "Truststore password (prompted for if not specified and --truststore is used)")
protected String trustPass;
@Option(names = "--insecure", description = "Turns off TLS validation")
protected boolean insecure;
@Option(names = {"-t", "--token"}, description = "Initial / Registration access token to use)") @Option(names = {"-t", "--token"}, description = "Initial / Registration access token to use)")
protected String token; public void setToken(String token) {
this.externalToken = token;
protected void initFromParent(AbstractAuthOptionsCmd 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;
token = parent.token;
insecure = parent.insecure;
} }
protected void applyDefaultOptionValues() { public AbstractAuthOptionsCmd() {
if (clientId == null) { super(KcRegMain.COMMAND_STATE);
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 &&
token == null && config == null;
}
@Override
protected void processOptions() {
if (config != null && noconfig) {
throw new IllegalArgumentException("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) {
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: ");
}
try {
HttpUtil.setTruststore(new File(truststore), pass);
} catch (Exception e) {
throw new RuntimeException("Failed to load truststore: " + truststore, e);
}
}
if (insecure) {
HttpUtil.setSkipCertificateValidation();
}
}
protected ConfigData ensureAuthInfo(ConfigData config) {
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();
// 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);
String grantTypeForAuthentication = user == null ? OAuth2Constants.CLIENT_CREDENTIALS : OAuth2Constants.PASSWORD;
rdata.setGrantTypeForAuthentication(grantTypeForAuthentication);
}
@Override
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 IllegalArgumentException("Unsupported option: " + name);
}
}
}
protected static String booleanOptionForCheck(boolean value) {
return value ? "true" : null;
}
} }

View file

@ -1,8 +1,9 @@
package org.keycloak.client.registration.cli.commands; package org.keycloak.client.registration.cli.commands;
import org.keycloak.client.registration.cli.common.AttributeKey; import org.keycloak.client.cli.common.AttributeKey;
import org.keycloak.client.registration.cli.common.EndpointType; import org.keycloak.client.cli.common.BaseGlobalOptionsCmd;
import org.keycloak.client.registration.cli.util.ReflectionUtil; import org.keycloak.client.registration.cli.EndpointType;
import org.keycloak.client.registration.cli.ReflectionUtil;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation; import org.keycloak.representations.oidc.OIDCClientRepresentation;
@ -20,18 +21,18 @@ import picocli.CommandLine.Command;
import picocli.CommandLine.Option; import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters; import picocli.CommandLine.Parameters;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD; import static org.keycloak.client.cli.util.OsUtil.PROMPT;
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT; import static org.keycloak.client.registration.cli.KcRegMain.CMD;
import static org.keycloak.client.registration.cli.util.ReflectionUtil.getAttributeListWithJSonTypes; import static org.keycloak.client.registration.cli.ReflectionUtil.getAttributeListWithJSonTypes;
import static org.keycloak.client.registration.cli.util.ReflectionUtil.isBasicType; import static org.keycloak.client.registration.cli.ReflectionUtil.isBasicType;
import static org.keycloak.client.registration.cli.util.ReflectionUtil.isListType; import static org.keycloak.client.registration.cli.ReflectionUtil.isListType;
import static org.keycloak.client.registration.cli.util.ReflectionUtil.isMapType; import static org.keycloak.client.registration.cli.ReflectionUtil.isMapType;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/ */
@Command(name = "attrs", description = "[ATTRIBUTE] [--endpoint TYPE]") @Command(name = "attrs", description = "[ATTRIBUTE] [--endpoint TYPE]")
public class AttrsCmd extends AbstractGlobalOptionsCmd { public class AttrsCmd extends BaseGlobalOptionsCmd {
CommandLine.Model.CommandSpec spec; CommandLine.Model.CommandSpec spec;

View file

@ -22,7 +22,7 @@ import java.io.StringWriter;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD; import static org.keycloak.client.registration.cli.KcRegMain.CMD;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>

View file

@ -1,243 +1,18 @@
package org.keycloak.client.registration.cli.commands; package org.keycloak.client.registration.cli.commands;
import org.keycloak.OAuth2Constants; import org.keycloak.client.cli.common.BaseConfigCredentialsCmd;
import org.keycloak.client.registration.cli.config.ConfigData; import org.keycloak.client.registration.cli.KcRegMain;
import org.keycloak.client.registration.cli.config.RealmConfigData;
import org.keycloak.client.registration.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 picocli.CommandLine.Command; import picocli.CommandLine.Command;
import static org.keycloak.client.registration.cli.util.AuthUtil.getAuthTokens;
import static org.keycloak.client.registration.cli.util.AuthUtil.getAuthTokensByJWT;
import static org.keycloak.client.registration.cli.util.AuthUtil.getAuthTokensBySecret;
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
import static org.keycloak.client.registration.cli.util.ConfigUtil.getHandler;
import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig;
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveTokens;
import static org.keycloak.client.registration.cli.util.IoUtil.printErr;
import static org.keycloak.client.registration.cli.util.IoUtil.readSecret;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH;
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/ */
@Command(name = "credentials", description = "--server SERVER_URL --realm REALM [ARGUMENTS]") @Command(name = "credentials", description = "--server SERVER_URL --realm REALM [ARGUMENTS]")
public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd { public class ConfigCredentialsCmd extends BaseConfigCredentialsCmd {
private int sigLifetime = 600; public ConfigCredentialsCmd() {
super(KcRegMain.COMMAND_STATE);
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
protected String[] getUnsupportedOptions() {
return new String[] {"--no-config", booleanOptionForCheck(noconfig)};
}
@Override
protected boolean nothingToDo() {
return noOptions();
}
@Override
protected void process() {
// 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();
String grantTypeForAuthentication = null;
if (user != null) {
grantTypeForAuthentication = OAuth2Constants.PASSWORD;
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: ");
}
// if secret was set to be read from stdin, then ask for it
if ("-".equals(secret) && keystore == null) {
secret = readSecret("Enter client secret: ");
}
} else if (keystore != null || secret != null || clientSet) {
grantTypeForAuthentication = OAuth2Constants.CLIENT_CREDENTIALS;
printErr("Logging into " + server + " as " + "service-account-" + clientId + " of realm " + realm);
if (keystore == null) {
if (secret == null) {
secret = readSecret("Enter client secret: ");
}
}
}
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: ");
keyPass = readSecret("Enter key password: ");
}
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;
}
setupTruststore(copyWithServerInfo(loadConfig()));
// 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, grantTypeForAuthentication);
}
@Override
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 [ARGUMENTS]");
out.println(" " + 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. That is enough to establish unauthenticated session.");
out.println("If --client is not provided it defaults to 'admin-cli'. The authantication options / requirements depend on how this client is configured.");
out.println();
out.println("If you have an account configured with the rights to manage clients then you can use username, and password to authenticate.");
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')");
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 --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 --realm master --user admin");
} else {
out.println(" " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080 --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 --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 --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 --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 --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();
}
} }

View file

@ -1,8 +1,9 @@
package org.keycloak.client.registration.cli.commands; package org.keycloak.client.registration.cli.commands;
import org.keycloak.client.registration.cli.config.RealmConfigData; import org.keycloak.client.cli.config.RealmConfigData;
import org.keycloak.client.registration.cli.util.IoUtil; import org.keycloak.client.cli.util.IoUtil;
import org.keycloak.client.registration.cli.util.ParseUtil; import org.keycloak.client.registration.cli.CmdStdinContext;
import org.keycloak.client.registration.cli.KcRegMain;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
@ -11,12 +12,11 @@ import picocli.CommandLine.Command;
import picocli.CommandLine.Option; import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters; import picocli.CommandLine.Parameters;
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.cli.util.ConfigUtil.saveMergeConfig;
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig; import static org.keycloak.client.cli.util.IoUtil.warnfOut;
import static org.keycloak.client.registration.cli.util.IoUtil.warnfOut; import static org.keycloak.client.cli.util.OsUtil.OS_ARCH;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD; import static org.keycloak.client.cli.util.OsUtil.PROMPT;
import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH; import static org.keycloak.client.registration.cli.KcRegMain.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -34,7 +34,7 @@ public class ConfigInitialTokenCmd extends AbstractAuthOptionsCmd {
@Override @Override
protected boolean nothingToDo() { protected boolean nothingToDo() {
return noOptions() && token == null && !delete && !keepDomain; return super.nothingToDo() && token == null && !delete && !keepDomain;
} }
@Override @Override
@ -60,7 +60,7 @@ public class ConfigInitialTokenCmd extends AbstractAuthOptionsCmd {
} }
if (token != null && token.startsWith("-")) { if (token != null && token.startsWith("-")) {
warnfOut(ParseUtil.TOKEN_OPTION_WARN, token); warnfOut(CmdStdinContext.TOKEN_OPTION_WARN, token);
} }
if (!delete && token == null) { if (!delete && token == null) {
@ -106,7 +106,7 @@ public class ConfigInitialTokenCmd extends AbstractAuthOptionsCmd {
out.println(); out.println();
out.println(" Global options:"); out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error"); 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(" --config Path to the config file (" + KcRegMain.DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(); out.println();
out.println(" Command specific options:"); out.println(" Command specific options:");
out.println(" --server SERVER Server endpoint url (e.g. 'http://localhost:8080')"); out.println(" --server SERVER Server endpoint url (e.g. 'http://localhost:8080')");

View file

@ -1,7 +1,8 @@
package org.keycloak.client.registration.cli.commands; package org.keycloak.client.registration.cli.commands;
import org.keycloak.client.registration.cli.config.RealmConfigData; import org.keycloak.client.registration.cli.KcRegMain;
import org.keycloak.client.registration.cli.util.IoUtil; import org.keycloak.client.cli.config.RealmConfigData;
import org.keycloak.client.cli.util.IoUtil;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
@ -10,11 +11,10 @@ import picocli.CommandLine.Command;
import picocli.CommandLine.Option; import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters; import picocli.CommandLine.Parameters;
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.cli.util.ConfigUtil.saveMergeConfig;
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig; import static org.keycloak.client.cli.util.OsUtil.OS_ARCH;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD; import static org.keycloak.client.cli.util.OsUtil.PROMPT;
import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH; import static org.keycloak.client.registration.cli.KcRegMain.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -30,7 +30,7 @@ public class ConfigRegistrationTokenCmd extends AbstractAuthOptionsCmd {
@Override @Override
protected boolean nothingToDo() { protected boolean nothingToDo() {
return noOptions() && token == null && !delete; return super.nothingToDo() && token == null && !delete;
} }
@Override @Override
@ -97,7 +97,7 @@ public class ConfigRegistrationTokenCmd extends AbstractAuthOptionsCmd {
out.println(); out.println();
out.println(" Global options:"); out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error"); 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(" --config Path to the config file (" + KcRegMain.DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(); out.println();
out.println(" Command specific options:"); out.println(" Command specific options:");
out.println(" --server SERVER Server endpoint url (e.g. 'http://localhost:8080')"); out.println(" --server SERVER Server endpoint url (e.g. 'http://localhost:8080')");

View file

@ -16,131 +16,19 @@
*/ */
package org.keycloak.client.registration.cli.commands; package org.keycloak.client.registration.cli.commands;
import java.io.File; import org.keycloak.client.cli.common.BaseConfigTruststoreCmd;
import java.io.PrintWriter; import org.keycloak.client.registration.cli.KcRegMain;
import java.io.StringWriter;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
import static org.keycloak.client.registration.cli.util.IoUtil.readSecret;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH;
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/ */
@Command(name = "truststore", description = "PATH [ARGUMENTS]") @Command(name = "truststore", description = "PATH [ARGUMENTS]")
public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd { public class ConfigTruststoreCmd extends BaseConfigTruststoreCmd {
@Option(names = {"-d", "--delete"}, description = "Indicates that initial access token should be removed") public ConfigTruststoreCmd() {
private boolean delete; super(KcRegMain.COMMAND_STATE);
@Parameters(arity = "0..1")
private String truststorePath;
@Override
protected boolean nothingToDo() {
return noOptions() && truststorePath == null && !delete;
} }
@Override
protected String[] getUnsupportedOptions() {
return new String[] {"--server", server,
"--realm", realm,
"--client", clientId,
"--user", user,
"--password", password,
"--secret", secret,
"--truststore", trustStore,
"--keystore", keystore,
"--keypass", keyPass,
"--alias", alias,
"--no-config", booleanOptionForCheck(noconfig)};
}
@Override
protected void process() {
// now update the config
String store;
String pass;
if (!delete) {
if (truststorePath == null) {
throw new IllegalArgumentException("No truststore specified");
}
if (!new File(truststorePath).isFile()) {
throw new RuntimeException("Truststore file not found: " + truststorePath);
}
if ("-".equals(trustPass)) {
trustPass = readSecret("Enter truststore password: ");
}
store = truststorePath;
pass = trustPass;
} else {
if (truststorePath != 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);
});
}
@Override
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();
}
} }

View file

@ -17,12 +17,13 @@
package org.keycloak.client.registration.cli.commands; package org.keycloak.client.registration.cli.commands;
import org.keycloak.client.registration.cli.CmdStdinContext;
import org.keycloak.client.registration.cli.EndpointType;
import org.keycloak.client.registration.cli.EndpointTypeConverter; import org.keycloak.client.registration.cli.EndpointTypeConverter;
import org.keycloak.client.registration.cli.common.AttributeOperation; import org.keycloak.client.registration.cli.KcRegMain;
import org.keycloak.client.registration.cli.common.CmdStdinContext; import org.keycloak.client.cli.common.AttributeOperation;
import org.keycloak.client.registration.cli.common.EndpointType; import org.keycloak.client.cli.config.ConfigData;
import org.keycloak.client.registration.cli.config.ConfigData; import org.keycloak.client.cli.util.HttpUtil;
import org.keycloak.client.registration.cli.util.HttpUtil;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation; import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
@ -39,28 +40,23 @@ import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
import picocli.CommandLine.Option; import picocli.CommandLine.Option;
import static org.keycloak.client.registration.cli.common.AttributeOperation.Type.SET; import static org.keycloak.client.cli.common.AttributeOperation.Type.SET;
import static org.keycloak.client.registration.cli.common.EndpointType.DEFAULT; import static org.keycloak.client.cli.util.ConfigUtil.credentialsAvailable;
import static org.keycloak.client.registration.cli.common.EndpointType.OIDC; import static org.keycloak.client.cli.util.ConfigUtil.loadConfig;
import static org.keycloak.client.registration.cli.common.EndpointType.SAML2; import static org.keycloak.client.cli.util.ConfigUtil.saveMergeConfig;
import static org.keycloak.client.registration.cli.util.AuthUtil.ensureToken; import static org.keycloak.client.cli.util.ConfigUtil.setRegistrationToken;
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.cli.util.HttpUtil.doPost;
import static org.keycloak.client.registration.cli.util.ConfigUtil.credentialsAvailable; import static org.keycloak.client.cli.util.IoUtil.printErr;
import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig; import static org.keycloak.client.cli.util.IoUtil.printOut;
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig; import static org.keycloak.client.cli.util.IoUtil.readFully;
import static org.keycloak.client.registration.cli.util.ConfigUtil.setRegistrationToken; import static org.keycloak.client.cli.util.IoUtil.readSecret;
import static org.keycloak.client.registration.cli.util.HttpUtil.doPost; import static org.keycloak.client.cli.util.OsUtil.OS_ARCH;
import static org.keycloak.client.registration.cli.util.HttpUtil.getExpectedContentType; import static org.keycloak.client.cli.util.OsUtil.PROMPT;
import static org.keycloak.client.registration.cli.util.IoUtil.printErr; import static org.keycloak.client.cli.util.ParseUtil.parseKeyVal;
import static org.keycloak.client.registration.cli.util.IoUtil.printOut; import static org.keycloak.client.registration.cli.EndpointType.DEFAULT;
import static org.keycloak.client.registration.cli.util.IoUtil.readFully; import static org.keycloak.client.registration.cli.EndpointType.OIDC;
import static org.keycloak.client.registration.cli.util.IoUtil.readSecret; import static org.keycloak.client.registration.cli.EndpointType.SAML2;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD; import static org.keycloak.client.registration.cli.KcRegMain.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH;
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
import static org.keycloak.client.registration.cli.util.ParseUtil.mergeAttributes;
import static org.keycloak.client.registration.cli.util.ParseUtil.parseFileOrStdin;
import static org.keycloak.client.registration.cli.util.ParseUtil.parseKeyVal;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -109,13 +105,13 @@ public class CreateCmd extends AbstractAuthOptionsCmd {
} }
// if --token is specified read it // if --token is specified read it
if ("-".equals(token)) { if ("-".equals(externalToken)) {
token = readSecret("Enter Initial Access Token: "); externalToken = readSecret("Enter Initial Access Token: ");
} }
CmdStdinContext ctx = new CmdStdinContext(); CmdStdinContext ctx = new CmdStdinContext();
if (file != null) { if (file != null) {
ctx = parseFileOrStdin(file, regType); ctx = CmdStdinContext.parseFileOrStdin(file, regType);
} }
if (ctx.getEndpointType() == null) { if (ctx.getEndpointType() == null) {
@ -126,22 +122,22 @@ public class CreateCmd extends AbstractAuthOptionsCmd {
} }
if (attrs.size() > 0) { if (attrs.size() > 0) {
ctx = mergeAttributes(ctx, attrs); ctx = CmdStdinContext.mergeAttributes(ctx, attrs);
} }
String contentType = getExpectedContentType(ctx.getEndpointType()); String contentType = EndpointType.getExpectedContentType(ctx.getEndpointType());
ConfigData config = loadConfig(); ConfigData config = loadConfig();
config = copyWithServerInfo(config); config = copyWithServerInfo(config);
if (token == null) { if (externalToken == null) {
// if initial token is not set, try use the one from configuration // if initial token is not set, try use the one from configuration
token = config.sessionRealmConfigData().getInitialToken(); externalToken = config.sessionRealmConfigData().getInitialToken();
} }
setupTruststore(config); setupTruststore(config);
String auth = token; String auth = externalToken;
if (auth == null) { if (auth == null) {
config = ensureAuthInfo(config); config = ensureAuthInfo(config);
config = copyWithServerInfo(config); config = copyWithServerInfo(config);
@ -199,7 +195,7 @@ public class CreateCmd extends AbstractAuthOptionsCmd {
@Override @Override
protected boolean nothingToDo() { protected boolean nothingToDo() {
return noOptions() && regType == null && file == null && rawSets.isEmpty(); return super.nothingToDo() && regType == null && file == null && rawSets.isEmpty();
} }
@Override @Override
@ -220,7 +216,7 @@ public class CreateCmd extends AbstractAuthOptionsCmd {
out.println(); out.println();
out.println(" Global options:"); out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error"); 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(" --config Path to the config file (" + KcRegMain.DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --no-config Don't use config file - no authentication info is loaded or saved"); out.println(" --no-config Don't use config file - no authentication info is loaded or saved");
out.println(" --truststore PATH Path to a truststore containing trusted certificates"); 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(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");

View file

@ -17,8 +17,9 @@
package org.keycloak.client.registration.cli.commands; package org.keycloak.client.registration.cli.commands;
import org.keycloak.client.registration.cli.config.ConfigData; import org.keycloak.client.registration.cli.KcRegMain;
import org.keycloak.client.registration.cli.util.ParseUtil; import org.keycloak.client.cli.config.ConfigData;
import org.keycloak.client.registration.cli.CmdStdinContext;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
@ -26,17 +27,15 @@ import java.io.StringWriter;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters; import picocli.CommandLine.Parameters;
import static org.keycloak.client.registration.cli.util.AuthUtil.ensureToken; import static org.keycloak.client.cli.util.ConfigUtil.credentialsAvailable;
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.cli.util.ConfigUtil.getRegistrationToken;
import static org.keycloak.client.registration.cli.util.ConfigUtil.credentialsAvailable; import static org.keycloak.client.cli.util.ConfigUtil.loadConfig;
import static org.keycloak.client.registration.cli.util.ConfigUtil.getRegistrationToken; import static org.keycloak.client.cli.util.ConfigUtil.saveMergeConfig;
import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig; import static org.keycloak.client.cli.util.HttpUtil.doDelete;
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig; import static org.keycloak.client.cli.util.HttpUtil.urlencode;
import static org.keycloak.client.registration.cli.util.HttpUtil.doDelete; import static org.keycloak.client.cli.util.IoUtil.warnfErr;
import static org.keycloak.client.registration.cli.util.HttpUtil.urlencode; import static org.keycloak.client.cli.util.OsUtil.PROMPT;
import static org.keycloak.client.registration.cli.util.IoUtil.warnfErr; import static org.keycloak.client.registration.cli.KcRegMain.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
/** /**
@ -55,7 +54,7 @@ public class DeleteCmd extends AbstractAuthOptionsCmd {
} }
if (clientId.startsWith("-")) { if (clientId.startsWith("-")) {
warnfErr(ParseUtil.CLIENT_OPTION_WARN, clientId); warnfErr(CmdStdinContext.CLIENT_OPTION_WARN, clientId);
} }
String regType = "default"; String regType = "default";
@ -63,14 +62,14 @@ public class DeleteCmd extends AbstractAuthOptionsCmd {
ConfigData config = loadConfig(); ConfigData config = loadConfig();
config = copyWithServerInfo(config); config = copyWithServerInfo(config);
if (token == null) { if (externalToken == null) {
// if registration access token is not set via -t, try use the one from configuration // if registration access token is not set via -t, try use the one from configuration
token = getRegistrationToken(config.sessionRealmConfigData(), clientId); externalToken = getRegistrationToken(config.sessionRealmConfigData(), clientId);
} }
setupTruststore(config); setupTruststore(config);
String auth = token; String auth = externalToken;
if (auth == null) { if (auth == null) {
config = ensureAuthInfo(config); config = ensureAuthInfo(config);
config = copyWithServerInfo(config); config = copyWithServerInfo(config);
@ -94,7 +93,7 @@ public class DeleteCmd extends AbstractAuthOptionsCmd {
@Override @Override
protected boolean nothingToDo() { protected boolean nothingToDo() {
return noOptions() && clientId == null; return super.nothingToDo() && clientId == null;
} }
@Override @Override
@ -114,7 +113,7 @@ public class DeleteCmd extends AbstractAuthOptionsCmd {
out.println(); out.println();
out.println(" Global options:"); out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error"); 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(" --config Path to the config file (" + KcRegMain.DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --no-config Don't use config file - no authentication info is loaded or saved"); out.println(" --no-config Don't use config file - no authentication info is loaded or saved");
out.println(" --truststore PATH Path to a truststore containing trusted certificates"); 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(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");

View file

@ -17,9 +17,10 @@
package org.keycloak.client.registration.cli.commands; package org.keycloak.client.registration.cli.commands;
import org.keycloak.client.registration.cli.config.ConfigData; import org.keycloak.client.registration.cli.KcRegMain;
import org.keycloak.client.registration.cli.common.EndpointType; import org.keycloak.client.cli.config.ConfigData;
import org.keycloak.client.registration.cli.util.ParseUtil; import org.keycloak.client.registration.cli.CmdStdinContext;
import org.keycloak.client.registration.cli.EndpointType;
import org.keycloak.representations.adapters.config.AdapterConfig; import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation; import org.keycloak.representations.oidc.OIDCClientRepresentation;
@ -34,21 +35,19 @@ import picocli.CommandLine.Command;
import picocli.CommandLine.Option; import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters; import picocli.CommandLine.Parameters;
import static org.keycloak.client.registration.cli.util.AuthUtil.ensureToken; import static org.keycloak.client.cli.util.ConfigUtil.credentialsAvailable;
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.cli.util.ConfigUtil.getRegistrationToken;
import static org.keycloak.client.registration.cli.util.ConfigUtil.credentialsAvailable; import static org.keycloak.client.cli.util.ConfigUtil.loadConfig;
import static org.keycloak.client.registration.cli.util.ConfigUtil.getRegistrationToken; import static org.keycloak.client.cli.util.ConfigUtil.saveMergeConfig;
import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig; import static org.keycloak.client.cli.util.ConfigUtil.setRegistrationToken;
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig; import static org.keycloak.client.cli.util.HttpUtil.APPLICATION_JSON;
import static org.keycloak.client.registration.cli.util.ConfigUtil.setRegistrationToken; import static org.keycloak.client.cli.util.HttpUtil.doGet;
import static org.keycloak.client.registration.cli.util.HttpUtil.APPLICATION_JSON; import static org.keycloak.client.cli.util.HttpUtil.urlencode;
import static org.keycloak.client.registration.cli.util.HttpUtil.doGet; import static org.keycloak.client.cli.util.IoUtil.printOut;
import static org.keycloak.client.registration.cli.util.HttpUtil.urlencode; import static org.keycloak.client.cli.util.IoUtil.readFully;
import static org.keycloak.client.registration.cli.util.IoUtil.warnfErr; import static org.keycloak.client.cli.util.IoUtil.warnfErr;
import static org.keycloak.client.registration.cli.util.IoUtil.printOut; import static org.keycloak.client.cli.util.OsUtil.PROMPT;
import static org.keycloak.client.registration.cli.util.IoUtil.readFully; import static org.keycloak.client.registration.cli.KcRegMain.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -75,20 +74,20 @@ public class GetCmd extends AbstractAuthOptionsCmd {
if (clientId.startsWith("-")) { if (clientId.startsWith("-")) {
warnfErr(ParseUtil.CLIENT_OPTION_WARN, clientId); warnfErr(CmdStdinContext.CLIENT_OPTION_WARN, clientId);
} }
ConfigData config = loadConfig(); ConfigData config = loadConfig();
config = copyWithServerInfo(config); config = copyWithServerInfo(config);
if (token == null) { if (externalToken == null) {
// if registration access token is not set via -t, try use the one from configuration // if registration access token is not set via -t, try use the one from configuration
token = getRegistrationToken(config.sessionRealmConfigData(), clientId); externalToken = getRegistrationToken(config.sessionRealmConfigData(), clientId);
} }
setupTruststore(config); setupTruststore(config);
String auth = token; String auth = externalToken;
if (auth == null) { if (auth == null) {
config = ensureAuthInfo(config); config = ensureAuthInfo(config);
config = copyWithServerInfo(config); config = copyWithServerInfo(config);
@ -156,7 +155,7 @@ public class GetCmd extends AbstractAuthOptionsCmd {
@Override @Override
protected boolean nothingToDo() { protected boolean nothingToDo() {
return noOptions() && endpoint == null && clientId == null; return super.nothingToDo() && endpoint == null && clientId == null;
} }
@Override @Override
@ -176,7 +175,7 @@ public class GetCmd extends AbstractAuthOptionsCmd {
out.println(); out.println();
out.println(" Global options:"); out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error"); 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(" --config Path to the config file (" + KcRegMain.DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --no-config Don't use config file - no authentication info is loaded or saved"); out.println(" --no-config Don't use config file - no authentication info is loaded or saved");
out.println(" --truststore PATH Path to a truststore containing trusted certificates"); 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(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");

View file

@ -5,7 +5,7 @@ import java.util.List;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters; import picocli.CommandLine.Parameters;
import static org.keycloak.client.registration.cli.util.IoUtil.printOut; import static org.keycloak.client.cli.util.IoUtil.printOut;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -27,7 +27,7 @@ public class HelpCmd implements Runnable {
if (args.size() > 1) { if (args.size() > 1) {
switch (args.get(1)) { switch (args.get(1)) {
case "credentials": { case "credentials": {
printOut(ConfigCredentialsCmd.usage()); printOut(new ConfigCredentialsCmd().help());
break outer; break outer;
} }
case "initial-token": { case "initial-token": {
@ -39,7 +39,7 @@ public class HelpCmd implements Runnable {
break outer; break outer;
} }
case "truststore": { case "truststore": {
printOut(ConfigTruststoreCmd.usage()); printOut(new ConfigTruststoreCmd().help());
break outer; break outer;
} }
} }

View file

@ -16,15 +16,16 @@
*/ */
package org.keycloak.client.registration.cli.commands; package org.keycloak.client.registration.cli.commands;
import org.keycloak.client.cli.common.BaseGlobalOptionsCmd;
import org.keycloak.client.registration.cli.KcRegMain;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.cli.util.OsUtil.PROMPT;
import static org.keycloak.client.registration.cli.util.IoUtil.printOut; import static org.keycloak.client.registration.cli.KcRegMain.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -49,13 +50,18 @@ subcommands = {
AttrsCmd.class, AttrsCmd.class,
UpdateTokenCmd.class UpdateTokenCmd.class
}) })
public class KcRegCmd extends AbstractGlobalOptionsCmd { public class KcRegCmd extends BaseGlobalOptionsCmd {
@Override @Override
protected boolean nothingToDo() { protected boolean nothingToDo() {
return true; return true;
} }
@Override
protected String help() {
return usage();
}
public static String usage() { public static String usage() {
StringWriter sb = new StringWriter(); StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb); PrintWriter out = new PrintWriter(sb);
@ -93,7 +99,7 @@ public class KcRegCmd extends AbstractGlobalOptionsCmd {
out.println("Global options:"); out.println("Global options:");
out.println(" -x Print full stack trace when exiting with error"); out.println(" -x Print full stack trace when exiting with error");
out.println(" --help Print help for specific command"); out.println(" --help Print help for specific command");
out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)"); out.println(" --config Path to the config file (" + KcRegMain.DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --no-config Don't use config file - no authentication info is loaded or saved"); out.println(" --no-config Don't use config file - no authentication info is loaded or saved");
out.println(); out.println();
out.println("Commands: "); out.println("Commands: ");

View file

@ -24,13 +24,13 @@ import picocli.CommandLine.Command;
import picocli.CommandLine.Option; import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters; import picocli.CommandLine.Parameters;
import org.keycloak.client.registration.cli.CmdStdinContext;
import org.keycloak.client.registration.cli.EndpointType;
import org.keycloak.client.registration.cli.EndpointTypeConverter; import org.keycloak.client.registration.cli.EndpointTypeConverter;
import org.keycloak.client.registration.cli.common.AttributeOperation; import org.keycloak.client.registration.cli.ReflectionUtil;
import org.keycloak.client.registration.cli.config.ConfigData; import org.keycloak.client.registration.cli.KcRegMain;
import org.keycloak.client.registration.cli.common.CmdStdinContext; import org.keycloak.client.cli.common.AttributeOperation;
import org.keycloak.client.registration.cli.common.EndpointType; import org.keycloak.client.cli.config.ConfigData;
import org.keycloak.client.registration.cli.util.ParseUtil;
import org.keycloak.client.registration.cli.util.ReflectionUtil;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation; import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
@ -43,29 +43,25 @@ import java.util.ArrayList;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import static org.keycloak.client.registration.cli.common.AttributeOperation.Type.DELETE; import static org.keycloak.client.cli.common.AttributeOperation.Type.DELETE;
import static org.keycloak.client.registration.cli.common.AttributeOperation.Type.SET; import static org.keycloak.client.cli.common.AttributeOperation.Type.SET;
import static org.keycloak.client.registration.cli.util.AuthUtil.ensureToken; import static org.keycloak.client.cli.util.ConfigUtil.credentialsAvailable;
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.cli.util.ConfigUtil.getRegistrationToken;
import static org.keycloak.client.registration.cli.util.ConfigUtil.credentialsAvailable; import static org.keycloak.client.cli.util.ConfigUtil.loadConfig;
import static org.keycloak.client.registration.cli.util.ConfigUtil.getRegistrationToken; import static org.keycloak.client.cli.util.ConfigUtil.saveMergeConfig;
import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig; import static org.keycloak.client.cli.util.ConfigUtil.setRegistrationToken;
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig; import static org.keycloak.client.cli.util.HttpUtil.APPLICATION_JSON;
import static org.keycloak.client.registration.cli.util.ConfigUtil.setRegistrationToken; import static org.keycloak.client.cli.util.HttpUtil.doGet;
import static org.keycloak.client.registration.cli.common.EndpointType.DEFAULT; import static org.keycloak.client.cli.util.HttpUtil.doPut;
import static org.keycloak.client.registration.cli.common.EndpointType.OIDC; import static org.keycloak.client.cli.util.HttpUtil.urlencode;
import static org.keycloak.client.registration.cli.util.HttpUtil.doGet; import static org.keycloak.client.cli.util.IoUtil.printOut;
import static org.keycloak.client.registration.cli.util.HttpUtil.doPut; import static org.keycloak.client.cli.util.IoUtil.readFully;
import static org.keycloak.client.registration.cli.util.HttpUtil.urlencode; import static org.keycloak.client.cli.util.IoUtil.warnfErr;
import static org.keycloak.client.registration.cli.util.IoUtil.printOut; import static org.keycloak.client.cli.util.OsUtil.PROMPT;
import static org.keycloak.client.registration.cli.util.IoUtil.warnfErr; import static org.keycloak.client.cli.util.ParseUtil.parseKeyVal;
import static org.keycloak.client.registration.cli.util.IoUtil.readFully; import static org.keycloak.client.registration.cli.EndpointType.DEFAULT;
import static org.keycloak.client.registration.cli.util.HttpUtil.APPLICATION_JSON; import static org.keycloak.client.registration.cli.EndpointType.OIDC;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD; import static org.keycloak.client.registration.cli.KcRegMain.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
import static org.keycloak.client.registration.cli.util.ParseUtil.mergeAttributes;
import static org.keycloak.client.registration.cli.util.ParseUtil.parseFileOrStdin;
import static org.keycloak.client.registration.cli.util.ParseUtil.parseKeyVal;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -123,7 +119,7 @@ public class UpdateCmd extends AbstractAuthOptionsCmd {
} }
if (clientId.startsWith("-")) { if (clientId.startsWith("-")) {
warnfErr(ParseUtil.CLIENT_OPTION_WARN, clientId); warnfErr(CmdStdinContext.CLIENT_OPTION_WARN, clientId);
} }
if (file == null && attrs.size() == 0) { if (file == null && attrs.size() == 0) {
@ -165,7 +161,7 @@ public class UpdateCmd extends AbstractAuthOptionsCmd {
CmdStdinContext ctx = new CmdStdinContext(); CmdStdinContext ctx = new CmdStdinContext();
if (file != null) { if (file != null) {
ctx = parseFileOrStdin(file, regType); ctx = CmdStdinContext.parseFileOrStdin(file, regType);
regType = ctx.getEndpointType(); regType = ctx.getEndpointType();
} }
@ -185,7 +181,7 @@ public class UpdateCmd extends AbstractAuthOptionsCmd {
final String server = config.getServerUrl(); final String server = config.getServerUrl();
final String realm = config.getRealm(); final String realm = config.getRealm();
if (token == null) { if (externalToken == null) {
// if registration access token is not set via --token, see if it's in the body of any input file // if registration access token is not set via --token, see if it's in the body of any input file
// but first see if it's overridden by --set, or maybe deliberately muted via -d registrationAccessToken // but first see if it's overridden by --set, or maybe deliberately muted via -d registrationAccessToken
boolean processed = false; boolean processed = false;
@ -193,25 +189,25 @@ public class UpdateCmd extends AbstractAuthOptionsCmd {
if ("registrationAccessToken".equals(op.getKey().toString())) { if ("registrationAccessToken".equals(op.getKey().toString())) {
processed = true; processed = true;
if (op.getType() == AttributeOperation.Type.SET) { if (op.getType() == AttributeOperation.Type.SET) {
token = op.getValue(); externalToken = op.getValue();
} }
// otherwise it's delete - meaning it should stay null // otherwise it's delete - meaning it should stay null
break; break;
} }
} }
if (!processed) { if (!processed) {
token = ctx.getRegistrationAccessToken(); externalToken = ctx.getRegistrationAccessToken();
} }
} }
if (token == null) { if (externalToken == null) {
// if registration access token is not set, try use the one from configuration // if registration access token is not set, try use the one from configuration
token = getRegistrationToken(config.sessionRealmConfigData(), clientId); externalToken = getRegistrationToken(config.sessionRealmConfigData(), clientId);
} }
setupTruststore(config); setupTruststore(config);
String auth = token; String auth = externalToken;
if (auth == null) { if (auth == null) {
config = ensureAuthInfo(config); config = ensureAuthInfo(config);
config = copyWithServerInfo(config); config = copyWithServerInfo(config);
@ -236,10 +232,10 @@ public class UpdateCmd extends AbstractAuthOptionsCmd {
if (regType == DEFAULT) { if (regType == DEFAULT) {
ctxremote.setClient(JsonSerialization.readValue(json, ClientRepresentation.class)); ctxremote.setClient(JsonSerialization.readValue(json, ClientRepresentation.class));
token = ctxremote.getClient().getRegistrationAccessToken(); externalToken = ctxremote.getClient().getRegistrationAccessToken();
} else if (regType == OIDC) { } else if (regType == OIDC) {
ctxremote.setOidcClient(JsonSerialization.readValue(json, OIDCClientRepresentation.class)); ctxremote.setOidcClient(JsonSerialization.readValue(json, OIDCClientRepresentation.class));
token = ctxremote.getOidcClient().getRegistrationAccessToken(); externalToken = ctxremote.getOidcClient().getRegistrationAccessToken();
} }
} catch (JsonParseException e) { } catch (JsonParseException e) {
throw new RuntimeException("Not a valid JSON document. " + e.getMessage(), e); throw new RuntimeException("Not a valid JSON document. " + e.getMessage(), e);
@ -249,11 +245,11 @@ public class UpdateCmd extends AbstractAuthOptionsCmd {
// we have to use registration access token retrieved from previous operation // we have to use registration access token retrieved from previous operation
// that ensures optimistic locking semantics // that ensures optimistic locking semantics
if (token != null) { if (externalToken != null) {
// we use auth with doPost later // we use auth with doPost later
auth = "Bearer " + token; auth = "Bearer " + externalToken;
String newToken = token; String newToken = externalToken;
String clientToUpdate = clientId; String clientToUpdate = clientId;
saveMergeConfig(cfg -> { saveMergeConfig(cfg -> {
setRegistrationToken(cfg.ensureRealmConfigData(server, realm), clientToUpdate, newToken); setRegistrationToken(cfg.ensureRealmConfigData(server, realm), clientToUpdate, newToken);
@ -270,7 +266,7 @@ public class UpdateCmd extends AbstractAuthOptionsCmd {
} }
if (attrs.size() > 0) { if (attrs.size() > 0) {
ctx = mergeAttributes(ctx, attrs); ctx = CmdStdinContext.mergeAttributes(ctx, attrs);
} }
// now update // now update
@ -280,14 +276,14 @@ public class UpdateCmd extends AbstractAuthOptionsCmd {
if (regType == DEFAULT) { if (regType == DEFAULT) {
ClientRepresentation clirep = JsonSerialization.readValue(response, ClientRepresentation.class); ClientRepresentation clirep = JsonSerialization.readValue(response, ClientRepresentation.class);
outputResult(clirep); outputResult(clirep);
token = clirep.getRegistrationAccessToken(); externalToken = clirep.getRegistrationAccessToken();
} else if (regType == OIDC) { } else if (regType == OIDC) {
OIDCClientRepresentation clirep = JsonSerialization.readValue(response, OIDCClientRepresentation.class); OIDCClientRepresentation clirep = JsonSerialization.readValue(response, OIDCClientRepresentation.class);
outputResult(clirep); outputResult(clirep);
token = clirep.getRegistrationAccessToken(); externalToken = clirep.getRegistrationAccessToken();
} }
String newToken = token; String newToken = externalToken;
String clientToUpdate = clientId; String clientToUpdate = clientId;
saveMergeConfig(cfg -> { saveMergeConfig(cfg -> {
setRegistrationToken(cfg.ensureRealmConfigData(server, realm), clientToUpdate, newToken); setRegistrationToken(cfg.ensureRealmConfigData(server, realm), clientToUpdate, newToken);
@ -310,7 +306,7 @@ public class UpdateCmd extends AbstractAuthOptionsCmd {
@Override @Override
protected boolean nothingToDo() { protected boolean nothingToDo() {
return noOptions() && regType == null && file == null && rawAttributeOperations.isEmpty() && clientId == null; return super.nothingToDo() && regType == null && file == null && rawAttributeOperations.isEmpty() && clientId == null;
} }
@Override @Override
@ -332,7 +328,7 @@ public class UpdateCmd extends AbstractAuthOptionsCmd {
out.println(); out.println();
out.println(" Global options:"); out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error"); 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(" --config Path to the config file (" + KcRegMain.DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --no-config Don't use config file - no authentication info is loaded or saved"); out.println(" --no-config Don't use config file - no authentication info is loaded or saved");
out.println(" --truststore PATH Path to a truststore containing trusted certificates"); 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(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");

View file

@ -22,8 +22,9 @@ import com.fasterxml.jackson.core.type.TypeReference;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters; import picocli.CommandLine.Parameters;
import org.keycloak.client.registration.cli.config.ConfigData; import org.keycloak.client.registration.cli.KcRegMain;
import org.keycloak.client.registration.cli.util.ParseUtil; import org.keycloak.client.cli.config.ConfigData;
import org.keycloak.client.registration.cli.CmdStdinContext;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
@ -33,18 +34,16 @@ import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import java.util.List; import java.util.List;
import static org.keycloak.client.registration.cli.util.AuthUtil.ensureToken; import static org.keycloak.client.cli.util.ConfigUtil.loadConfig;
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.cli.util.ConfigUtil.saveMergeConfig;
import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig; import static org.keycloak.client.cli.util.ConfigUtil.setRegistrationToken;
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig; import static org.keycloak.client.cli.util.HttpUtil.APPLICATION_JSON;
import static org.keycloak.client.registration.cli.util.ConfigUtil.setRegistrationToken; import static org.keycloak.client.cli.util.HttpUtil.doGet;
import static org.keycloak.client.registration.cli.util.HttpUtil.APPLICATION_JSON; import static org.keycloak.client.cli.util.HttpUtil.doPost;
import static org.keycloak.client.registration.cli.util.HttpUtil.doGet; import static org.keycloak.client.cli.util.IoUtil.printOut;
import static org.keycloak.client.registration.cli.util.HttpUtil.doPost; import static org.keycloak.client.cli.util.IoUtil.warnfOut;
import static org.keycloak.client.registration.cli.util.IoUtil.printOut; import static org.keycloak.client.cli.util.OsUtil.PROMPT;
import static org.keycloak.client.registration.cli.util.IoUtil.warnfOut; import static org.keycloak.client.registration.cli.KcRegMain.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -62,7 +61,7 @@ public class UpdateTokenCmd extends AbstractAuthOptionsCmd {
} }
if (clientId.startsWith("-")) { if (clientId.startsWith("-")) {
warnfOut(ParseUtil.CLIENT_OPTION_WARN, clientId); warnfOut(CmdStdinContext.CLIENT_OPTION_WARN, clientId);
} }
ConfigData config = loadConfig(); ConfigData config = loadConfig();
@ -116,7 +115,7 @@ public class UpdateTokenCmd extends AbstractAuthOptionsCmd {
@Override @Override
protected boolean nothingToDo() { protected boolean nothingToDo() {
return noOptions() && clientId == null; return super.nothingToDo() && clientId == null;
} }
@Override @Override
@ -136,7 +135,7 @@ public class UpdateTokenCmd extends AbstractAuthOptionsCmd {
out.println(); out.println();
out.println(" Global options:"); out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error"); 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(" --config Path to the config file (" + KcRegMain.DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --no-config Don't use config file - no authentication info is loaded or saved"); out.println(" --no-config Don't use config file - no authentication info is loaded or saved");
out.println(" --truststore PATH Path to a truststore containing trusted certificates"); 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(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");

View file

@ -1,154 +0,0 @@
package org.keycloak.client.registration.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 + "]" : "");
}
}
}

View file

@ -1,42 +0,0 @@
package org.keycloak.client.registration.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
}
}

View file

@ -1,75 +0,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.
*/
package org.keycloak.client.registration.cli.common;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class CmdStdinContext {
private EndpointType regType;
private ClientRepresentation client;
private OIDCClientRepresentation oidcClient;
private String content;
public CmdStdinContext() {}
public EndpointType getEndpointType() {
return regType;
}
public void setEndpointType(EndpointType regType) {
this.regType = regType;
}
public ClientRepresentation getClient() {
return client;
}
public void setClient(ClientRepresentation client) {
this.client = client;
}
public OIDCClientRepresentation getOidcClient() {
return oidcClient;
}
public void setOidcClient(OIDCClientRepresentation oidcClient) {
this.oidcClient = oidcClient;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getRegistrationAccessToken() {
if (client != null) {
return client.getRegistrationAccessToken();
} else if (oidcClient != null) {
return oidcClient.getRegistrationAccessToken();
}
return null;
}
}

View file

@ -1,113 +0,0 @@
package org.keycloak.client.registration.cli.common;
/**
* An iterator wrapping command line
*
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class ParsingContext {
private int offset;
private int pos = -1;
private String [] args;
public ParsingContext(String [] args) {
this(args, 0, -1);
}
public ParsingContext(String [] args, int offset) {
this(args, offset, -1);
}
public ParsingContext(String [] args, int offset, int pos) {
this.args = args.clone();
this.offset = offset;
this.pos = pos;
}
public boolean hasNext() {
return pos < args.length-1;
}
public boolean hasNext(int count) {
return pos < args.length - count;
}
public boolean hasPrevious() {
return pos > 0;
}
/**
* Get next argument
*
* @return Next argument or null if beyond the end of arguments
*/
public String next() {
if (hasNext()) {
return args[++pos];
} else {
pos = args.length;
return null;
}
}
/**
* Check that a next argument is available
*
* @return Next argument or RuntimeException if next argument is not available
*/
public String nextRequired() {
if (!hasNext()) {
throw new RuntimeException("Option " + current() + " requires a value");
}
return next();
}
/**
* Get next n-th argument
*
* @return Next n-th argument or null if beyond the end of arguments
*/
public String next(int n) {
if (hasNext(n)) {
pos += n;
return args[pos];
} else {
pos = args.length;
return null;
}
}
/**
* Get previous argument
*
* @return Previous argument or null if previous call was at the beginning of the arguments (pos == 0)
*/
public String previous() {
if (hasPrevious()) {
return args[--pos];
} else {
pos = -1;
return null;
}
}
/**
* Get current argument
*
* @return Current argument or null if current parsing position is beyond end, or before start
*/
public String current() {
if (pos < 0 || pos >= args.length) {
return null;
} else {
return args[pos];
}
}
public String [] getArgs() {
return args;
}
}

View file

@ -1,177 +0,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.
*/
package org.keycloak.client.registration.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();
}
}
}

View file

@ -1,12 +0,0 @@
package org.keycloak.client.registration.cli.config;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public interface ConfigHandler {
void saveMergeConfig(ConfigUpdateOperation op);
ConfigData loadConfig();
}

View file

@ -1,10 +0,0 @@
package org.keycloak.client.registration.cli.config;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public interface ConfigUpdateOperation {
void update(ConfigData data);
}

View file

@ -1,119 +0,0 @@
package org.keycloak.client.registration.cli.config;
import org.keycloak.client.registration.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.registration.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);
}
}
}

View file

@ -1,24 +0,0 @@
package org.keycloak.client.registration.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;
}
}

View file

@ -1,23 +0,0 @@
package org.keycloak.client.registration.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;
}
}

View file

@ -1,214 +0,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.
*/
package org.keycloak.client.registration.cli.util;
import org.keycloak.client.registration.cli.config.ConfigData;
import org.keycloak.client.registration.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.registration.cli.util.ConfigUtil.checkAuthInfo;
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
import static org.keycloak.client.registration.cli.util.HttpUtil.APPLICATION_FORM_URL_ENCODED;
import static org.keycloak.client.registration.cli.util.HttpUtil.APPLICATION_JSON;
import static org.keycloak.client.registration.cli.util.HttpUtil.doPost;
import static org.keycloak.client.registration.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() != null && 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();
if (realmConfig.getRefreshToken() != null) {
body.append("grant_type=refresh_token")
.append("&refresh_token=").append(realmConfig.getRefreshToken());
} else {
body.append("grant_type=").append(realmConfig.getGrantTypeForAuthentication());
}
body.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);
if (token.getRefreshToken() != null) {
realmData.setRefreshExpiresAt(currentTimeMillis() + token.getRefreshExpiresIn() * 1000);
}
});
return token.getToken();
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Unexpected error", e);
} catch (IOException e) {
throw new RuntimeException("Failed to read Refresh Token response", 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) {
KeystoreUtil.KeystoreFormat keystoreType = Enum.valueOf(KeystoreUtil.KeystoreFormat.class, KeystoreUtil.getKeystoreType(null, keystore, KeystoreUtil.KeystoreFormat.JKS.toString()));
KeyPair keypair = KeystoreUtil.loadKeyPairFromKeystore(keystore, storePass, keyPass, alias, keystoreType);
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;
}
}

View file

@ -1,60 +0,0 @@
/*
* Copyright 2022 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.util;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.stream.Stream;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClassLoaderUtil {
/**
* Detect if BC FIPS jars are present in the given directory. Return classloader with appropriate JARS based on that
*/
public static ClassLoader resolveClassLoader(String libDir) {
File[] jarsInDir = new File(libDir).listFiles(file -> file.getName().endsWith(".jar"));
// Detect if BC FIPS jars are present in the "client/lib" directory
boolean bcFipsJarPresent = Stream.of(jarsInDir).anyMatch(file -> file.getName().startsWith("bc-fips"));
String[] validJarPrefixes = bcFipsJarPresent ? new String[] {"keycloak-crypto-fips1402", "bc-fips", "bctls-fips"} : new String[] {"keycloak-crypto-default", "bcprov-jdk18on"};
URL[] usedJars = Stream.of(jarsInDir)
.filter(file -> {
for (String prefix : validJarPrefixes) {
if (file.getName().startsWith(prefix + "-")) return true;
}
return false;
})
.map(file -> {
try {
return file.toURI().toURL();
} catch (MalformedURLException ex) {
throw new IllegalStateException("Error when converting file into URL. Please check the files in the directory " + jarsInDir, ex);
}
}).toArray(URL[]::new);
return new URLClassLoader(usedJars, ClassLoaderUtil.class.getClassLoader());
}
}

View file

@ -1,122 +0,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.
*/
package org.keycloak.client.registration.cli.util;
import org.keycloak.OAuth2Constants;
import org.keycloak.client.registration.cli.config.ConfigData;
import org.keycloak.client.registration.cli.config.ConfigHandler;
import org.keycloak.client.registration.cli.config.ConfigUpdateOperation;
import org.keycloak.client.registration.cli.config.InMemoryConfigHandler;
import org.keycloak.client.registration.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_CONFIG_FILE_STRING = OsUtil.OS_ARCH.isWindows() ? "%HOMEDRIVE%%HOMEPATH%\\.keycloak\\kcreg.config" : "~/.keycloak/kcreg.config";
public static final String DEFAULT_CONFIG_FILE_PATH = System.getProperty("user.home") + "/.keycloak/kcreg.config";
private static ConfigHandler handler;
public static ConfigHandler getHandler() {
return handler;
}
public static void setHandler(ConfigHandler handler) {
ConfigUtil.handler = handler;
}
public static String getRegistrationToken(RealmConfigData data, String clientId) {
String token = data.getClients().get(clientId);
return token == null || token.length() == 0 ? null : token;
}
public static void setRegistrationToken(RealmConfigData data, String clientId, String token) {
data.getClients().put(clientId, token == null ? "" : token);
}
public static void saveTokens(AccessTokenResponse tokens, String endpoint, String realm, String clientId, String signKey, Long sigExpiresAt, String secret,
String grantTypeForAuthentication) {
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);
if (realmConfig.getRefreshToken() != null) {
realmConfig.setRefreshExpiresAt(tokens.getRefreshExpiresIn() == 0 ?
Long.MAX_VALUE : System.currentTimeMillis() + tokens.getRefreshExpiresIn() * 1000);
}
realmConfig.setSigExpiresAt(sigExpiresAt);
realmConfig.setClientId(clientId);
realmConfig.setGrantTypeForAuthentication(grantTypeForAuthentication);
});
}
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) {
// Just supporting "client_credentials" grant type for the case when refresh token is missing
boolean credsAvailable = config.getServerUrl() != null && config.getRealm() != null
&& config.sessionRealmConfigData() != null &&
(config.sessionRealmConfigData().getRefreshToken() != null || (config.sessionRealmConfigData().getToken() != null && OAuth2Constants.CLIENT_CREDENTIALS.equals(config.sessionRealmConfigData().getGrantTypeForAuthentication())));
return credsAvailable;
}
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);
}
}

Some files were not shown because too many files have changed in this diff Show more