diff --git a/docs/documentation/upgrading/topics/changes/changes-25_0_0.adoc b/docs/documentation/upgrading/topics/changes/changes-25_0_0.adoc index 642bb29028..32f5b93e31 100644 --- a/docs/documentation/upgrading/topics/changes/changes-25_0_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-25_0_0.adoc @@ -69,9 +69,9 @@ Options `cache`, `cache-stack`, and `cache-config-file` are no longer build opti This eliminates the need to execute the build phase and rebuild your image due to them. Be aware that they will not be recognized during the `build` phase, so you need to remove them. -= kcadm Changes += kcadm and kcreg changes -How kcadm parses and handles options and parameters has changed. Error messages from usage errors, the wrong option or parameter, may be slightly different than previous versions. Also usage errors will have an exit code of 2 instead of 1. +How kcadm and kcreg parse and handle options and parameters has changed. Error messages from usage errors, the wrong option or parameter, may be slightly different than previous versions. Also usage errors will have an exit code of 2 instead of 1. = Removing custom user attribute indexes diff --git a/integration/client-cli/client-registration-cli/pom.xml b/integration/client-cli/client-registration-cli/pom.xml index 2072efa6c7..8b85db7a73 100755 --- a/integration/client-cli/client-registration-cli/pom.xml +++ b/integration/client-cli/client-registration-cli/pom.xml @@ -31,8 +31,12 @@ - org.jboss.aesh - aesh + info.picocli + picocli + + + org.keycloak + keycloak-admin-cli org.keycloak diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/EndpointTypeConverter.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/EndpointTypeConverter.java new file mode 100644 index 0000000000..80015edd23 --- /dev/null +++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/EndpointTypeConverter.java @@ -0,0 +1,17 @@ +package org.keycloak.client.registration.cli; + +import org.keycloak.client.registration.cli.common.EndpointType; + +import picocli.CommandLine.ITypeConverter; + +/** + * @author Marko Strukelj + */ +public class EndpointTypeConverter implements ITypeConverter { + + @Override + public EndpointType convert(String value) throws Exception { + return EndpointType.of(value); + } + +} diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/Globals.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/Globals.java new file mode 100644 index 0000000000..d266c7a953 --- /dev/null +++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/Globals.java @@ -0,0 +1,12 @@ +package org.keycloak.client.registration.cli; + +/** + * @author Marko Strukelj + */ +public class Globals { + + public static boolean dumpTrace = false; + + public static boolean help = false; + +} diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/KcRegMain.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/KcRegMain.java index 5e42799144..90330bf596 100644 --- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/KcRegMain.java +++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/KcRegMain.java @@ -1,21 +1,16 @@ package org.keycloak.client.registration.cli; -import org.jboss.aesh.console.AeshConsoleBuilder; -import org.jboss.aesh.console.AeshConsoleImpl; -import org.jboss.aesh.console.Prompt; -import org.jboss.aesh.console.command.registry.AeshCommandRegistryBuilder; -import org.jboss.aesh.console.command.registry.CommandRegistry; -import org.jboss.aesh.console.settings.Settings; -import org.jboss.aesh.console.settings.SettingsBuilder; -import org.keycloak.client.registration.cli.aesh.AeshEnhancer; -import org.keycloak.client.registration.cli.aesh.ValveInputStream; -import org.keycloak.client.registration.cli.aesh.Globals; +import org.keycloak.client.admin.cli.ExecutionExceptionHandler; +import org.keycloak.client.admin.cli.ShortErrorMessageHandler; 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.util.ArrayList; -import java.util.Arrays; +import java.io.PrintWriter; + +import picocli.CommandLine; +import picocli.CommandLine.Model.CommandSpec; /** * @author Marko Strukelj @@ -32,54 +27,20 @@ public class KcRegMain { CryptoIntegration.init(cl); - Globals.stdin = new ValveInputStream(); + CommandLine cli = createCommandLine(); + int exitCode = cli.execute(args); + System.exit(exitCode); + } - Settings settings = new SettingsBuilder() - .logging(false) - .readInputrc(false) - .disableCompletion(true) - .disableHistory(true) - .enableAlias(false) - .enableExport(false) - .inputStream(Globals.stdin) - .create(); + public static CommandLine createCommandLine() { + CommandSpec spec = CommandSpec.forAnnotatedObject(new KcRegCmd()).name(OsUtil.CMD); - CommandRegistry registry = new AeshCommandRegistryBuilder() - .command(KcRegCmd.class) - .create(); + CommandLine cmd = new CommandLine(spec); - AeshConsoleImpl console = (AeshConsoleImpl) new AeshConsoleBuilder() - .settings(settings) - .commandRegistry(registry) - .prompt(new Prompt("")) - .create(); + cmd.setExecutionExceptionHandler(new ExecutionExceptionHandler()); + cmd.setParameterExceptionHandler(new ShortErrorMessageHandler()); + cmd.setErr(new PrintWriter(System.err, true)); - AeshEnhancer.enhance(console); - - // work around parser issues with quotes and brackets - ArrayList arguments = new ArrayList<>(); - arguments.add("kcreg"); - arguments.addAll(Arrays.asList(args)); - Globals.args = arguments; - - StringBuilder b = new StringBuilder(); - for (String s : args) { - // quote if necessary - boolean needQuote = false; - needQuote = s.indexOf(' ') != -1 || s.indexOf('\"') != -1 || s.indexOf('\'') != -1; - b.append(' '); - if (needQuote) { - b.append('\''); - } - b.append(s); - if (needQuote) { - b.append('\''); - } - } - console.setEcho(false); - - console.execute("kcreg" + b.toString()); - - console.start(); + return cmd; } } diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/AeshConsoleCallbackImpl.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/AeshConsoleCallbackImpl.java deleted file mode 100644 index 608c26d0e1..0000000000 --- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/AeshConsoleCallbackImpl.java +++ /dev/null @@ -1,101 +0,0 @@ -package org.keycloak.client.registration.cli.aesh; - -import org.jboss.aesh.cl.parser.OptionParserException; -import org.jboss.aesh.cl.result.ResultHandler; -import org.jboss.aesh.console.AeshConsoleCallback; -import org.jboss.aesh.console.AeshConsoleImpl; -import org.jboss.aesh.console.ConsoleOperation; -import org.jboss.aesh.console.command.CommandNotFoundException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.container.CommandContainer; -import org.jboss.aesh.console.command.container.CommandContainerResult; -import org.jboss.aesh.console.command.invocation.AeshCommandInvocation; -import org.jboss.aesh.console.command.invocation.AeshCommandInvocationProvider; -import org.jboss.aesh.parser.AeshLine; -import org.jboss.aesh.parser.ParserStatus; - -import java.lang.reflect.Method; - -class AeshConsoleCallbackImpl extends AeshConsoleCallback { - - private final AeshConsoleImpl console; - private CommandResult result; - - AeshConsoleCallbackImpl(AeshConsoleImpl aeshConsole) { - this.console = aeshConsole; - } - - @Override - @SuppressWarnings("unchecked") - public int execute(ConsoleOperation output) throws InterruptedException { - if (output != null && output.getBuffer().trim().length() > 0) { - ResultHandler resultHandler = null; - //AeshLine aeshLine = Parser.findAllWords(output.getBuffer()); - AeshLine aeshLine = new AeshLine(output.getBuffer(), Globals.args, ParserStatus.OK, ""); - try (CommandContainer commandContainer = getCommand(output, aeshLine)) { - resultHandler = commandContainer.getParser().getProcessedCommand().getResultHandler(); - CommandContainerResult ccResult = - commandContainer.executeCommand(aeshLine, console.getInvocationProviders(), console.getAeshContext(), - new AeshCommandInvocationProvider().enhanceCommandInvocation( - new AeshCommandInvocation(console, - output.getControlOperator(), output.getPid(), this))); - - result = ccResult.getCommandResult(); - - if(result == CommandResult.SUCCESS && resultHandler != null) - resultHandler.onSuccess(); - else if(resultHandler != null) - resultHandler.onFailure(result); - - if (result == CommandResult.FAILURE) { - // we assume the command has already output any error messages - System.exit(1); - } - } catch (Exception e) { - console.stop(); - - if (e instanceof OptionParserException) { - System.err.println("Unknown command: " + aeshLine.getWords().get(0)); - } else { - System.err.println(e.getMessage()); - } - if (Globals.dumpTrace) { - e.printStackTrace(); - } - - System.exit(1); - } - } - // empty line - else if (output != null) { - result = CommandResult.FAILURE; - } - else { - //stop(); - result = CommandResult.FAILURE; - } - - if (result == CommandResult.SUCCESS) { - return 0; - } else { - return 1; - } - } - - private CommandContainer getCommand(ConsoleOperation output, AeshLine aeshLine) throws CommandNotFoundException { - Method m; - try { - m = console.getClass().getDeclaredMethod("getCommand", AeshLine.class, String.class); - } catch (NoSuchMethodException e) { - throw new RuntimeException("Unexpected error: ", e); - } - - m.setAccessible(true); - - try { - return (CommandContainer) m.invoke(console, aeshLine, output.getBuffer()); - } catch (Exception e) { - throw new RuntimeException("Unexpected error: ", e); - } - } -} diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/AeshEnhancer.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/AeshEnhancer.java deleted file mode 100644 index d68e392395..0000000000 --- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/AeshEnhancer.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.keycloak.client.registration.cli.aesh; - -import org.jboss.aesh.console.AeshConsoleImpl; -import org.jboss.aesh.console.Console; - -import java.lang.reflect.Field; - -/** - * @author Marko Strukelj - */ -public class AeshEnhancer { - - public static void enhance(AeshConsoleImpl console) { - try { - Globals.stdin.setConsole(console); - - Field field = AeshConsoleImpl.class.getDeclaredField("console"); - field.setAccessible(true); - Console internalConsole = (Console) field.get(console); - internalConsole.setConsoleCallback(new AeshConsoleCallbackImpl(console)); - } catch (Exception e) { - throw new RuntimeException("Failed to install Aesh enhancement", e); - } - } -} diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/EndpointTypeConverter.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/EndpointTypeConverter.java deleted file mode 100644 index eb1c16a931..0000000000 --- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/EndpointTypeConverter.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.keycloak.client.registration.cli.aesh; - -import org.jboss.aesh.cl.converter.Converter; -import org.jboss.aesh.cl.validator.OptionValidatorException; -import org.jboss.aesh.console.command.converter.ConverterInvocation; -import org.keycloak.client.registration.cli.common.EndpointType; - -/** - * @author Marko Strukelj - */ -public class EndpointTypeConverter implements Converter { - - @Override - public EndpointType convert(ConverterInvocation converterInvocation) throws OptionValidatorException { - return EndpointType.of(converterInvocation.getInput()); - } -} diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/Globals.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/Globals.java deleted file mode 100644 index bdeab201e5..0000000000 --- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/Globals.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.keycloak.client.registration.cli.aesh; - -import java.util.List; - -/** - * @author Marko Strukelj - */ -public class Globals { - - public static boolean dumpTrace = false; - - public static ValveInputStream stdin; - - public static List args; -} diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/ValveInputStream.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/ValveInputStream.java deleted file mode 100644 index 76691dcc77..0000000000 --- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/aesh/ValveInputStream.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.keycloak.client.registration.cli.aesh; - -import org.jboss.aesh.console.AeshConsoleImpl; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InterruptedIOException; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; - -/** - * This stream blocks and waits, until there is some stream in the queue. - * It reads all streams from the queue, and then blocks until it receives more. - */ -public class ValveInputStream extends InputStream { - - private BlockingQueue queue = new LinkedBlockingQueue<>(10); - - private InputStream current; - - private AeshConsoleImpl console; - - @Override - public int read() throws IOException { - if (current == null) { - try { - current = queue.take(); - } catch (InterruptedException e) { - throw new InterruptedIOException("Signalled to exit"); - } - } - int c = current.read(); - if (c == -1) { - //current = null; - if (console != null) { - console.stop(); - } - } - - return c; - } - - /** - * For some reason AeshInputStream wants to do blocking read of whole buffers, which for stdin - * results in blocked input. - */ - @Override - public int read(byte b[], int off, int len) throws IOException { - int c = read(); - if (c == -1) { - return c; - } - b[off] = (byte) c; - return 1; - } - - public void setInputStream(InputStream is) { - if (queue.contains(is)) { - return; - } - queue.add(is); - } - - public void setConsole(AeshConsoleImpl console) { - this.console = console; - } - - public boolean isStdinAvailable() { - return console.isRunning(); - } -} diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/AbstractAuthOptionsCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/AbstractAuthOptionsCmd.java index 2d55decb78..436b2f2270 100644 --- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/AbstractAuthOptionsCmd.java +++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/AbstractAuthOptionsCmd.java @@ -1,7 +1,5 @@ package org.keycloak.client.registration.cli.commands; -import org.jboss.aesh.cl.Option; -import org.jboss.aesh.console.command.invocation.CommandInvocation; import org.keycloak.OAuth2Constants; import org.keycloak.client.registration.cli.config.ConfigData; import org.keycloak.client.registration.cli.config.ConfigHandler; @@ -14,6 +12,8 @@ import org.keycloak.client.registration.cli.util.IoUtil; import java.io.File; +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; @@ -26,58 +26,55 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd { static final String DEFAULT_CLIENT = "admin-cli"; - @Option(name = "config", description = "Path to the config file (~/.keycloak/kcreg.config by default)", hasValue = true) + @Option(names = "--config", description = "Path to the config file (~/.keycloak/kcreg.config by default)") protected String config; - @Option(name = "no-config", description = "No configuration file should be used, no authentication info is loaded or saved", hasValue = false) + @Option(names = "--no-config", description = "No configuration file should be used, no authentication info is loaded or saved") protected boolean noconfig; - @Option(name = "server", description = "Server endpoint url (e.g. 'http://localhost:8080')", hasValue = true) + @Option(names = "--server", description = "Server endpoint url (e.g. 'http://localhost:8080')") protected String server; - @Option(name = "realm", description = "Realm name to authenticate against", hasValue = true) + @Option(names = "--realm", description = "Realm name to authenticate against") protected String realm; - @Option(name = "client", description = "Realm name to authenticate against", hasValue = true) + @Option(names = "--client", description = "Realm name to authenticate against") protected String clientId; - @Option(name = "user", description = "Username to login with", hasValue = true) + @Option(names = "--user", description = "Username to login with") protected String user; - @Option(name = "password", description = "Password to login with (prompted for if not specified and --user is used)", hasValue = true) + @Option(names = "--password", description = "Password to login with (prompted for if not specified and --user is used)") protected String password; - @Option(name = "secret", description = "Secret to authenticate the client (prompted for if no --user or --keystore is specified)", hasValue = true) + @Option(names = "--secret", description = "Secret to authenticate the client (prompted for if no --user or --keystore is specified)") protected String secret; - @Option(name = "keystore", description = "Path to a keystore containing private key", hasValue = true) + @Option(names = "--keystore", description = "Path to a keystore containing private key") protected String keystore; - @Option(name = "storepass", description = "Keystore password (prompted for if not specified and --keystore is used)", hasValue = true) + @Option(names = "--storepass", description = "Keystore password (prompted for if not specified and --keystore is used)") protected String storePass; - @Option(name = "keypass", description = "Key password (prompted for if not specified and --keystore is used without --storepass, \n otherwise defaults to keystore password)", hasValue = true) + @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(name = "alias", description = "Alias of the key inside a keystore (defaults to the value of ClientId)", hasValue = true) + @Option(names = "--alias", description = "Alias of the key inside a keystore (defaults to the value of ClientId)") protected String alias; - @Option(name = "truststore", description = "Path to a truststore", hasValue = true) + @Option(names = "--truststore", description = "Path to a truststore") protected String trustStore; - @Option(name = "trustpass", description = "Truststore password (prompted for if not specified and --truststore is used)", hasValue = true) + @Option(names = "--trustpass", description = "Truststore password (prompted for if not specified and --truststore is used)") protected String trustPass; - @Option(name = "insecure", description = "Turns off TLS validation", hasValue = false) + @Option(names = "--insecure", description = "Turns off TLS validation") protected boolean insecure; - @Option(shortName = 't', name = "token", description = "Initial / Registration access token to use)", hasValue = true) + @Option(names = {"-t", "--token"}, description = "Initial / Registration access token to use)") protected String token; protected void initFromParent(AbstractAuthOptionsCmd parent) { - - super.initFromParent(parent); - noconfig = parent.noconfig; config = parent.config; server = parent.server; @@ -110,12 +107,11 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd { token == null && config == null; } - protected void processGlobalOptions() { - - super.processGlobalOptions(); + @Override + protected void processOptions() { if (config != null && noconfig) { - throw new RuntimeException("Options --config and --no-config are mutually exclusive"); + throw new IllegalArgumentException("Options --config and --no-config are mutually exclusive"); } if (!noconfig) { @@ -130,7 +126,7 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd { } } - protected void setupTruststore(ConfigData configData, CommandInvocation invocation ) { + protected void setupTruststore(ConfigData configData) { if (!configData.getServerUrl().startsWith("https:")) { return; @@ -147,7 +143,7 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd { pass = configData.getTrustpass(); } if (pass == null) { - pass = IoUtil.readSecret("Enter truststore password: ", invocation); + pass = IoUtil.readSecret("Enter truststore password: "); } try { @@ -162,7 +158,7 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd { } } - protected ConfigData ensureAuthInfo(ConfigData config, CommandInvocation commandInvocation) { + protected ConfigData ensureAuthInfo(ConfigData config) { if (requiresLogin()) { // make sure current handler is in-memory handler @@ -178,7 +174,7 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd { ConfigCredentialsCmd login = new ConfigCredentialsCmd(); login.initFromParent(this); login.init(config); - login.process(commandInvocation); + login.process(); // this must be executed before finally block which restores config handler return loadConfig(); @@ -237,6 +233,7 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd { rdata.setGrantTypeForAuthentication(grantTypeForAuthentication); } + @Override protected void checkUnsupportedOptions(String ... options) { if (options.length % 2 != 0) { throw new IllegalArgumentException("Even number of argument required"); diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/AbstractGlobalOptionsCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/AbstractGlobalOptionsCmd.java index fa412ceaf2..5c9c7a7094 100644 --- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/AbstractGlobalOptionsCmd.java +++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/AbstractGlobalOptionsCmd.java @@ -1,38 +1,37 @@ package org.keycloak.client.registration.cli.commands; -import org.jboss.aesh.cl.Option; -import org.jboss.aesh.console.command.Command; -import org.keycloak.client.registration.cli.aesh.Globals; +import org.keycloak.client.registration.cli.Globals; + +import picocli.CommandLine; +import picocli.CommandLine.Option; import static org.keycloak.client.registration.cli.util.IoUtil.printOut; /** * @author Marko Strukelj */ -public abstract class AbstractGlobalOptionsCmd implements Command { +public abstract class AbstractGlobalOptionsCmd implements Runnable { - @Option(shortName = 'x', description = "Print full stack trace when exiting with error", hasValue = false) - protected boolean dumpTrace; - - @Option(name = "help", description = "Print command specific help", hasValue = false) - protected boolean help; - - protected void initFromParent(AbstractGlobalOptionsCmd parent) { - dumpTrace = parent.dumpTrace; - help = parent.help; + @Option(names = "--help", + description = "Print command specific help") + public void setHelp(boolean help) { + Globals.help = help; } - protected void processGlobalOptions() { + @Option(names = "-x", + description = "Print full stack trace when exiting with error") + public void setDumpTrace(boolean dumpTrace) { Globals.dumpTrace = dumpTrace; } - protected boolean printHelp() { - if (help || nothingToDo()) { + protected void printHelpIfNeeded() { + if (Globals.help) { printOut(help()); - return true; + System.exit(CommandLine.ExitCode.OK); + } else if (nothingToDo()) { + printOut(help()); + System.exit(CommandLine.ExitCode.USAGE); } - - return false; } protected boolean nothingToDo() { @@ -42,4 +41,47 @@ public abstract class AbstractGlobalOptionsCmd implements Command { protected String help() { return KcRegCmd.usage(); } + + @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; + } + } diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/AttrsCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/AttrsCmd.java index efa8e25500..9f3a036aef 100644 --- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/AttrsCmd.java +++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/AttrsCmd.java @@ -1,11 +1,5 @@ package org.keycloak.client.registration.cli.commands; -import org.jboss.aesh.cl.Arguments; -import org.jboss.aesh.cl.CommandDefinition; -import org.jboss.aesh.cl.Option; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; import org.keycloak.client.registration.cli.common.AttributeKey; import org.keycloak.client.registration.cli.common.EndpointType; import org.keycloak.client.registration.cli.util.ReflectionUtil; @@ -19,9 +13,13 @@ import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + import static org.keycloak.client.registration.cli.util.OsUtil.CMD; import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT; import static org.keycloak.client.registration.cli.util.ReflectionUtil.getAttributeListWithJSonTypes; @@ -32,97 +30,78 @@ import static org.keycloak.client.registration.cli.util.ReflectionUtil.isMapType /** * @author Marko Strukelj */ -@CommandDefinition(name = "attrs", description = "[ATTRIBUTE] [--endpoint TYPE]") +@Command(name = "attrs", description = "[ATTRIBUTE] [--endpoint TYPE]") public class AttrsCmd extends AbstractGlobalOptionsCmd { - @Option(shortName = 'e', name = "endpoint", description = "Endpoint type to use", hasValue = true) + CommandLine.Model.CommandSpec spec; + + @Option(names = {"-e", "--endpoint"}, description = "Endpoint type to use") protected String endpoint; - @Arguments - protected List args; - + @Parameters(arity = "0..1") protected String attr; @Override - public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - try { - processGlobalOptions(); + protected void process() { + EndpointType regType = EndpointType.DEFAULT; + PrintStream out = System.out; - if (printHelp()) { - return CommandResult.SUCCESS; + if (endpoint != null) { + regType = EndpointType.of(endpoint); + } + + Class type = regType == EndpointType.DEFAULT ? ClientRepresentation.class : (regType == EndpointType.OIDC ? OIDCClientRepresentation.class : null); + if (type == null) { + throw new IllegalArgumentException("Endpoint not supported: " + regType); + } + AttributeKey key = attr == null ? new AttributeKey() : new AttributeKey(attr); + + Field f = ReflectionUtil.resolveField(type, key); + String ts = f != null ? ReflectionUtil.getTypeString(null, f) : null; + + if (f == null) { + out.printf("Attributes for %s format:\n", regType.getEndpoint()); + + LinkedHashMap items = getAttributeListWithJSonTypes(type, key); + for (Map.Entry item : items.entrySet()) { + out.printf(" %-40s %s\n", item.getKey(), item.getValue()); } - EndpointType regType = EndpointType.DEFAULT; - PrintStream out = commandInvocation.getShell().out(); + } else { + out.printf("%-40s %s", attr, ts); + boolean eol = false; - if (endpoint != null) { - regType = EndpointType.of(endpoint); - } - - if (args != null) { - if (args.size() > 1) { - throw new IllegalArgumentException("Invalid option: " + args.get(1)); - } - attr = args.get(0); - } - - Class type = regType == EndpointType.DEFAULT ? ClientRepresentation.class : (regType == EndpointType.OIDC ? OIDCClientRepresentation.class : null); - if (type == null) { - throw new IllegalArgumentException("Endpoint not supported: " + regType); - } - AttributeKey key = attr == null ? new AttributeKey() : new AttributeKey(attr); - - Field f = ReflectionUtil.resolveField(type, key); - String ts = f != null ? ReflectionUtil.getTypeString(null, f) : null; - - if (f == null) { - out.printf("Attributes for %s format:\n", regType.getEndpoint()); - - LinkedHashMap items = getAttributeListWithJSonTypes(type, key); - for (Map.Entry item : items.entrySet()) { - out.printf(" %-40s %s\n", item.getKey(), item.getValue()); - } - - } else { - out.printf("%-40s %s", attr, ts); - boolean eol = false; - - Type t = f.getGenericType(); - if (isListType(f.getType()) && t instanceof ParameterizedType) { - t = ((ParameterizedType) t).getActualTypeArguments()[0]; - if (!isBasicType(t) && t instanceof Class) { - eol = true; - System.out.printf(", where value is:\n", ts); - LinkedHashMap items = ReflectionUtil.getAttributeListWithJSonTypes((Class) t, null); - for (Map.Entry item : items.entrySet()) { - out.printf(" %-36s %s\n", item.getKey(), item.getValue()); - } - } - } else if (isMapType(f.getType()) && t instanceof ParameterizedType) { - t = ((ParameterizedType) t).getActualTypeArguments()[1]; - if (!isBasicType(t) && t instanceof Class) { - eol = true; - out.printf(", where value is:\n", ts); - LinkedHashMap items = ReflectionUtil.getAttributeListWithJSonTypes((Class) t, null); - for (Map.Entry item : items.entrySet()) { - out.printf(" %-36s %s\n", item.getKey(), item.getValue()); - } + Type t = f.getGenericType(); + if (isListType(f.getType()) && t instanceof ParameterizedType) { + t = ((ParameterizedType) t).getActualTypeArguments()[0]; + if (!isBasicType(t) && t instanceof Class) { + eol = true; + out.printf(", where value is:\n", ts); + LinkedHashMap items = ReflectionUtil.getAttributeListWithJSonTypes((Class) t, null); + for (Map.Entry item : items.entrySet()) { + out.printf(" %-36s %s\n", item.getKey(), item.getValue()); } } - - if (!eol) { - // add end of line - out.println(); + } else if (isMapType(f.getType()) && t instanceof ParameterizedType) { + t = ((ParameterizedType) t).getActualTypeArguments()[1]; + if (!isBasicType(t) && t instanceof Class) { + eol = true; + out.printf(", where value is:\n", ts); + LinkedHashMap items = ReflectionUtil.getAttributeListWithJSonTypes((Class) t, null); + for (Map.Entry item : items.entrySet()) { + out.printf(" %-36s %s\n", item.getKey(), item.getValue()); + } } } - return CommandResult.SUCCESS; - - } finally { - commandInvocation.stop(); + if (!eol) { + // add end of line + out.println(); + } } } + @Override protected String help() { return usage(); } diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigCmd.java index 82c4a54a47..22dbd3e1fe 100644 --- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigCmd.java +++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigCmd.java @@ -17,80 +17,36 @@ package org.keycloak.client.registration.cli.commands; -import org.jboss.aesh.cl.Arguments; -import org.jboss.aesh.cl.GroupCommandDefinition; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.Command; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; - import java.io.PrintWriter; import java.io.StringWriter; -import java.util.List; + +import picocli.CommandLine.Command; import static org.keycloak.client.registration.cli.util.OsUtil.CMD; -import static org.keycloak.client.registration.cli.util.OsUtil.EOL; /** * @author Marko Strukelj */ -@GroupCommandDefinition(name = "config", description = "COMMAND [ARGUMENTS]", groupCommands = {ConfigCredentialsCmd.class} ) -public class ConfigCmd extends AbstractAuthOptionsCmd implements Command { +@Command(name = "config", description = "COMMAND [ARGUMENTS]", subcommands = { + ConfigCredentialsCmd.class, + ConfigInitialTokenCmd.class, + ConfigRegistrationTokenCmd.class, + ConfigTruststoreCmd.class +}) +public class ConfigCmd extends AbstractAuthOptionsCmd { - @Arguments - protected List args; + @Override + protected void process() { - - public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - try { - if (args != null && args.size() > 0) { - String cmd = args.get(0); - switch (cmd) { - case "credentials": { - ConfigCredentialsCmd command = new ConfigCredentialsCmd(); - command.initFromParent(this); - return command.execute(commandInvocation); - } - case "truststore": { - ConfigTruststoreCmd command = new ConfigTruststoreCmd(); - command.initFromParent(this); - return command.execute(commandInvocation); - } - case "initial-token": { - ConfigInitialTokenCmd command = new ConfigInitialTokenCmd(); - command.initFromParent(this); - return command.execute(commandInvocation); - } - case "registration-token": { - ConfigRegistrationTokenCmd command = new ConfigRegistrationTokenCmd(); - command.initFromParent(this); - return command.execute(commandInvocation); - } - default: { - if (printHelp()) { - return help ? CommandResult.SUCCESS : CommandResult.FAILURE; - } - throw new IllegalArgumentException("Unknown sub-command: " + cmd + suggestHelp()); - } - } - } - - if (printHelp()) { - return help ? CommandResult.SUCCESS : CommandResult.FAILURE; - } - - throw new IllegalArgumentException("Sub-command required by '" + CMD + " config' - one of: 'credentials', 'truststore', 'initial-token', 'registration-token'"); - - } finally { - commandInvocation.stop(); - } } - protected String suggestHelp() { - return EOL + "Try '" + CMD + " help config' for more information"; + @Override + protected boolean nothingToDo() { + return true; } + @Override protected String help() { return usage(); } diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigCredentialsCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigCredentialsCmd.java index 7cc6c54099..a0f16c9409 100644 --- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigCredentialsCmd.java +++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigCredentialsCmd.java @@ -1,10 +1,5 @@ package org.keycloak.client.registration.cli.commands; -import org.jboss.aesh.cl.CommandDefinition; -import org.jboss.aesh.console.command.Command; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; import org.keycloak.OAuth2Constants; import org.keycloak.client.registration.cli.config.ConfigData; import org.keycloak.client.registration.cli.config.RealmConfigData; @@ -16,6 +11,8 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.net.URL; +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; @@ -26,15 +23,14 @@ 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.EOL; import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH; import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT; /** * @author Marko Strukelj */ -@CommandDefinition(name = "credentials", description = "--server SERVER_URL --realm REALM [ARGUMENTS]") -public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd implements Command { +@Command(name = "credentials", description = "--server SERVER_URL --realm REALM [ARGUMENTS]") +public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd { private int sigLifetime = 600; @@ -60,24 +56,9 @@ public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd implements Comm } } - @Override - public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - try { - if (printHelp()) { - return help ? CommandResult.SUCCESS : CommandResult.FAILURE; - } - - checkUnsupportedOptions("--no-config", booleanOptionForCheck(noconfig)); - - processGlobalOptions(); - - return process(commandInvocation); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e); - } finally { - commandInvocation.stop(); - } + protected String[] getUnsupportedOptions() { + return new String[] {"--no-config", booleanOptionForCheck(noconfig)}; } @Override @@ -85,8 +66,8 @@ public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd implements Comm return noOptions(); } - public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - + @Override + protected void process() { // check server if (server == null) { throw new IllegalArgumentException("Required option not specified: --server"); @@ -113,18 +94,18 @@ public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd implements Comm // if user was set there needs to be a password so we can authenticate if (password == null) { - password = readSecret("Enter password: ", commandInvocation); + 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: ", commandInvocation); + 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: ", commandInvocation); + secret = readSecret("Enter client secret: "); } } } @@ -139,8 +120,8 @@ public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd implements Comm } if (storePass == null) { - storePass = readSecret("Enter keystore password: ", commandInvocation); - keyPass = readSecret("Enter key password: ", commandInvocation); + storePass = readSecret("Enter keystore password: "); + keyPass = readSecret("Enter key password: "); } if (keyPass == null) { @@ -163,10 +144,10 @@ public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd implements Comm config.setServerUrl(server); config.setRealm(realm); }); - return CommandResult.SUCCESS; + return; } - setupTruststore(copyWithServerInfo(loadConfig()), commandInvocation); + setupTruststore(copyWithServerInfo(loadConfig())); // now use the token endpoint to retrieve access token, and refresh token AccessTokenResponse tokens = signedRequestToken != null ? @@ -179,14 +160,9 @@ public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd implements Comm // save tokens to config file saveTokens(tokens, server, realm, clientId, signedRequestToken, sigExpiresAt, secret, grantTypeForAuthentication); - - return CommandResult.SUCCESS; - } - - protected String suggestHelp() { - return EOL + "Try '" + CMD + " help config credentials' for more information"; } + @Override protected String help() { return usage(); } diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigInitialTokenCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigInitialTokenCmd.java index 486f1ab2b8..9bd9e72718 100644 --- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigInitialTokenCmd.java +++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigInitialTokenCmd.java @@ -1,107 +1,45 @@ package org.keycloak.client.registration.cli.commands; -import org.jboss.aesh.cl.CommandDefinition; -import org.jboss.aesh.console.command.Command; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; import org.keycloak.client.registration.cli.config.RealmConfigData; import org.keycloak.client.registration.cli.util.IoUtil; import org.keycloak.client.registration.cli.util.ParseUtil; import java.io.PrintWriter; import java.io.StringWriter; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; + +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.warnfOut; import static org.keycloak.client.registration.cli.util.OsUtil.CMD; -import static org.keycloak.client.registration.cli.util.OsUtil.EOL; import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH; import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT; /** * @author Marko Strukelj */ -@CommandDefinition(name = "initial-token", description = "[--server SERVER] --realm REALM [--delete | TOKEN] [ARGUMENTS]") -public class ConfigInitialTokenCmd extends AbstractAuthOptionsCmd implements Command { - - private ConfigCmd parent; +@Command(name = "initial-token", description = "[--server SERVER] --realm REALM [--delete | TOKEN] [ARGUMENTS]") +public class ConfigInitialTokenCmd extends AbstractAuthOptionsCmd { + @Option(names = {"-d", "--delete"}, description = "Indicates that initial access token should be removed") private boolean delete; + @Option(names = {"-k", "--keep-domain"}, description = "Don't overwrite default server and realm") private boolean keepDomain; - - protected void initFromParent(ConfigCmd parent) { - this.parent = parent; - super.initFromParent(parent); - } - - @Override - public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - try { - if (printHelp()) { - return help ? CommandResult.SUCCESS : CommandResult.FAILURE; - } - - return process(commandInvocation); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e); - } finally { - commandInvocation.stop(); - } - } + @Parameters(arity = "0..1") + private String token; @Override protected boolean nothingToDo() { - return noOptions() && parent.args.size() == 1; + return noOptions() && token == null && !delete && !keepDomain; } - public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - - List args = new ArrayList<>(); - - Iterator it = parent.args.iterator(); - // skip the first argument 'initial-token' - it.next(); - - while (it.hasNext()) { - String arg = it.next(); - switch (arg) { - case "-d": - case "--delete": { - delete = true; - break; - } - case "-k": - case "--keep-domain": { - keepDomain = true; - break; - } - default: { - args.add(arg); - } - } - } - - if (args.size() > 1) { - throw new IllegalArgumentException("Invalid option: " + args.get(1)); - } - - String token = args.size() == 1 ? args.get(0) : null; - - if (realm == null) { - throw new IllegalArgumentException("Realm not specified"); - } - - if (token != null && token.startsWith("-")) { - warnfOut(ParseUtil.TOKEN_OPTION_WARN, token); - } - - checkUnsupportedOptions( + @Override + protected String[] getUnsupportedOptions() { + return new String[] { "--client", clientId, "--user", user, "--password", password, @@ -112,15 +50,24 @@ public class ConfigInitialTokenCmd extends AbstractAuthOptionsCmd implements Com "--alias", alias, "--truststore", trustStore, "--trustpass", keyPass, - "--no-config", booleanOptionForCheck(noconfig)); + "--no-config", booleanOptionForCheck(noconfig)}; + } + @Override + protected void process() { + if (realm == null) { + throw new IllegalArgumentException("Realm not specified"); + } + + if (token != null && token.startsWith("-")) { + warnfOut(ParseUtil.TOKEN_OPTION_WARN, token); + } if (!delete && token == null) { - token = IoUtil.readSecret("Enter Initial Access Token: ", commandInvocation); + token = IoUtil.readSecret("Enter Initial Access Token: "); } // now update the config - processGlobalOptions(); String initialToken = token; saveMergeConfig(config -> { @@ -138,14 +85,9 @@ public class ConfigInitialTokenCmd extends AbstractAuthOptionsCmd implements Com rdata.setInitialToken(initialToken); } }); - - return CommandResult.SUCCESS; - } - - protected String suggestHelp() { - return EOL + "Try '" + CMD + " help config initial-token' for more information"; } + @Override protected String help() { return usage(); } diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigRegistrationTokenCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigRegistrationTokenCmd.java index 091402cb9b..8d11b0a588 100644 --- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigRegistrationTokenCmd.java +++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigRegistrationTokenCmd.java @@ -1,91 +1,40 @@ package org.keycloak.client.registration.cli.commands; -import org.jboss.aesh.cl.CommandDefinition; -import org.jboss.aesh.console.command.Command; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; import org.keycloak.client.registration.cli.config.RealmConfigData; import org.keycloak.client.registration.cli.util.IoUtil; import java.io.PrintWriter; import java.io.StringWriter; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; + +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.OsUtil.CMD; -import static org.keycloak.client.registration.cli.util.OsUtil.EOL; import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH; import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT; /** * @author Marko Strukelj */ -@CommandDefinition(name = "registration-token", description = "[--server SERVER] --realm REALM --client CLIENT [--delete | TOKEN] [ARGUMENTS]") -public class ConfigRegistrationTokenCmd extends AbstractAuthOptionsCmd implements Command { - - private ConfigCmd parent; +@Command(name = "registration-token", description = "[--server SERVER] --realm REALM --client CLIENT [--delete | TOKEN] [ARGUMENTS]") +public class ConfigRegistrationTokenCmd extends AbstractAuthOptionsCmd { + @Option(names = {"-d", "--delete"}, description = "Indicates that initial access token should be removed") private boolean delete; - - protected void initFromParent(ConfigCmd parent) { - this.parent = parent; - super.initFromParent(parent); - } - - @Override - public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - try { - if (printHelp()) { - return help ? CommandResult.SUCCESS : CommandResult.FAILURE; - } - - return process(commandInvocation); - - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e); - } finally { - commandInvocation.stop(); - } - } + @Parameters(arity = "0..1") + private String token; @Override protected boolean nothingToDo() { - return noOptions() && parent.args.size() == 1; + return noOptions() && token == null && !delete; } - public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - - List args = new ArrayList<>(); - - Iterator it = parent.args.iterator(); - // skip the first argument 'registration-token' - it.next(); - - while (it.hasNext()) { - String arg = it.next(); - switch (arg) { - case "-d": - case "--delete": { - delete = true; - break; - } - default: { - args.add(arg); - } - } - } - - if (args.size() > 1) { - throw new IllegalArgumentException("Invalid option: " + args.get(1)); - } - - String token = args.size() == 1 ? args.get(0) : null; - + @Override + protected void process() { if (server == null) { throw new IllegalArgumentException("Required option not specified: --server"); } @@ -112,11 +61,10 @@ public class ConfigRegistrationTokenCmd extends AbstractAuthOptionsCmd implement if (!delete && token == null) { - token = IoUtil.readSecret("Enter Registration Access Token: ", commandInvocation); + token = IoUtil.readSecret("Enter Registration Access Token: "); } // now update the config - processGlobalOptions(); String registrationToken = token; saveMergeConfig(config -> { @@ -129,14 +77,9 @@ public class ConfigRegistrationTokenCmd extends AbstractAuthOptionsCmd implement config.ensureRealmConfigData(server, realm).getClients().put(clientId, registrationToken); } }); - - return CommandResult.SUCCESS; - } - - protected String suggestHelp() { - return EOL + "Try '" + CMD + " help config registration-token' for more information"; } + @Override protected String help() { return usage(); } diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigTruststoreCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigTruststoreCmd.java index d88d6b0265..3e032515e7 100644 --- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigTruststoreCmd.java +++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/ConfigTruststoreCmd.java @@ -16,96 +16,41 @@ */ package org.keycloak.client.registration.cli.commands; -import org.jboss.aesh.cl.CommandDefinition; -import org.jboss.aesh.console.command.Command; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; - import java.io.File; import java.io.PrintWriter; import java.io.StringWriter; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; + +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.EOL; import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH; import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT; /** * @author Marko Strukelj */ -@CommandDefinition(name = "truststore", description = "PATH [ARGUMENTS]") -public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd implements Command { - - private ConfigCmd parent; +@Command(name = "truststore", description = "PATH [ARGUMENTS]") +public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd { + @Option(names = {"-d", "--delete"}, description = "Indicates that initial access token should be removed") private boolean delete; - - protected void initFromParent(ConfigCmd parent) { - this.parent = parent; - super.initFromParent(parent); - } - - @Override - public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - try { - if (printHelp()) { - return help ? CommandResult.SUCCESS : CommandResult.FAILURE; - } - - return process(commandInvocation); - - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e); - } finally { - commandInvocation.stop(); - } - } + @Parameters(arity = "0..1") + private String truststorePath; @Override protected boolean nothingToDo() { - return noOptions() && parent.args.size() == 1; + return noOptions() && truststorePath == null && !delete; } - public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - - List args = new ArrayList<>(); - - Iterator it = parent.args.iterator(); - // skip the first argument 'truststore' - it.next(); - - while (it.hasNext()) { - String arg = it.next(); - switch (arg) { - case "-d": - case "--delete": { - delete = true; - break; - } - default: { - args.add(arg); - } - } - } - - if (args.size() > 1) { - throw new IllegalArgumentException("Invalid option: " + args.get(1)); - } - - String truststore = null; - if (args.size() > 0) { - truststore = args.get(0); - } - - checkUnsupportedOptions("--server", server, + @Override + protected String[] getUnsupportedOptions() { + return new String[] {"--server", server, "--realm", realm, "--client", clientId, "--user", user, @@ -115,33 +60,35 @@ public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd implements Comma "--keystore", keystore, "--keypass", keyPass, "--alias", alias, - "--no-config", booleanOptionForCheck(noconfig)); + "--no-config", booleanOptionForCheck(noconfig)}; + } + @Override + protected void process() { // now update the config - processGlobalOptions(); String store; String pass; if (!delete) { - if (truststore == null) { + if (truststorePath == null) { throw new IllegalArgumentException("No truststore specified"); } - if (!new File(truststore).isFile()) { - throw new RuntimeException("Truststore file not found: " + truststore); + if (!new File(truststorePath).isFile()) { + throw new RuntimeException("Truststore file not found: " + truststorePath); } if ("-".equals(trustPass)) { - trustPass = readSecret("Enter truststore password: ", commandInvocation); + trustPass = readSecret("Enter truststore password: "); } - store = truststore; + store = truststorePath; pass = trustPass; } else { - if (truststore != null) { + if (truststorePath != null) { throw new IllegalArgumentException("Option --delete is mutually exclusive with specifying a TRUSTSTORE"); } if (trustPass != null) { @@ -155,14 +102,9 @@ public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd implements Comma config.setTruststore(store); config.setTrustpass(pass); }); - - return CommandResult.SUCCESS; - } - - protected String suggestHelp() { - return EOL + "Try '" + CMD + " help config truststore' for more information"; } + @Override protected String help() { return usage(); } diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/CreateCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/CreateCmd.java index ae3791e122..605b88a1e9 100644 --- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/CreateCmd.java +++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/CreateCmd.java @@ -17,19 +17,11 @@ package org.keycloak.client.registration.cli.commands; -import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; -import org.jboss.aesh.cl.Arguments; -import org.jboss.aesh.cl.CommandDefinition; -import org.jboss.aesh.cl.Option; -import org.jboss.aesh.console.command.Command; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; -import org.keycloak.client.registration.cli.aesh.EndpointTypeConverter; +import org.keycloak.client.registration.cli.EndpointTypeConverter; import org.keycloak.client.registration.cli.common.AttributeOperation; -import org.keycloak.client.registration.cli.config.ConfigData; import org.keycloak.client.registration.cli.common.CmdStdinContext; import org.keycloak.client.registration.cli.common.EndpointType; +import org.keycloak.client.registration.cli.config.ConfigData; import org.keycloak.client.registration.cli.util.HttpUtil; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.oidc.OIDCClientRepresentation; @@ -39,183 +31,155 @@ import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.io.StringWriter; -import java.util.Iterator; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; +import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; + +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + import static org.keycloak.client.registration.cli.common.AttributeOperation.Type.SET; import static org.keycloak.client.registration.cli.common.EndpointType.DEFAULT; import static org.keycloak.client.registration.cli.common.EndpointType.OIDC; import static org.keycloak.client.registration.cli.common.EndpointType.SAML2; +import static org.keycloak.client.registration.cli.util.AuthUtil.ensureToken; import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.registration.cli.util.ConfigUtil.credentialsAvailable; import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig; import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig; +import static org.keycloak.client.registration.cli.util.ConfigUtil.setRegistrationToken; +import static org.keycloak.client.registration.cli.util.HttpUtil.doPost; import static org.keycloak.client.registration.cli.util.HttpUtil.getExpectedContentType; import static org.keycloak.client.registration.cli.util.IoUtil.printErr; +import static org.keycloak.client.registration.cli.util.IoUtil.printOut; import static org.keycloak.client.registration.cli.util.IoUtil.readFully; 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.EOL; 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.AuthUtil.ensureToken; -import static org.keycloak.client.registration.cli.util.ConfigUtil.setRegistrationToken; -import static org.keycloak.client.registration.cli.util.HttpUtil.doPost; -import static org.keycloak.client.registration.cli.util.IoUtil.printOut; import static org.keycloak.client.registration.cli.util.ParseUtil.parseKeyVal; /** * @author Marko Strukelj */ -@CommandDefinition(name = "create", description = "[ARGUMENTS]") -public class CreateCmd extends AbstractAuthOptionsCmd implements Command { +@Command(name = "create", description = "[ARGUMENTS]") +public class CreateCmd extends AbstractAuthOptionsCmd { - @Option(shortName = 'i', name = "clientId", description = "After creation only print clientId to standard output", hasValue = false) + @Option(names = {"-i", "--clientId"}, description = "After creation only print clientId to standard output") protected boolean returnClientId = false; - @Option(shortName = 'e', name = "endpoint", description = "Endpoint type / document format to use - one of: 'default', 'oidc', 'saml2'", - hasValue = true, converter = EndpointTypeConverter.class) + @Option(names = {"-e", "--endpoint"}, description = "Endpoint type / document format to use - one of: 'default', 'oidc', 'saml2'", converter = EndpointTypeConverter.class) protected EndpointType regType; - @Option(shortName = 'f', name = "file", description = "Read object from file or standard input if FILENAME is set to '-'", hasValue = true) + @Option(names = {"-f", "--file"}, description = "Read object from file or standard input if FILENAME is set to '-'") protected String file; - @Option(shortName = 'o', name = "output", description = "After creation output the new client configuration to standard output", hasValue = false) + @Option(names = {"-o", "--output"}, description = "After creation output the new client configuration to standard output") protected boolean outputClient = false; - @Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false) + @Option(names = {"-c", "--compressed"}, description = "Don't pretty print the output") protected boolean compressed = false; - //@OptionGroup(shortName = 's', name = "set", description = "Set attribute to the specified value") - //Map attributes = new LinkedHashMap<>(); + @Option(names = {"-s", "--set"}, description = "Set a specific attribute NAME to a specified value VALUE") + List rawSets = new ArrayList<>(); - @Arguments - protected List args; + List attrs = new ArrayList<>(); @Override - public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { + protected void processOptions() { + super.processOptions(); - List attrs = new LinkedList<>(); + for (String set : rawSets) { + String[] keyVal = parseKeyVal(set); + attrs.add(new AttributeOperation(SET, keyVal[0], keyVal[1])); + } + } + + @Override + protected void process() { + if (file == null && attrs.size() == 0) { + throw new IllegalArgumentException("No file nor attribute values specified"); + } + + if (outputClient && returnClientId) { + throw new IllegalArgumentException("Options -o and -i are mutually exclusive"); + } + + // if --token is specified read it + if ("-".equals(token)) { + token = readSecret("Enter Initial Access Token: "); + } + + CmdStdinContext ctx = new CmdStdinContext(); + if (file != null) { + ctx = parseFileOrStdin(file, regType); + } + + if (ctx.getEndpointType() == null) { + regType = regType != null ? regType : DEFAULT; + ctx.setEndpointType(regType); + } else if (regType != null && ctx.getEndpointType() != regType) { + throw new RuntimeException("Requested endpoint type not compatible with detected configuration format: " + ctx.getEndpointType()); + } + + if (attrs.size() > 0) { + ctx = mergeAttributes(ctx, attrs); + } + + String contentType = getExpectedContentType(ctx.getEndpointType()); + + ConfigData config = loadConfig(); + config = copyWithServerInfo(config); + + if (token == null) { + // if initial token is not set, try use the one from configuration + token = config.sessionRealmConfigData().getInitialToken(); + } + + setupTruststore(config); + + String auth = token; + if (auth == null) { + config = ensureAuthInfo(config); + config = copyWithServerInfo(config); + if (credentialsAvailable(config)) { + auth = ensureToken(config); + } + } + + auth = auth != null ? "Bearer " + auth : null; + + final String server = config.getServerUrl(); + final String realm = config.getRealm(); + + InputStream response = doPost(server + "/realms/" + realm + "/clients-registrations/" + ctx.getEndpointType().getEndpoint(), + contentType, HttpUtil.APPLICATION_JSON, ctx.getContent(), auth); try { - if (printHelp()) { - return help ? CommandResult.SUCCESS : CommandResult.FAILURE; + if (ctx.getEndpointType() == DEFAULT || ctx.getEndpointType() == SAML2) { + ClientRepresentation client = JsonSerialization.readValue(response, ClientRepresentation.class); + outputResult(client.getClientId(), client); + + saveMergeConfig(cfg -> { + setRegistrationToken(cfg.ensureRealmConfigData(server, realm), client.getClientId(), client.getRegistrationAccessToken()); + }); + } else if (ctx.getEndpointType() == OIDC) { + OIDCClientRepresentation client = JsonSerialization.readValue(response, OIDCClientRepresentation.class); + outputResult(client.getClientId(), client); + + saveMergeConfig(cfg -> { + setRegistrationToken(cfg.ensureRealmConfigData(server, realm), client.getClientId(), client.getRegistrationAccessToken()); + }); + } else { + printOut("Response from server: " + readFully(response)); } - - processGlobalOptions(); - - if (args != null) { - Iterator it = args.iterator(); - while (it.hasNext()) { - String option = it.next(); - switch (option) { - case "-s": - case "--set": { - if (!it.hasNext()) { - throw new IllegalArgumentException("Option " + option + " requires a value"); - } - String[] keyVal = parseKeyVal(it.next()); - attrs.add(new AttributeOperation(SET, keyVal[0], keyVal[1])); - break; - } - default: { - throw new IllegalArgumentException("Unsupported option: " + option); - } - } - } - } - - if (file == null && attrs.size() == 0) { - throw new IllegalArgumentException("No file nor attribute values specified"); - } - - if (outputClient && returnClientId) { - throw new IllegalArgumentException("Options -o and -i are mutually exclusive"); - } - - // if --token is specified read it - if ("-".equals(token)) { - token = readSecret("Enter Initial Access Token: ", commandInvocation); - } - - CmdStdinContext ctx = new CmdStdinContext(); - if (file != null) { - ctx = parseFileOrStdin(file, regType); - } - - if (ctx.getEndpointType() == null) { - regType = regType != null ? regType : DEFAULT; - ctx.setEndpointType(regType); - } else if (regType != null && ctx.getEndpointType() != regType) { - throw new RuntimeException("Requested endpoint type not compatible with detected configuration format: " + ctx.getEndpointType()); - } - - if (attrs.size() > 0) { - ctx = mergeAttributes(ctx, attrs); - } - - String contentType = getExpectedContentType(ctx.getEndpointType()); - - ConfigData config = loadConfig(); - config = copyWithServerInfo(config); - - if (token == null) { - // if initial token is not set, try use the one from configuration - token = config.sessionRealmConfigData().getInitialToken(); - } - - setupTruststore(config, commandInvocation); - - String auth = token; - if (auth == null) { - config = ensureAuthInfo(config, commandInvocation); - config = copyWithServerInfo(config); - if (credentialsAvailable(config)) { - auth = ensureToken(config); - } - } - - auth = auth != null ? "Bearer " + auth : null; - - final String server = config.getServerUrl(); - final String realm = config.getRealm(); - - InputStream response = doPost(server + "/realms/" + realm + "/clients-registrations/" + ctx.getEndpointType().getEndpoint(), - contentType, HttpUtil.APPLICATION_JSON, ctx.getContent(), auth); - - try { - if (ctx.getEndpointType() == DEFAULT || ctx.getEndpointType() == SAML2) { - ClientRepresentation client = JsonSerialization.readValue(response, ClientRepresentation.class); - outputResult(client.getClientId(), client); - - saveMergeConfig(cfg -> { - setRegistrationToken(cfg.ensureRealmConfigData(server, realm), client.getClientId(), client.getRegistrationAccessToken()); - }); - } else if (ctx.getEndpointType() == OIDC) { - OIDCClientRepresentation client = JsonSerialization.readValue(response, OIDCClientRepresentation.class); - outputResult(client.getClientId(), client); - - saveMergeConfig(cfg -> { - setRegistrationToken(cfg.ensureRealmConfigData(server, realm), client.getClientId(), client.getRegistrationAccessToken()); - }); - } else { - printOut("Response from server: " + readFully(response)); - } - } catch (UnrecognizedPropertyException e) { - throw new RuntimeException("Failed to process HTTP reponse - " + e.getMessage(), e); - } catch (IOException e) { - throw new RuntimeException("Failed to process HTTP response", e); - } - - return CommandResult.SUCCESS; - - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e); - } finally { - commandInvocation.stop(); + } catch (UnrecognizedPropertyException e) { + throw new RuntimeException("Failed to process HTTP reponse - " + e.getMessage(), e); + } catch (IOException e) { + throw new RuntimeException("Failed to process HTTP response", e); } } @@ -235,13 +199,10 @@ public class CreateCmd extends AbstractAuthOptionsCmd implements Command { @Override protected boolean nothingToDo() { - return noOptions() && regType == null && file == null && (args == null || args.size() == 0); - } - - protected String suggestHelp() { - return EOL + "Try '" + CMD + " help create' for more information"; + return noOptions() && regType == null && file == null && rawSets.isEmpty(); } + @Override protected String help() { return usage(); } diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/DeleteCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/DeleteCmd.java index 457e5e0064..3337cbb843 100644 --- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/DeleteCmd.java +++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/DeleteCmd.java @@ -17,17 +17,14 @@ package org.keycloak.client.registration.cli.commands; -import org.jboss.aesh.cl.Arguments; -import org.jboss.aesh.cl.CommandDefinition; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; import org.keycloak.client.registration.cli.config.ConfigData; import org.keycloak.client.registration.cli.util.ParseUtil; import java.io.PrintWriter; import java.io.StringWriter; -import java.util.List; + +import picocli.CommandLine.Command; +import picocli.CommandLine.Parameters; import static org.keycloak.client.registration.cli.util.AuthUtil.ensureToken; import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; @@ -39,92 +36,68 @@ import static org.keycloak.client.registration.cli.util.HttpUtil.doDelete; import static org.keycloak.client.registration.cli.util.HttpUtil.urlencode; import static org.keycloak.client.registration.cli.util.IoUtil.warnfErr; import static org.keycloak.client.registration.cli.util.OsUtil.CMD; -import static org.keycloak.client.registration.cli.util.OsUtil.EOL; import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT; /** * @author Marko Strukelj */ -@CommandDefinition(name = "delete", description = "CLIENT [GLOBAL_OPTIONS]") +@Command(name = "delete", description = "CLIENT [GLOBAL_OPTIONS]") public class DeleteCmd extends AbstractAuthOptionsCmd { - @Arguments - private List args; + @Parameters(arity = "0..1") + String clientId; @Override - public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - try { - if (printHelp()) { - return help ? CommandResult.SUCCESS : CommandResult.FAILURE; - } - - processGlobalOptions(); - - if (args == null || args.isEmpty()) { - throw new IllegalArgumentException("CLIENT not specified"); - } - - if (args.size() > 1) { - throw new IllegalArgumentException("Invalid option: " + args.get(1)); - } - - String clientId = args.get(0); - - if (clientId.startsWith("-")) { - warnfErr(ParseUtil.CLIENT_OPTION_WARN, clientId); - } - - String regType = "default"; - - ConfigData config = loadConfig(); - config = copyWithServerInfo(config); - - if (token == null) { - // if registration access token is not set via -t, try use the one from configuration - token = getRegistrationToken(config.sessionRealmConfigData(), clientId); - } - - setupTruststore(config, commandInvocation); - - String auth = token; - if (auth == null) { - config = ensureAuthInfo(config, commandInvocation); - config = copyWithServerInfo(config); - if (credentialsAvailable(config)) { - auth = ensureToken(config); - } - } - - auth = auth != null ? "Bearer " + auth : null; - - - final String server = config.getServerUrl(); - final String realm = config.getRealm(); - - doDelete(server + "/realms/" + realm + "/clients-registrations/" + regType + "/" + urlencode(clientId), auth); - - saveMergeConfig(cfg -> { - cfg.ensureRealmConfigData(server, realm).getClients().remove(clientId); - }); - return CommandResult.SUCCESS; - - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e); - } finally { - commandInvocation.stop(); + protected void process() { + if (clientId == null) { + throw new IllegalArgumentException("CLIENT not specified"); } + + if (clientId.startsWith("-")) { + warnfErr(ParseUtil.CLIENT_OPTION_WARN, clientId); + } + + String regType = "default"; + + ConfigData config = loadConfig(); + config = copyWithServerInfo(config); + + if (token == null) { + // if registration access token is not set via -t, try use the one from configuration + token = getRegistrationToken(config.sessionRealmConfigData(), clientId); + } + + setupTruststore(config); + + String auth = token; + if (auth == null) { + config = ensureAuthInfo(config); + config = copyWithServerInfo(config); + if (credentialsAvailable(config)) { + auth = ensureToken(config); + } + } + + auth = auth != null ? "Bearer " + auth : null; + + + final String server = config.getServerUrl(); + final String realm = config.getRealm(); + + doDelete(server + "/realms/" + realm + "/clients-registrations/" + regType + "/" + urlencode(clientId), auth); + + saveMergeConfig(cfg -> { + cfg.ensureRealmConfigData(server, realm).getClients().remove(clientId); + }); } @Override protected boolean nothingToDo() { - return noOptions() && (args == null || args.size() == 0); - } - - protected String suggestHelp() { - return EOL + "Try '" + CMD + " help delete' for more information"; + return noOptions() && clientId == null; } + @Override protected String help() { return usage(); } diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/GetCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/GetCmd.java index 2a9ee8ec88..fd8ba9b5df 100644 --- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/GetCmd.java +++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/GetCmd.java @@ -17,12 +17,6 @@ package org.keycloak.client.registration.cli.commands; -import org.jboss.aesh.cl.Arguments; -import org.jboss.aesh.cl.CommandDefinition; -import org.jboss.aesh.cl.Option; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; import org.keycloak.client.registration.cli.config.ConfigData; import org.keycloak.client.registration.cli.common.EndpointType; import org.keycloak.client.registration.cli.util.ParseUtil; @@ -35,7 +29,10 @@ import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.io.StringWriter; -import java.util.List; + +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; import static org.keycloak.client.registration.cli.util.AuthUtil.ensureToken; import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; @@ -51,142 +48,118 @@ import static org.keycloak.client.registration.cli.util.IoUtil.warnfErr; import static org.keycloak.client.registration.cli.util.IoUtil.printOut; import static org.keycloak.client.registration.cli.util.IoUtil.readFully; import static org.keycloak.client.registration.cli.util.OsUtil.CMD; -import static org.keycloak.client.registration.cli.util.OsUtil.EOL; import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT; /** * @author Marko Strukelj */ -@CommandDefinition(name = "get", description = "[ARGUMENTS]") +@Command(name = "get", description = "[ARGUMENTS]") public class GetCmd extends AbstractAuthOptionsCmd { - @Option(shortName = 'c', name = "compressed", description = "Print full stack trace when exiting with error", hasValue = false) + @Option(names = {"-c", "--compressed"}, description = "Print full stack trace when exiting with error") private boolean compressed = false; - @Option(shortName = 'e', name = "endpoint", description = "Endpoint type to use", hasValue = true) + @Option(names = {"-e", "--endpoint"}, description = "Endpoint type to use") private String endpoint; - @Arguments - private List args; + @Parameters(arity = "0..1") + String clientId; @Override - public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { + protected void process() { + if (clientId == null) { + throw new IllegalArgumentException("CLIENT not specified"); + } + + EndpointType regType = endpoint != null ? EndpointType.of(endpoint) : EndpointType.DEFAULT; + + + if (clientId.startsWith("-")) { + warnfErr(ParseUtil.CLIENT_OPTION_WARN, clientId); + } + + ConfigData config = loadConfig(); + config = copyWithServerInfo(config); + + if (token == null) { + // if registration access token is not set via -t, try use the one from configuration + token = getRegistrationToken(config.sessionRealmConfigData(), clientId); + } + + setupTruststore(config); + + String auth = token; + if (auth == null) { + config = ensureAuthInfo(config); + config = copyWithServerInfo(config); + if (credentialsAvailable(config)) { + auth = ensureToken(config); + } + } + + auth = auth != null ? "Bearer " + auth : null; + + + final String server = config.getServerUrl(); + final String realm = config.getRealm(); + + InputStream response = doGet(server + "/realms/" + realm + "/clients-registrations/" + regType.getEndpoint() + "/" + urlencode(clientId), + APPLICATION_JSON, auth); try { - if (printHelp()) { - return help ? CommandResult.SUCCESS : CommandResult.FAILURE; - } + String json = readFully(response); + Object result = null; - processGlobalOptions(); + switch (regType) { + case DEFAULT: { + ClientRepresentation client = JsonSerialization.readValue(json, ClientRepresentation.class); + result = client; - if (args == null || args.isEmpty()) { - throw new IllegalArgumentException("CLIENT not specified"); - } + saveMergeConfig(cfg -> { + setRegistrationToken(cfg.ensureRealmConfigData(server, realm), client.getClientId(), client.getRegistrationAccessToken()); + }); + break; + } + case OIDC: { + OIDCClientRepresentation client = JsonSerialization.readValue(json, OIDCClientRepresentation.class); + result = client; - if (args.size() > 1) { - throw new IllegalArgumentException("Invalid option: " + args.get(1)); - } - - String clientId = args.get(0); - EndpointType regType = endpoint != null ? EndpointType.of(endpoint) : EndpointType.DEFAULT; - - - if (clientId.startsWith("-")) { - warnfErr(ParseUtil.CLIENT_OPTION_WARN, clientId); - } - - ConfigData config = loadConfig(); - config = copyWithServerInfo(config); - - if (token == null) { - // if registration access token is not set via -t, try use the one from configuration - token = getRegistrationToken(config.sessionRealmConfigData(), clientId); - } - - setupTruststore(config, commandInvocation); - - String auth = token; - if (auth == null) { - config = ensureAuthInfo(config, commandInvocation); - config = copyWithServerInfo(config); - if (credentialsAvailable(config)) { - auth = ensureToken(config); + saveMergeConfig(cfg -> { + setRegistrationToken(cfg.ensureRealmConfigData(server, realm), client.getClientId(), client.getRegistrationAccessToken()); + }); + break; + } + case INSTALL: { + result = JsonSerialization.readValue(json, AdapterConfig.class); + break; + } + case SAML2: { + break; + } + default: { + throw new RuntimeException("Unexpected type: " + regType); } } - auth = auth != null ? "Bearer " + auth : null; - - - final String server = config.getServerUrl(); - final String realm = config.getRealm(); - - InputStream response = doGet(server + "/realms/" + realm + "/clients-registrations/" + regType.getEndpoint() + "/" + urlencode(clientId), - APPLICATION_JSON, auth); - - try { - String json = readFully(response); - Object result = null; - - switch (regType) { - case DEFAULT: { - ClientRepresentation client = JsonSerialization.readValue(json, ClientRepresentation.class); - result = client; - - saveMergeConfig(cfg -> { - setRegistrationToken(cfg.ensureRealmConfigData(server, realm), client.getClientId(), client.getRegistrationAccessToken()); - }); - break; - } - case OIDC: { - OIDCClientRepresentation client = JsonSerialization.readValue(json, OIDCClientRepresentation.class); - result = client; - - saveMergeConfig(cfg -> { - setRegistrationToken(cfg.ensureRealmConfigData(server, realm), client.getClientId(), client.getRegistrationAccessToken()); - }); - break; - } - case INSTALL: { - result = JsonSerialization.readValue(json, AdapterConfig.class); - break; - } - case SAML2: { - break; - } - default: { - throw new RuntimeException("Unexpected type: " + regType); - } - } - - if (!compressed && result != null) { - json = JsonSerialization.writeValueAsPrettyString(result); - } - - printOut(json); - - //} catch (UnrecognizedPropertyException e) { - // throw new RuntimeException("Failed to parse returned JSON - " + e.getMessage(), e); - } catch (IOException e) { - throw new RuntimeException("Failed to process HTTP response", e); + if (!compressed && result != null) { + json = JsonSerialization.writeValueAsPrettyString(result); } - return CommandResult.SUCCESS; - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e); - } finally { - commandInvocation.stop(); + printOut(json); + + //} catch (UnrecognizedPropertyException e) { + // throw new RuntimeException("Failed to parse returned JSON - " + e.getMessage(), e); + } catch (IOException e) { + throw new RuntimeException("Failed to process HTTP response", e); } } @Override protected boolean nothingToDo() { - return noOptions() && endpoint == null && (args == null || args.size() == 0); - } - - protected String suggestHelp() { - return EOL + "Try '" + CMD + " help get' for more information"; + return noOptions() && endpoint == null && clientId == null; } + @Override protected String help() { return usage(); } diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/HelpCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/HelpCmd.java index 820f84ac7a..ca8cc7e748 100644 --- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/HelpCmd.java +++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/HelpCmd.java @@ -1,90 +1,80 @@ package org.keycloak.client.registration.cli.commands; -import org.jboss.aesh.cl.Arguments; -import org.jboss.aesh.cl.CommandDefinition; -import org.jboss.aesh.console.command.Command; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; - import java.util.List; +import picocli.CommandLine.Command; +import picocli.CommandLine.Parameters; + import static org.keycloak.client.registration.cli.util.IoUtil.printOut; /** * @author Marko Strukelj */ -@CommandDefinition(name = "help", description = "This help") -public class HelpCmd implements Command { +@Command(name = "help", description = "This help") +public class HelpCmd implements Runnable { - @Arguments + @Parameters private List args; @Override - public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - try { - if (args == null || args.size() == 0) { - printOut(KcRegCmd.usage()); - } else { - outer: - switch (args.get(0)) { - case "config": { - if (args.size() > 1) { - switch (args.get(1)) { - case "credentials": { - printOut(ConfigCredentialsCmd.usage()); - break outer; - } - case "initial-token": { - printOut(ConfigInitialTokenCmd.usage()); - break outer; - } - case "registration-token": { - printOut(ConfigRegistrationTokenCmd.usage()); - break outer; - } - case "truststore": { - printOut(ConfigTruststoreCmd.usage()); - break outer; - } + public void run() { + if (args == null || args.size() == 0) { + printOut(KcRegCmd.usage()); + } else { + outer: + switch (args.get(0)) { + case "config": { + if (args.size() > 1) { + switch (args.get(1)) { + case "credentials": { + printOut(ConfigCredentialsCmd.usage()); + break outer; + } + case "initial-token": { + printOut(ConfigInitialTokenCmd.usage()); + break outer; + } + case "registration-token": { + printOut(ConfigRegistrationTokenCmd.usage()); + break outer; + } + case "truststore": { + printOut(ConfigTruststoreCmd.usage()); + break outer; } } - printOut(ConfigCmd.usage()); - break; - } - case "create": { - printOut(CreateCmd.usage()); - break; - } - case "get": { - printOut(GetCmd.usage()); - break; - } - case "update": { - printOut(UpdateCmd.usage()); - break; - } - case "delete": { - printOut(DeleteCmd.usage()); - break; - } - case "attrs": { - printOut(AttrsCmd.usage()); - break; - } - case "update-token": { - printOut(UpdateTokenCmd.usage()); - break; - } - default: { - throw new RuntimeException("Unknown command: " + args.get(0)); } + printOut(ConfigCmd.usage()); + break; + } + case "create": { + printOut(CreateCmd.usage()); + break; + } + case "get": { + printOut(GetCmd.usage()); + break; + } + case "update": { + printOut(UpdateCmd.usage()); + break; + } + case "delete": { + printOut(DeleteCmd.usage()); + break; + } + case "attrs": { + printOut(AttrsCmd.usage()); + break; + } + case "update-token": { + printOut(UpdateTokenCmd.usage()); + break; + } + default: { + throw new IllegalArgumentException("Unknown command: " + args.get(0)); } } - - return CommandResult.SUCCESS; - } finally { - commandInvocation.stop(); } } } diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/KcRegCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/KcRegCmd.java index 4224af710e..d2d2245abd 100644 --- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/KcRegCmd.java +++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/KcRegCmd.java @@ -16,14 +16,11 @@ */ package org.keycloak.client.registration.cli.commands; -import org.jboss.aesh.cl.GroupCommandDefinition; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; - import java.io.PrintWriter; import java.io.StringWriter; +import picocli.CommandLine.Command; + import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.registration.cli.util.IoUtil.printOut; import static org.keycloak.client.registration.cli.util.OsUtil.CMD; @@ -33,27 +30,30 @@ import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT; * @author Marko Strukelj */ -@GroupCommandDefinition(name = "kcreg", description = "COMMAND [ARGUMENTS]", groupCommands = { - HelpCmd.class, ConfigCmd.class, CreateCmd.class, UpdateCmd.class, GetCmd.class, DeleteCmd.class, AttrsCmd.class, UpdateTokenCmd.class} ) +@Command(name = "kcreg", +header = { + "Keycloak - Open Source Identity and Access Management", + "", + "Find more information at: https://www.keycloak.org/docs/latest" +}, +description = { + "%nCOMMAND [ARGUMENTS]" +}, +subcommands = { + HelpCmd.class, + ConfigCmd.class, + CreateCmd.class, + GetCmd.class, + UpdateCmd.class, + DeleteCmd.class, + AttrsCmd.class, + UpdateTokenCmd.class +}) public class KcRegCmd extends AbstractGlobalOptionsCmd { - - //@Arguments - //private List args; - + @Override - public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - try { - // if --help was requested then status is SUCCESS - // if not we print help anyway, but status is FAILURE - if (printHelp()) { - return CommandResult.SUCCESS; - } else { - printOut(usage()); - return CommandResult.FAILURE; - } - } finally { - commandInvocation.stop(); - } + protected boolean nothingToDo() { + return true; } public static String usage() { diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/UpdateCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/UpdateCmd.java index 4704608a94..c5e3c9e480 100644 --- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/UpdateCmd.java +++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/UpdateCmd.java @@ -18,13 +18,13 @@ package org.keycloak.client.registration.cli.commands; import com.fasterxml.jackson.core.JsonParseException; -import org.jboss.aesh.cl.Arguments; -import org.jboss.aesh.cl.CommandDefinition; -import org.jboss.aesh.cl.Option; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; -import org.keycloak.client.registration.cli.aesh.EndpointTypeConverter; + +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +import org.keycloak.client.registration.cli.EndpointTypeConverter; import org.keycloak.client.registration.cli.common.AttributeOperation; import org.keycloak.client.registration.cli.config.ConfigData; import org.keycloak.client.registration.cli.common.CmdStdinContext; @@ -39,7 +39,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.io.StringWriter; -import java.util.Iterator; +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; @@ -62,7 +62,6 @@ import static org.keycloak.client.registration.cli.util.IoUtil.warnfErr; import static org.keycloak.client.registration.cli.util.IoUtil.readFully; import static org.keycloak.client.registration.cli.util.HttpUtil.APPLICATION_JSON; import static org.keycloak.client.registration.cli.util.OsUtil.CMD; -import static org.keycloak.client.registration.cli.util.OsUtil.EOL; 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; @@ -71,258 +70,231 @@ import static org.keycloak.client.registration.cli.util.ParseUtil.parseKeyVal; /** * @author Marko Strukelj */ -@CommandDefinition(name = "update", description = "CLIENT_ID [ARGUMENTS]") +@Command(name = "update", description = "CLIENT_ID [ARGUMENTS]") public class UpdateCmd extends AbstractAuthOptionsCmd { - @Option(shortName = 'e', name = "endpoint", description = "Endpoint type to use - one of: 'default', 'oidc'", hasValue = true, converter = EndpointTypeConverter.class) + @Option(names = {"-e", "--endpoint"}, description = "Endpoint type to use - one of: 'default', 'oidc'", converter = EndpointTypeConverter.class) private EndpointType regType = null; - //@GroupOption(shortName = 's', name = "set", description = "Set specific attribute to a specified value", hasValue = true) - //private List attributes = new ArrayList<>(); - - @Option(shortName = 'f', name = "file", description = "Use the file or standard input if '-' is specified", hasValue = true) + @Option(names = {"-f", "--file"}, description = "Use the file or standard input if '-' is specified") private String file = null; - @Option(shortName = 'm', name = "merge", description = "Merge new values with existing configuration on the server", hasValue = false) - private boolean mergeMode = true; + @Option(names = {"-m", "--merge"}, description = "Merge new values with existing configuration on the server") + private boolean mergeMode = false; - @Option(shortName = 'o', name = "output", description = "After update output the new client configuration", hasValue = false) + @Option(names = {"-o", "--output"}, description = "After update output the new client configuration") private boolean outputClient = false; - @Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false) + @Option(names = {"-c", "--compressed"}, description = "Don't pretty print the output") private boolean compressed = false; - @Arguments - private List args; + @Parameters(arity = "0..1") + String clientId; + // to maintain relative positions of set and delete operations + static class AttributeOperations { + @Option(names = {"-s", "--set"}, required = true) String set; + @Option(names = {"-d", "--delete"}, required = true) String delete; + } + + @ArgGroup(exclusive = true, multiplicity = "0..*") + List rawAttributeOperations = new ArrayList<>(); + + List attrs = new LinkedList<>(); @Override - public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { + protected void processOptions() { + super.processOptions(); - List attrs = new LinkedList<>(); - - try { - if (printHelp()) { - return help ? CommandResult.SUCCESS : CommandResult.FAILURE; + for (AttributeOperations entry : rawAttributeOperations) { + if (entry.delete != null) { + attrs.add(new AttributeOperation(DELETE, entry.delete)); + } else { + String[] keyVal = parseKeyVal(entry.set); + attrs.add(new AttributeOperation(SET, keyVal[0], keyVal[1])); } + } + } - processGlobalOptions(); + @Override + protected void process() { + if (clientId == null) { + throw new IllegalArgumentException("CLIENT_ID not specified"); + } - String clientId = null; + if (clientId.startsWith("-")) { + warnfErr(ParseUtil.CLIENT_OPTION_WARN, clientId); + } - if (args != null) { - Iterator it = args.iterator(); - if (!it.hasNext()) { - throw new IllegalArgumentException("CLIENT_ID not specified"); - } + if (file == null && attrs.size() == 0) { + throw new IllegalArgumentException("No file nor attribute values specified"); + } - clientId = it.next(); + // We have several options for update: + // + // A) if a file is specified, then we can overwrite server state with that file + // (that's the normal flow - get and save locally, edit, post to server) + // + // update my_client -f new_client.json + // + // B) if a file is specified, and overrides are specified, then we override the file values with those from command line + // (that allows us to have a local file as a template, it's also batch job friendly) + // + // update my_client -s public=true -s enableDirectGrants=false -f new_client.json + // + // C) if no file is specified, then we can fetch the client definition from server, apply changes to it, and post it back + // (again a batch job friendly mode) + // + // update my_client -s public=true -s enableDirectGrants=false + // + // This is merge mode by default - if --merge is additionally specified, it is ignored + // + // D) if a file is specified, then we can merge the file with current state on the server + // (that is similar to previous mode except that the overrides are also taken from a file) + // + // update my_client --merge -f new_client.json + // update my_client --merge -s public=true -s enableDirectGrants=false -f new_client.json + // + // We could also support environment variables in input file, and apply them before parsing it. + // + // One problem - what if it is SAML XML? No problem as we don't support update for SAML - only create. + // + if (file == null && attrs.size() > 0) { + mergeMode = true; + } - if (clientId.startsWith("-")) { - warnfErr(ParseUtil.CLIENT_OPTION_WARN, clientId); - } + CmdStdinContext ctx = new CmdStdinContext(); + if (file != null) { + ctx = parseFileOrStdin(file, regType); + regType = ctx.getEndpointType(); + } - while (it.hasNext()) { - String option = it.next(); - switch (option) { - case "-s": - case "--set": { - if (!it.hasNext()) { - throw new IllegalArgumentException("Option " + option + " requires a value"); - } - String[] keyVal = parseKeyVal(it.next()); - attrs.add(new AttributeOperation(SET, keyVal[0], keyVal[1])); - break; - } - case "-d": - case "--delete": { - attrs.add(new AttributeOperation(DELETE, it.next())); - break; - } - default: { - throw new IllegalArgumentException("Unsupported option: " + option); - } + if (regType == null) { + regType = DEFAULT; + ctx.setEndpointType(regType); + } else if (regType != DEFAULT && regType != OIDC) { + throw new RuntimeException("Update not supported for endpoint type: " + regType.getEndpoint()); + } + + // initialize config only after reading from stdin, + // to allow proper operation when piping 'get' - which consumes the old + // registration access token, and saves the new one to the config + ConfigData config = loadConfig(); + config = copyWithServerInfo(config); + + final String server = config.getServerUrl(); + final String realm = config.getRealm(); + + if (token == null) { + // 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 + boolean processed = false; + for (AttributeOperation op: attrs) { + if ("registrationAccessToken".equals(op.getKey().toString())) { + processed = true; + if (op.getType() == AttributeOperation.Type.SET) { + token = op.getValue(); } + // otherwise it's delete - meaning it should stay null + break; } } - - if (file == null && attrs.size() == 0) { - throw new IllegalArgumentException("No file nor attribute values specified"); + if (!processed) { + token = ctx.getRegistrationAccessToken(); } + } - // We have several options for update: - // - // A) if a file is specified, then we can overwrite server state with that file - // (that's the normal flow - get and save locally, edit, post to server) - // - // update my_client -f new_client.json - // - // B) if a file is specified, and overrides are specified, then we override the file values with those from command line - // (that allows us to have a local file as a template, it's also batch job friendly) - // - // update my_client -s public=true -s enableDirectGrants=false -f new_client.json - // - // C) if no file is specified, then we can fetch the client definition from server, apply changes to it, and post it back - // (again a batch job friendly mode) - // - // update my_client -s public=true -s enableDirectGrants=false - // - // This is merge mode by default - if --merge is additionally specified, it is ignored - // - // D) if a file is specified, then we can merge the file with current state on the server - // (that is similar to previous mode except that the overrides are also taken from a file) - // - // update my_client --merge -f new_client.json - // update my_client --merge -s public=true -s enableDirectGrants=false -f new_client.json - // - // We could also support environment variables in input file, and apply them before parsing it. - // - // One problem - what if it is SAML XML? No problem as we don't support update for SAML - only create. - // - if (file == null && attrs.size() > 0) { - mergeMode = true; - } + if (token == null) { + // if registration access token is not set, try use the one from configuration + token = getRegistrationToken(config.sessionRealmConfigData(), clientId); + } - CmdStdinContext ctx = new CmdStdinContext(); - if (file != null) { - ctx = parseFileOrStdin(file, regType); - regType = ctx.getEndpointType(); - } + setupTruststore(config); - if (regType == null) { - regType = DEFAULT; - ctx.setEndpointType(regType); - } else if (regType != DEFAULT && regType != OIDC) { - throw new RuntimeException("Update not supported for endpoint type: " + regType.getEndpoint()); - } - - // initialize config only after reading from stdin, - // to allow proper operation when piping 'get' - which consumes the old - // registration access token, and saves the new one to the config - ConfigData config = loadConfig(); + String auth = token; + if (auth == null) { + config = ensureAuthInfo(config); config = copyWithServerInfo(config); - - final String server = config.getServerUrl(); - final String realm = config.getRealm(); - - if (token == null) { - // 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 - boolean processed = false; - for (AttributeOperation op: attrs) { - if ("registrationAccessToken".equals(op.getKey().toString())) { - processed = true; - if (op.getType() == AttributeOperation.Type.SET) { - token = op.getValue(); - } - // otherwise it's delete - meaning it should stay null - break; - } - } - if (!processed) { - token = ctx.getRegistrationAccessToken(); - } + if (credentialsAvailable(config)) { + auth = ensureToken(config); } + } - if (token == null) { - // if registration access token is not set, try use the one from configuration - token = getRegistrationToken(config.sessionRealmConfigData(), clientId); - } - - setupTruststore(config, commandInvocation); - - String auth = token; - if (auth == null) { - config = ensureAuthInfo(config, commandInvocation); - config = copyWithServerInfo(config); - if (credentialsAvailable(config)) { - auth = ensureToken(config); - } - } - - auth = auth != null ? "Bearer " + auth : null; + auth = auth != null ? "Bearer " + auth : null; - if (mergeMode) { - InputStream response = doGet(server + "/realms/" + realm + "/clients-registrations/" + regType.getEndpoint() + "/" + urlencode(clientId), - APPLICATION_JSON, auth); + if (mergeMode) { + InputStream response = doGet(server + "/realms/" + realm + "/clients-registrations/" + regType.getEndpoint() + "/" + urlencode(clientId), + APPLICATION_JSON, auth); - String json = readFully(response); + String json = readFully(response); - CmdStdinContext ctxremote = new CmdStdinContext(); - ctxremote.setContent(json); - ctxremote.setEndpointType(regType); - try { - - if (regType == DEFAULT) { - ctxremote.setClient(JsonSerialization.readValue(json, ClientRepresentation.class)); - token = ctxremote.getClient().getRegistrationAccessToken(); - } else if (regType == OIDC) { - ctxremote.setOidcClient(JsonSerialization.readValue(json, OIDCClientRepresentation.class)); - token = ctxremote.getOidcClient().getRegistrationAccessToken(); - } - } catch (JsonParseException e) { - throw new RuntimeException("Not a valid JSON document. " + e.getMessage(), e); - } catch (IOException e) { - throw new RuntimeException("Not a valid JSON document", e); - } - - // we have to use registration access token retrieved from previous operation - // that ensures optimistic locking semantics - if (token != null) { - // we use auth with doPost later - auth = "Bearer " + token; - - String newToken = token; - String clientToUpdate = clientId; - saveMergeConfig(cfg -> { - setRegistrationToken(cfg.ensureRealmConfigData(server, realm), clientToUpdate, newToken); - }); - } - - // merge local representation over remote one - if (ctx.getClient() != null) { - ReflectionUtil.merge(ctx.getClient(), ctxremote.getClient()); - } else if (ctx.getOidcClient() != null) { - ReflectionUtil.merge(ctx.getOidcClient(), ctxremote.getOidcClient()); - } - ctx = ctxremote; - } - - if (attrs.size() > 0) { - ctx = mergeAttributes(ctx, attrs); - } - - // now update - InputStream response = doPut(server + "/realms/" + realm + "/clients-registrations/" + regType.getEndpoint() + "/" + urlencode(clientId), - APPLICATION_JSON, APPLICATION_JSON, ctx.getContent(), auth); + CmdStdinContext ctxremote = new CmdStdinContext(); + ctxremote.setContent(json); + ctxremote.setEndpointType(regType); try { + if (regType == DEFAULT) { - ClientRepresentation clirep = JsonSerialization.readValue(response, ClientRepresentation.class); - outputResult(clirep); - token = clirep.getRegistrationAccessToken(); + ctxremote.setClient(JsonSerialization.readValue(json, ClientRepresentation.class)); + token = ctxremote.getClient().getRegistrationAccessToken(); } else if (regType == OIDC) { - OIDCClientRepresentation clirep = JsonSerialization.readValue(response, OIDCClientRepresentation.class); - outputResult(clirep); - token = clirep.getRegistrationAccessToken(); + ctxremote.setOidcClient(JsonSerialization.readValue(json, OIDCClientRepresentation.class)); + token = ctxremote.getOidcClient().getRegistrationAccessToken(); } + } catch (JsonParseException e) { + throw new RuntimeException("Not a valid JSON document. " + e.getMessage(), e); + } catch (IOException e) { + throw new RuntimeException("Not a valid JSON document", e); + } + + // we have to use registration access token retrieved from previous operation + // that ensures optimistic locking semantics + if (token != null) { + // we use auth with doPost later + auth = "Bearer " + token; String newToken = token; String clientToUpdate = clientId; saveMergeConfig(cfg -> { setRegistrationToken(cfg.ensureRealmConfigData(server, realm), clientToUpdate, newToken); }); - - } catch (IOException e) { - throw new RuntimeException("Failed to process HTTP response", e); } - return CommandResult.SUCCESS; + // merge local representation over remote one + if (ctx.getClient() != null) { + ReflectionUtil.merge(ctx.getClient(), ctxremote.getClient()); + } else if (ctx.getOidcClient() != null) { + ReflectionUtil.merge(ctx.getOidcClient(), ctxremote.getOidcClient()); + } + ctx = ctxremote; + } - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e); - } finally { - commandInvocation.stop(); + if (attrs.size() > 0) { + ctx = mergeAttributes(ctx, attrs); + } + + // now update + InputStream response = doPut(server + "/realms/" + realm + "/clients-registrations/" + regType.getEndpoint() + "/" + urlencode(clientId), + APPLICATION_JSON, APPLICATION_JSON, ctx.getContent(), auth); + try { + if (regType == DEFAULT) { + ClientRepresentation clirep = JsonSerialization.readValue(response, ClientRepresentation.class); + outputResult(clirep); + token = clirep.getRegistrationAccessToken(); + } else if (regType == OIDC) { + OIDCClientRepresentation clirep = JsonSerialization.readValue(response, OIDCClientRepresentation.class); + outputResult(clirep); + token = clirep.getRegistrationAccessToken(); + } + + String newToken = token; + String clientToUpdate = clientId; + saveMergeConfig(cfg -> { + setRegistrationToken(cfg.ensureRealmConfigData(server, realm), clientToUpdate, newToken); + }); + + } catch (IOException e) { + throw new RuntimeException("Failed to process HTTP response", e); } } @@ -338,13 +310,10 @@ public class UpdateCmd extends AbstractAuthOptionsCmd { @Override protected boolean nothingToDo() { - return noOptions() && regType == null && file == null && (args == null || args.size() == 0); - } - - protected String suggestHelp() { - return EOL + "Try '" + CMD + " help update' for more information"; + return noOptions() && regType == null && file == null && rawAttributeOperations.isEmpty() && clientId == null; } + @Override protected String help() { return usage(); } diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/UpdateTokenCmd.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/UpdateTokenCmd.java index e555638c70..85f35054b2 100644 --- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/UpdateTokenCmd.java +++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/commands/UpdateTokenCmd.java @@ -18,11 +18,10 @@ package org.keycloak.client.registration.cli.commands; import com.fasterxml.jackson.core.type.TypeReference; -import org.jboss.aesh.cl.Arguments; -import org.jboss.aesh.cl.CommandDefinition; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; + +import picocli.CommandLine.Command; +import picocli.CommandLine.Parameters; + import org.keycloak.client.registration.cli.config.ConfigData; import org.keycloak.client.registration.cli.util.ParseUtil; import org.keycloak.representations.idm.ClientRepresentation; @@ -45,105 +44,82 @@ import static org.keycloak.client.registration.cli.util.HttpUtil.doPost; import static org.keycloak.client.registration.cli.util.IoUtil.printOut; import static org.keycloak.client.registration.cli.util.IoUtil.warnfOut; import static org.keycloak.client.registration.cli.util.OsUtil.CMD; -import static org.keycloak.client.registration.cli.util.OsUtil.EOL; import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT; /** * @author Marko Strukelj */ -@CommandDefinition(name = "update-token", description = "CLIENT [ARGUMENTS]") +@Command(name = "update-token", description = "CLIENT [ARGUMENTS]") public class UpdateTokenCmd extends AbstractAuthOptionsCmd { - @Arguments - private List args; + @Parameters(arity = "0..1") + String clientId; @Override - public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { + protected void process() { + if (clientId == null) { + throw new IllegalArgumentException("CLIENT not specified"); + } + + if (clientId.startsWith("-")) { + warnfOut(ParseUtil.CLIENT_OPTION_WARN, clientId); + } + + ConfigData config = loadConfig(); + config = copyWithServerInfo(config); + setupTruststore(config); + + config = ensureAuthInfo(config); + String auth = ensureToken(config); + + String cid = null; + + final String server = config.getServerUrl(); + final String realm = config.getRealm(); + + // first we need to get id of the client with client_id == clientId + InputStream response = doGet(server + "/admin/realms/" + realm + "/clients", APPLICATION_JSON, "Bearer " + auth); + try { + List clients = JsonSerialization.readValue(response, new TypeReference>() {}); + for (ClientRepresentation client: clients) { + if (clientId.equals(client.getClientId())) { + cid = client.getId(); + break; + } + } + } catch (IOException e) { + throw new RuntimeException("Failed to process response from server", e); + } + + if (cid == null) { + throw new RuntimeException("No client found for: " + clientId); + } + + response = doPost(server + "/admin/realms/" + realm + "/clients/" + cid + "/registration-access-token", + APPLICATION_JSON, APPLICATION_JSON, null, "Bearer " + auth); try { - if (printHelp()) { - return help ? CommandResult.SUCCESS : CommandResult.FAILURE; + ClientRepresentation client = JsonSerialization.readValue(response, ClientRepresentation.class); + + if (noconfig) { + // output to stdout + printOut(client.getRegistrationAccessToken()); + } else { + saveMergeConfig(cfg -> { + setRegistrationToken(cfg.ensureRealmConfigData(server, realm), client.getClientId(), client.getRegistrationAccessToken()); + }); } - - processGlobalOptions(); - - if (args == null || args.isEmpty()) { - throw new IllegalArgumentException("CLIENT not specified"); - } - - String clientId = args.get(0); - - if (clientId.startsWith("-")) { - warnfOut(ParseUtil.CLIENT_OPTION_WARN, clientId); - } - - ConfigData config = loadConfig(); - config = copyWithServerInfo(config); - setupTruststore(config, commandInvocation); - - config = ensureAuthInfo(config, commandInvocation); - String auth = ensureToken(config); - - String cid = null; - - final String server = config.getServerUrl(); - final String realm = config.getRealm(); - - // first we need to get id of the client with client_id == clientId - InputStream response = doGet(server + "/admin/realms/" + realm + "/clients", APPLICATION_JSON, "Bearer " + auth); - try { - List clients = JsonSerialization.readValue(response, new TypeReference>() {}); - for (ClientRepresentation client: clients) { - if (clientId.equals(client.getClientId())) { - cid = client.getId(); - break; - } - } - } catch (IOException e) { - throw new RuntimeException("Failed to process response from server", e); - } - - if (cid == null) { - throw new RuntimeException("No client found for: " + clientId); - } - - response = doPost(server + "/admin/realms/" + realm + "/clients/" + cid + "/registration-access-token", - APPLICATION_JSON, APPLICATION_JSON, null, "Bearer " + auth); - - try { - ClientRepresentation client = JsonSerialization.readValue(response, ClientRepresentation.class); - - if (noconfig) { - // output to stdout - printOut(client.getRegistrationAccessToken()); - } else { - saveMergeConfig(cfg -> { - setRegistrationToken(cfg.ensureRealmConfigData(server, realm), client.getClientId(), client.getRegistrationAccessToken()); - }); - } - } catch (IOException e) { - throw new RuntimeException("Failed to process response from server", e); - } - - //System.out.println("Token updated for client " + clientId); - return CommandResult.SUCCESS; - - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e); - } finally { - commandInvocation.stop(); + } catch (IOException e) { + throw new RuntimeException("Failed to process response from server", e); } } @Override protected boolean nothingToDo() { - return noOptions() && (args == null || args.size() == 0); - } - - protected String suggestHelp() { - return EOL + "Try '" + CMD + " help update-token' for more information"; + return noOptions() && clientId == null; } + @Override protected String help() { return usage(); } diff --git a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/IoUtil.java b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/IoUtil.java index 7b38505785..e1c38d38c8 100644 --- a/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/IoUtil.java +++ b/integration/client-cli/client-registration-cli/src/main/java/org/keycloak/client/registration/cli/util/IoUtil.java @@ -17,14 +17,7 @@ package org.keycloak.client.registration.cli.util; -import org.jboss.aesh.console.AeshConsoleBufferBuilder; -import org.jboss.aesh.console.AeshInputProcessorBuilder; -import org.jboss.aesh.console.ConsoleBuffer; -import org.jboss.aesh.console.InputProcessor; -import org.jboss.aesh.console.Prompt; -import org.jboss.aesh.console.command.invocation.CommandInvocation; -import org.keycloak.client.registration.cli.aesh.Globals; - +import java.io.Console; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; @@ -50,7 +43,6 @@ import static java.nio.file.Files.createDirectories; import static java.nio.file.Files.createFile; import static java.nio.file.Files.isDirectory; import static java.nio.file.Files.isRegularFile; -import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH; /** * @author Marko Strukelj @@ -81,43 +73,14 @@ public class IoUtil { } } - public static String readSecret(String prompt, CommandInvocation invocation) { - - // TODO Windows hack - masking not working on Windows - char maskChar = OS_ARCH.isWindows() ? 0 : '*'; - ConsoleBuffer consoleBuffer = new AeshConsoleBufferBuilder() - .shell(invocation.getShell()) - .prompt(new Prompt(prompt, maskChar)) - .create(); - InputProcessor inputProcessor = new AeshInputProcessorBuilder() - .consoleBuffer(consoleBuffer) - .create(); - - consoleBuffer.displayPrompt(); - - // activate stdin - Globals.stdin.setInputStream(System.in); - - String result; - try { - do { - result = inputProcessor.parseOperation(invocation.getInput()); - } while (result == null); - } catch (Exception e) { - throw new RuntimeException("^C", e); + public static String readSecret(String prompt) { + Console cons; + char[] passwd; + if ((cons = System.console()) != null && + (passwd = cons.readPassword("%s", prompt)) != null) { + return new String(passwd); } - /* - if (!Globals.stdin.isStdinAvailable()) { - try { - return readLine(new InputStreamReader(System.in)); - } catch (IOException e) { - throw new RuntimeException("Standard input not available"); - } - } - */ - // Windows hack - get rid of any \n - result = result.replaceAll("\\n", ""); - return result; + throw new RuntimeException("Console is not active, or no password provided"); } public static String readFully(InputStream is) { diff --git a/pom.xml b/pom.xml index 7bfdb058b9..a84ffe4997 100644 --- a/pom.xml +++ b/pom.xml @@ -80,7 +80,6 @@ 7.5.22.Final-redhat-1 - 0.66.19 4.5.14 1.5.1.Final @@ -948,11 +947,6 @@ pax-web-spi ${pax.web.version} - - org.jboss.aesh - aesh - ${jboss.aesh.version} - diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegConfigTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegConfigTest.java index e189a292d3..6d0c766993 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegConfigTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegConfigTest.java @@ -20,27 +20,27 @@ public class KcRegConfigTest extends AbstractRegCliTest { @Test public void testRegistrationToken() throws IOException { - FileConfigHandler handler = initCustomConfigFile(); + initCustomConfigFile(); - try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) { + try (TempFileResource configFile = new TempFileResource(FileConfigHandler.getConfigFile())) { // without --server KcRegExec exe = execute("config registration-token --config '" + configFile.getName() + "' "); - assertExitCodeAndStreamSizes(exe, 1, 0, 2); + assertExitCodeAndStreamSizes(exe, 2, 0, 2); Assert.assertEquals("error message", "Required option not specified: --server", exe.stderrLines().get(0)); - Assert.assertEquals("try help", "Try '" + CMD + " help config registration-token' for more information", exe.stderrLines().get(1)); + Assert.assertEquals("try help", "Try '" + CMD + " config registration-token --help' for more information on the available options.", exe.stderrLines().get(1)); // without --realm exe = execute("config registration-token --config '" + configFile.getName() + "' --server http://localhost:8080/auth"); - assertExitCodeAndStreamSizes(exe, 1, 0, 2); + assertExitCodeAndStreamSizes(exe, 2, 0, 2); Assert.assertEquals("error message", "Required option not specified: --realm", exe.stderrLines().get(0)); - Assert.assertEquals("try help", "Try '" + CMD + " help config registration-token' for more information", exe.stderrLines().get(1)); + Assert.assertEquals("try help", "Try '" + CMD + " config registration-token --help' for more information on the available options.", exe.stderrLines().get(1)); // without --client exe = execute("config registration-token --config '" + configFile.getName() + "' --server http://localhost:8080/auth --realm test"); - assertExitCodeAndStreamSizes(exe, 1, 0, 2); + assertExitCodeAndStreamSizes(exe, 2, 0, 2); Assert.assertEquals("error message", "Required option not specified: --client", exe.stderrLines().get(0)); - Assert.assertEquals("try help", "Try '" + CMD + " help config registration-token' for more information", exe.stderrLines().get(1)); + Assert.assertEquals("try help", "Try '" + CMD + " config registration-token --help' for more information on the available options.", exe.stderrLines().get(1)); // specify token on cmdline exe = execute("config registration-token --config '" + configFile.getName() + "' --server http://localhost:8080/auth --realm test --client my_client NEWTOKEN"); @@ -75,14 +75,14 @@ public class KcRegConfigTest extends AbstractRegCliTest { public void testNoConfigOption() throws IOException { KcRegExec exe = execute("config registration-token --no-config --server http://localhost:8080/auth --realm test --client my_client --delete"); - assertExitCodeAndStreamSizes(exe, 1, 0, 2); + assertExitCodeAndStreamSizes(exe, 2, 0, 2); Assert.assertEquals("stderr first line", "Unsupported option: --no-config", exe.stderrLines().get(0)); - Assert.assertEquals("try help", "Try '" + CMD + " help config registration-token' for more information", exe.stderrLines().get(1)); + Assert.assertEquals("try help", "Try '" + CMD + " config registration-token --help' for more information on the available options.", exe.stderrLines().get(1)); exe = execute("config initial-token --no-config --server http://localhost:8080/auth --realm test --delete"); - assertExitCodeAndStreamSizes(exe, 1, 0, 2); + assertExitCodeAndStreamSizes(exe, 2, 0, 2); Assert.assertEquals("stderr first line", "Unsupported option: --no-config", exe.stderrLines().get(0)); - Assert.assertEquals("try help", "Try '" + CMD + " help config initial-token' for more information", exe.stderrLines().get(1)); + Assert.assertEquals("try help", "Try '" + CMD + " config initial-token --help' for more information on the available options.", exe.stderrLines().get(1)); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTest.java index 478415a4d9..dc35e1e226 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTest.java @@ -36,7 +36,7 @@ public class KcRegTest extends AbstractRegCliTest { */ KcRegExec exe = execute(""); - assertExitCodeAndStdErrSize(exe, 1, 0); + assertExitCodeAndStdErrSize(exe, 2, 0); List lines = exe.stdoutLines(); Assert.assertTrue("stdout output not empty", lines.size() > 0); @@ -49,51 +49,51 @@ public class KcRegTest extends AbstractRegCliTest { * Test commands without arguments */ exe = execute("config"); - assertExitCodeAndStreamSizes(exe, 1, 0, 1); + assertExitCodeAndStreamSizes(exe, 2, 8, 0); Assert.assertEquals("error message", - "Sub-command required by '" + CMD + " config' - one of: 'credentials', 'truststore', 'initial-token', 'registration-token'", - exe.stderrLines().get(0)); + "Usage: kcreg.sh config SUB_COMMAND [ARGUMENTS]", + exe.stdoutLines().get(0)); exe = execute("config credentials"); - assertExitCodeAndStdErrSize(exe, 1, 0); + assertExitCodeAndStdErrSize(exe, 2, 0); Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10); Assert.assertEquals("help message", "Usage: " + CMD + " config credentials --server SERVER_URL --realm REALM [ARGUMENTS]", exe.stdoutLines().get(0)); exe = execute("config initial-token"); - assertExitCodeAndStdErrSize(exe, 1, 0); + assertExitCodeAndStdErrSize(exe, 2, 0); Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10); Assert.assertEquals("help message", "Usage: " + CMD + " config initial-token --server SERVER --realm REALM [--delete | TOKEN] [ARGUMENTS]", exe.stdoutLines().get(0)); exe = execute("config registration-token"); - assertExitCodeAndStdErrSize(exe, 1, 0); + assertExitCodeAndStdErrSize(exe, 2, 0); Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10); Assert.assertEquals("help message", "Usage: " + CMD + " config registration-token --server SERVER --realm REALM --client CLIENT [--delete | TOKEN] [ARGUMENTS]", exe.stdoutLines().get(0)); exe = execute("config truststore"); - assertExitCodeAndStdErrSize(exe, 1, 0); + assertExitCodeAndStdErrSize(exe, 2, 0); Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10); Assert.assertEquals("help message", "Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWORD] [ARGUMENTS]", exe.stdoutLines().get(0)); exe = execute("create"); - assertExitCodeAndStdErrSize(exe, 1, 0); + assertExitCodeAndStdErrSize(exe, 2, 0); Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10); Assert.assertEquals("help message", "Usage: " + CMD + " create [ARGUMENTS]", exe.stdoutLines().get(0)); //Assert.assertEquals("error message", "No file nor attribute values specified", exe.stderrLines().get(0)); exe = execute("get"); - assertExitCodeAndStdErrSize(exe, 1, 0); + assertExitCodeAndStdErrSize(exe, 2, 0); Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10); Assert.assertEquals("help message", "Usage: " + CMD + " get CLIENT [ARGUMENTS]", exe.stdoutLines().get(0)); //Assert.assertEquals("error message", "CLIENT not specified", exe.stderrLines().get(0)); exe = execute("update"); - assertExitCodeAndStdErrSize(exe, 1, 0); + assertExitCodeAndStdErrSize(exe, 2, 0); Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10); Assert.assertEquals("help message", "Usage: " + CMD + " update CLIENT [ARGUMENTS]", exe.stdoutLines().get(0)); //Assert.assertEquals("error message", "No file nor attribute values specified", exe.stderrLines().get(0)); exe = execute("delete"); - assertExitCodeAndStdErrSize(exe, 1, 0); + assertExitCodeAndStdErrSize(exe, 2, 0); Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10); Assert.assertEquals("help message", "Usage: " + CMD + " delete CLIENT [ARGUMENTS]", exe.stdoutLines().get(0)); //Assert.assertEquals("error message", "CLIENT not specified", exe.stderrLines().get(0)); @@ -104,7 +104,7 @@ public class KcRegTest extends AbstractRegCliTest { Assert.assertEquals("first line", "Attributes for default format:", exe.stdoutLines().get(0)); exe = execute("update-token"); - assertExitCodeAndStdErrSize(exe, 1, 0); + assertExitCodeAndStdErrSize(exe, 2, 0); Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10); Assert.assertEquals("help message", "Usage: " + CMD + " update-token CLIENT [ARGUMENTS]", exe.stdoutLines().get(0)); //Assert.assertEquals("error message", "CLIENT not specified", exe.stderrLines().get(0)); @@ -188,8 +188,8 @@ public class KcRegTest extends AbstractRegCliTest { */ KcRegExec exe = execute("nonexistent"); - assertExitCodeAndStreamSizes(exe, 1, 0, 1); - Assert.assertEquals("stderr first line", "Unknown command: nonexistent", exe.stderrLines().get(0)); + assertExitCodeAndStreamSizes(exe, 2, 0, 3); + Assert.assertEquals("stderr first line", "Unmatched argument at index 0: 'nonexistent'", exe.stderrLines().get(0)); } @Test @@ -199,8 +199,8 @@ public class KcRegTest extends AbstractRegCliTest { */ KcRegExec exe = execute("--nonexistent"); - assertExitCodeAndStreamSizes(exe, 1, 0, 1); - Assert.assertEquals("stderr first line", "Unknown command: --nonexistent", exe.stderrLines().get(0)); + assertExitCodeAndStreamSizes(exe, 2, 0, 2); + Assert.assertEquals("stderr first line", "Unknown option: '--nonexistent'", exe.stderrLines().get(0)); } @Test @@ -211,9 +211,9 @@ public class KcRegTest extends AbstractRegCliTest { KcRegExec exe = execute("get my_client --nonexistent"); - assertExitCodeAndStreamSizes(exe, 1, 0, 2); - Assert.assertEquals("stderr first line", "Invalid option: --nonexistent", exe.stderrLines().get(0)); - Assert.assertEquals("try help", "Try '" + CMD + " help get' for more information", exe.stderrLines().get(1)); + assertExitCodeAndStreamSizes(exe, 2, 0, 3); + Assert.assertEquals("stderr first line", "Unknown option: '--nonexistent'", exe.stderrLines().get(0)); + Assert.assertEquals("try help", "Try '" + CMD + " get --help' for more information on the available options.", exe.stderrLines().get(2)); } @Test @@ -233,9 +233,9 @@ public class KcRegTest extends AbstractRegCliTest { */ KcRegExec exe = execute("config credentials --realm master --user admin --password admin"); - assertExitCodeAndStreamSizes(exe, 1, 0, 2); + assertExitCodeAndStreamSizes(exe, 2, 0, 2); Assert.assertEquals("stderr first line", "Required option not specified: --server", exe.stderrLines().get(0)); - Assert.assertEquals("try help", "Try '" + CMD + " help config credentials' for more information", exe.stderrLines().get(1)); + Assert.assertEquals("try help", "Try '" + CMD + " config credentials --help' for more information on the available options.", exe.stderrLines().get(1)); } @Test @@ -245,9 +245,9 @@ public class KcRegTest extends AbstractRegCliTest { */ KcRegExec exe = execute("config credentials --server " + serverUrl + " --user admin --password admin"); - assertExitCodeAndStreamSizes(exe, 1, 0, 2); + assertExitCodeAndStreamSizes(exe, 2, 0, 2); Assert.assertEquals("stderr first line", "Required option not specified: --realm", exe.stderrLines().get(0)); - Assert.assertEquals("try help", "Try '" + CMD + " help config credentials' for more information", exe.stderrLines().get(1)); + Assert.assertEquals("try help", "Try '" + CMD + " config credentials --help' for more information on the available options.", exe.stderrLines().get(1)); } @Test @@ -257,9 +257,9 @@ public class KcRegTest extends AbstractRegCliTest { */ KcRegExec exe = KcRegExec.execute("config credentials --no-config --server " + serverUrl + " --realm master --user admin --password admin"); - assertExitCodeAndStreamSizes(exe, 1, 0, 2); + assertExitCodeAndStreamSizes(exe, 2, 0, 2); Assert.assertEquals("stderr first line", "Unsupported option: --no-config", exe.stderrLines().get(0)); - Assert.assertEquals("try help", "Try '" + CMD + " help config credentials' for more information", exe.stderrLines().get(1)); + Assert.assertEquals("try help", "Try '" + CMD + " config credentials --help' for more information on the available options.", exe.stderrLines().get(1)); } @Test diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTruststoreTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTruststoreTest.java index f4443a71fc..d6cb6af448 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTruststoreTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegTruststoreTest.java @@ -6,6 +6,7 @@ import org.keycloak.client.registration.cli.config.ConfigData; import org.keycloak.client.registration.cli.config.FileConfigHandler; import org.keycloak.client.registration.cli.util.OsUtil; import org.keycloak.testsuite.cli.KcRegExec; +import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.TempFileResource; import java.io.File; @@ -29,9 +30,9 @@ public class KcRegTruststoreTest extends AbstractRegCliTest { KcRegExec exe = execute("config truststore --no-config '" + truststore.getAbsolutePath() + "'"); - assertExitCodeAndStreamSizes(exe, 1, 0, 2); + assertExitCodeAndStreamSizes(exe, 2, 0, 2); Assert.assertEquals("stderr first line", "Unsupported option: --no-config", exe.stderrLines().get(0)); - Assert.assertEquals("try help", "Try '" + OsUtil.CMD + " help config truststore' for more information", exe.stderrLines().get(1)); + Assert.assertEquals("try help", "Try '" + OsUtil.CMD + " config truststore --help' for more information on the available options.", exe.stderrLines().get(1)); // only run the rest of this test if ssl protected keycloak server is available if (!AUTH_SERVER_SSL_REQUIRED) { @@ -39,9 +40,9 @@ public class KcRegTruststoreTest extends AbstractRegCliTest { return; } - FileConfigHandler handler = initCustomConfigFile(); + initCustomConfigFile(); - try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) { + try (TempFileResource configFile = new TempFileResource(FileConfigHandler.getConfigFile())) { if (runIntermittentlyFailingTests()) { // configure truststore @@ -52,7 +53,7 @@ public class KcRegTruststoreTest extends AbstractRegCliTest { // perform authentication against server - asks for password, then for truststore password exe = KcRegExec.newBuilder() - .argsLine("config credentials --server " + oauth.AUTH_SERVER_ROOT + " --realm test --user user1" + + .argsLine("config credentials --server " + OAuthClient.AUTH_SERVER_ROOT + " --realm test --user user1" + " --config '" + configFile.getName() + "'") .executeAsync(); @@ -72,7 +73,7 @@ public class KcRegTruststoreTest extends AbstractRegCliTest { // perform authentication against server - asks for password, then for truststore password exe = KcRegExec.newBuilder() - .argsLine("config credentials --server " + oauth.AUTH_SERVER_ROOT + " --realm test --user user1" + + .argsLine("config credentials --server " + OAuthClient.AUTH_SERVER_ROOT + " --realm test --user user1" + " --config '" + configFile.getName() + "'") .executeAsync(); @@ -99,17 +100,17 @@ public class KcRegTruststoreTest extends AbstractRegCliTest { assertExitCodeAndStreamSizes(exe, 0, 0, 0); exe = execute("config truststore --delete '" + truststore.getAbsolutePath() + "'"); - assertExitCodeAndStreamSizes(exe, 1, 0, 2); + assertExitCodeAndStreamSizes(exe, 2, 0, 2); Assert.assertEquals("incompatible", "Option --delete is mutually exclusive with specifying a TRUSTSTORE", exe.stderrLines().get(0)); - Assert.assertEquals("try help", "Try '" + CMD + " help config truststore' for more information", exe.stderrLines().get(1)); + Assert.assertEquals("try help", "Try '" + CMD + " config truststore --help' for more information on the available options.", exe.stderrLines().get(1)); exe = execute("config truststore --delete --trustpass secret"); - assertExitCodeAndStreamSizes(exe, 1, 0, 2); + assertExitCodeAndStreamSizes(exe, 2, 0, 2); Assert.assertEquals("no truststore error", "Options --trustpass and --delete are mutually exclusive", exe.stderrLines().get(0)); - Assert.assertEquals("try help", "Try '" + CMD + " help config truststore' for more information", exe.stderrLines().get(1)); + Assert.assertEquals("try help", "Try '" + CMD + " config truststore --help' for more information on the available options.", exe.stderrLines().get(1)); FileConfigHandler cfghandler = new FileConfigHandler(); - cfghandler.setConfigFile(DEFAULT_CONFIG_FILE_PATH); + FileConfigHandler.setConfigFile(DEFAULT_CONFIG_FILE_PATH); ConfigData config = cfghandler.loadConfig(); Assert.assertNull("truststore null", config.getTruststore()); Assert.assertNull("trustpass null", config.getTrustpass()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTest.java index dd5bca3d54..7c6d4c50e9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/registration/KcRegUpdateTest.java @@ -26,7 +26,7 @@ public class KcRegUpdateTest extends AbstractRegCliTest { FileConfigHandler handler = initCustomConfigFile(); - try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) { + try (TempFileResource configFile = new TempFileResource(FileConfigHandler.getConfigFile())) { final String realm = "test"; @@ -91,9 +91,9 @@ public class KcRegUpdateTest extends AbstractRegCliTest { // check that using an invalid attribute key is not ignored exe = execute("update my_client --nonexisting --config '" + configFile.getName() + "'"); - assertExitCodeAndStreamSizes(exe, 1, 0, 2); - Assert.assertEquals("error message", "Unsupported option: --nonexisting", exe.stderrLines().get(0)); - Assert.assertEquals("try help", "Try '" + CMD + " help update' for more information", exe.stderrLines().get(1)); + assertExitCodeAndStreamSizes(exe, 2, 0, 3); + Assert.assertEquals("error message", "Unknown option: '--nonexisting'", exe.stderrLines().get(0)); + Assert.assertEquals("try help", "Try '" + CMD + " update --help' for more information on the available options.", exe.stderrLines().get(2)); // try use incompatible endpoint