From e9ad9d05643e9492f800deeefb98201a1f2b7d67 Mon Sep 17 00:00:00 2001 From: Steven Hawkins Date: Thu, 28 Mar 2024 09:34:06 -0400 Subject: [PATCH] fix: replace aesh with picocli (#27458) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: replace aesh with picocli closes: #27388 Signed-off-by: Steve Hawkins * Update integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractRequestCmd.java Co-authored-by: Martin Bartoš * splitting the error handling for password input Signed-off-by: Steve Hawkins * adding a change note about kcadm Signed-off-by: Steve Hawkins * Update docs/documentation/upgrading/topics/changes/changes-25_0_0.adoc Co-authored-by: Martin Bartoš --------- Signed-off-by: Steve Hawkins Co-authored-by: Martin Bartoš --- .../topics/changes/changes-25_0_0.adoc | 4 + integration/client-cli/admin-cli/pom.xml | 14 +- .../admin/cli/ExecutionExceptionHandler.java | 34 ++ .../client/admin/cli/{aesh => }/Globals.java | 7 +- .../keycloak/client/admin/cli/KcAdmMain.java | 82 ++-- .../admin/cli/ShortErrorMessageHandler.java | 53 +++ .../cli/aesh/AeshConsoleCallbackImpl.java | 118 ------ .../client/admin/cli/aesh/AeshEnhancer.java | 41 -- .../admin/cli/aesh/ValveInputStream.java | 89 ----- .../cli/commands/AbstractAuthOptionsCmd.java | 83 ++-- .../commands/AbstractGlobalOptionsCmd.java | 105 ++++-- .../cli/commands/AbstractRequestCmd.java | 160 +++----- .../admin/cli/commands/AddRolesCmd.java | 341 ++++++++--------- .../client/admin/cli/commands/ConfigCmd.java | 55 +-- .../cli/commands/ConfigCredentialsCmd.java | 57 +-- .../cli/commands/ConfigTruststoreCmd.java | 104 ++--- .../client/admin/cli/commands/CreateCmd.java | 75 ++-- .../client/admin/cli/commands/DeleteCmd.java | 20 +- .../client/admin/cli/commands/GetCmd.java | 78 ++-- .../admin/cli/commands/GetRolesCmd.java | 110 +++--- .../client/admin/cli/commands/HelpCmd.java | 135 +++---- .../client/admin/cli/commands/KcAdmCmd.java | 57 ++- .../admin/cli/commands/NewObjectCmd.java | 79 +--- .../admin/cli/commands/RemoveRolesCmd.java | 354 ++++++++---------- .../admin/cli/commands/SetPasswordCmd.java | 60 +-- .../client/admin/cli/commands/UpdateCmd.java | 85 ++--- .../client/admin/cli/util/ConfigUtil.java | 2 +- .../client/admin/cli/util/IoUtil.java | 53 +-- .../client/admin/cli/util/ParseUtil.java | 2 +- .../testsuite/cli/AbstractCliTest.java | 12 +- .../testsuite/cli/admin/KcAdmSessionTest.java | 8 +- .../testsuite/cli/admin/KcAdmTest.java | 68 ++-- .../cli/admin/KcAdmTruststoreTest.java | 24 +- .../testsuite/cli/admin/KcAdmUpdateTest.java | 20 +- 34 files changed, 975 insertions(+), 1614 deletions(-) create mode 100644 integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/ExecutionExceptionHandler.java rename integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/{aesh => }/Globals.java (84%) create mode 100644 integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/ShortErrorMessageHandler.java delete mode 100644 integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshConsoleCallbackImpl.java delete mode 100644 integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshEnhancer.java delete mode 100644 integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/ValveInputStream.java 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 2af1994294..9ef5f354a1 100644 --- a/docs/documentation/upgrading/topics/changes/changes-25_0_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-25_0_0.adoc @@ -61,6 +61,10 @@ The module `org.keycloak:keycloak-model-legacy` module was deprecated in a previ The old behavior to preload offline sessions at startup is now removed after it has been deprecated in the previous release. += kcadm 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. + = Removing custom user attribute indexes When searching for users by user attribute, Keycloak no longer searches for user attribute names forcing lower case comparisons. This means Keycloak's native index on the user attribute table will now be used when searching. If you have created your own index based on `lower(name)`to speed up searches, you can now remove it. diff --git a/integration/client-cli/admin-cli/pom.xml b/integration/client-cli/admin-cli/pom.xml index c0d8ef6358..55f9e3a10e 100755 --- a/integration/client-cli/admin-cli/pom.xml +++ b/integration/client-cli/admin-cli/pom.xml @@ -29,20 +29,10 @@ Keycloak Admin CLI - - 1.18 - - - org.jboss.aesh - aesh - - - - org.fusesource.jansi - jansi - ${jansi.version} + info.picocli + picocli org.keycloak diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/ExecutionExceptionHandler.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/ExecutionExceptionHandler.java new file mode 100644 index 0000000000..352c5f4f89 --- /dev/null +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/ExecutionExceptionHandler.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.client.admin.cli; + +import picocli.CommandLine; +import picocli.CommandLine.ParseResult; + +public final class ExecutionExceptionHandler implements CommandLine.IExecutionExceptionHandler { + + @Override + public int handleExecutionException(Exception cause, CommandLine cmd, ParseResult parseResult) { + int exitCode = ShortErrorMessageHandler.shortErrorMessage(cause, cmd); + if (Globals.dumpTrace) { + cause.printStackTrace(); + } + return exitCode; + } + +} diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/Globals.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/Globals.java similarity index 84% rename from integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/Globals.java rename to integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/Globals.java index e16a43c863..d35d4a0a96 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/Globals.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/Globals.java @@ -14,9 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.client.admin.cli.aesh; - -import java.util.List; +package org.keycloak.client.admin.cli; /** * @author Marko Strukelj @@ -25,7 +23,6 @@ public class Globals { public static boolean dumpTrace = false; - public static ValveInputStream stdin; + public static boolean help = false; - public static List args; } diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/KcAdmMain.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/KcAdmMain.java index 150ba5b797..cc913235e0 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/KcAdmMain.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/KcAdmMain.java @@ -16,22 +16,15 @@ */ package org.keycloak.client.admin.cli; -import org.jboss.aesh.console.AeshConsoleBuilder; -import org.jboss.aesh.console.AeshConsoleImpl; -import org.jboss.aesh.console.Prompt; -import org.jboss.aesh.console.command.registry.AeshCommandRegistryBuilder; -import org.jboss.aesh.console.command.registry.CommandRegistry; -import org.jboss.aesh.console.settings.Settings; -import org.jboss.aesh.console.settings.SettingsBuilder; -import org.keycloak.client.admin.cli.aesh.AeshEnhancer; -import org.keycloak.client.admin.cli.aesh.Globals; -import org.keycloak.client.admin.cli.aesh.ValveInputStream; import org.keycloak.client.admin.cli.commands.KcAdmCmd; import org.keycloak.client.admin.cli.util.ClassLoaderUtil; +import org.keycloak.client.admin.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 @@ -47,53 +40,22 @@ public class KcAdmMain { Thread.currentThread().setContextClassLoader(cl); CryptoIntegration.init(cl); - - Globals.stdin = new ValveInputStream(); - Settings settings = new SettingsBuilder() - .logging(false) - .readInputrc(false) - .disableCompletion(true) - .disableHistory(true) - .enableAlias(false) - .enableExport(false) - .inputStream(Globals.stdin) - .create(); - - CommandRegistry registry = new AeshCommandRegistryBuilder() - .command(KcAdmCmd.class) - .create(); - - AeshConsoleImpl console = (AeshConsoleImpl) new AeshConsoleBuilder() - .settings(settings) - .commandRegistry(registry) - .prompt(new Prompt("")) - // .commandInvocationProvider(new CommandInvocationServices() { - // - // }) - .create(); - - AeshEnhancer.enhance(console); - - // work around parser issues with quotes and brackets - ArrayList arguments = new ArrayList<>(); - arguments.add("kcadm"); - arguments.addAll(Arrays.asList(args)); - Globals.args = arguments; - - StringBuilder b = new StringBuilder(); - for (String s : args) { - // quote if necessary - b.append(' '); - s = s.replace("'", "\\'"); - b.append('\''); - b.append(s); - b.append('\''); - } - console.setEcho(false); - - console.execute("kcadm" + b.toString()); - - console.start(); - } + CommandLine cli = createCommandLine(); + int exitCode = cli.execute(args); + System.exit(exitCode); } + + public static CommandLine createCommandLine() { + CommandSpec spec = CommandSpec.forAnnotatedObject(new KcAdmCmd()).name(OsUtil.CMD); + + CommandLine cmd = new CommandLine(spec); + + cmd.setExecutionExceptionHandler(new ExecutionExceptionHandler()); + cmd.setParameterExceptionHandler(new ShortErrorMessageHandler()); + cmd.setErr(new PrintWriter(System.err, true)); + + return cmd; + } + +} diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/ShortErrorMessageHandler.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/ShortErrorMessageHandler.java new file mode 100644 index 0000000000..b0c6d9bc16 --- /dev/null +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/ShortErrorMessageHandler.java @@ -0,0 +1,53 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.client.admin.cli; + +import java.io.PrintWriter; + +import picocli.CommandLine; +import picocli.CommandLine.IParameterExceptionHandler; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.ParameterException; +import picocli.CommandLine.UnmatchedArgumentException; + +public class ShortErrorMessageHandler implements IParameterExceptionHandler { + + @Override + public int handleParseException(ParameterException ex, String[] args) { + CommandLine cmd = ex.getCommandLine(); + return shortErrorMessage(ex, cmd); + } + + static int shortErrorMessage(Exception ex, CommandLine cmd) { + PrintWriter writer = cmd.getErr(); + String errorMessage = ex.getMessage(); + + writer.println(cmd.getColorScheme().errorText(errorMessage)); + if (ex instanceof ParameterException) { + UnmatchedArgumentException.printSuggestions((ParameterException)ex, writer); + } + + if (ex instanceof ParameterException || ex instanceof IllegalArgumentException) { + CommandSpec spec = cmd.getCommandSpec(); + writer.printf("Try '%s%s' for more information on the available options.%n", spec.qualifiedName(), "help".equals(spec.name())?"":" --help"); + return cmd.getCommandSpec().exitCodeOnInvalidInput(); + } + return cmd.getCommandSpec().exitCodeOnExecutionException(); + } + +} diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshConsoleCallbackImpl.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshConsoleCallbackImpl.java deleted file mode 100644 index d9ad42187b..0000000000 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshConsoleCallbackImpl.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.keycloak.client.admin.cli.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 && "Option: - must be followed by a valid operator".equals(e.getMessage())) { - System.err.println("Please double check your command options, one or more of them are not specified correctly. " - + "It is possible to have unintentional overlap with other options. e.g. using --clientid will get mistaken for --client, however --cclientid is needed."); - } else { - System.err.println(e.getMessage()); - } - if (Globals.dumpTrace) { - e.printStackTrace(); - } - - System.exit(1); - } - } - // empty line - else if (output != null) { - result = CommandResult.FAILURE; - } - else { - //stop(); - result = CommandResult.FAILURE; - } - - if (result == CommandResult.SUCCESS) { - return 0; - } else { - return 1; - } - } - - private CommandContainer getCommand(ConsoleOperation output, AeshLine aeshLine) throws CommandNotFoundException { - Method m; - try { - m = console.getClass().getDeclaredMethod("getCommand", AeshLine.class, String.class); - } catch (NoSuchMethodException e) { - throw new RuntimeException("Unexpected error: ", e); - } - - m.setAccessible(true); - - try { - return (CommandContainer) m.invoke(console, aeshLine, output.getBuffer()); - } catch (Exception e) { - throw new RuntimeException("Unexpected error: ", e); - } - } -} diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshEnhancer.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshEnhancer.java deleted file mode 100644 index 9e21b18dc0..0000000000 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/AeshEnhancer.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.keycloak.client.admin.cli.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/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/ValveInputStream.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/ValveInputStream.java deleted file mode 100644 index ec2cf4ff76..0000000000 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/aesh/ValveInputStream.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.keycloak.client.admin.cli.aesh; - -import org.jboss.aesh.console.AeshConsoleImpl; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InterruptedIOException; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; - -/** - * This stream blocks and waits, until there is a stream in the queue. - * It reads the stream to the end, then stops Aesh console. - * - * @author Marko Strukelj - */ -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/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractAuthOptionsCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractAuthOptionsCmd.java index fe5a12dac3..c17bb19e91 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractAuthOptionsCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractAuthOptionsCmd.java @@ -16,8 +16,6 @@ */ package org.keycloak.client.admin.cli.commands; -import org.jboss.aesh.cl.Option; -import org.jboss.aesh.console.command.invocation.CommandInvocation; import org.keycloak.OAuth2Constants; import org.keycloak.client.admin.cli.config.ConfigData; import org.keycloak.client.admin.cli.config.ConfigHandler; @@ -30,6 +28,8 @@ import org.keycloak.client.admin.cli.util.IoUtil; import java.io.File; +import picocli.CommandLine.Option; + import static org.keycloak.client.admin.cli.config.FileConfigHandler.setConfigFile; import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CLIENT; import static org.keycloak.client.admin.cli.util.ConfigUtil.checkAuthInfo; @@ -42,65 +42,61 @@ import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig; */ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd { - @Option(shortName = 'a', name = "admin-root", description = "URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/admin") + @Option(names = {"-a", "--admin-root"}, description = "URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/admin") String adminRestRoot; - @Option(name = "config", description = "Path to the config file (~/.keycloak/kcadm.config by default)") + @Option(names = "--config", description = "Path to the config file (~/.keycloak/kcadm.config by default)") String config; - @Option(name = "no-config", description = "Don't use config file - no authentication info is loaded or saved", hasValue = false) + @Option(names = "--no-config", description = "Don't use config file - no authentication info is loaded or saved") boolean noconfig; - @Option(name = "server", description = "Server endpoint url (e.g. 'http://localhost:8080')") + @Option(names = "--server", description = "Server endpoint url (e.g. 'http://localhost:8080')") String server; - @Option(shortName = 'r', name = "target-realm", description = "Realm to target - when it's different than the realm we authenticate against") + @Option(names = {"-r", "--target-realm"}, description = "Realm to target - when it's different than the realm we authenticate against") String targetRealm; - @Option(name = "realm", description = "Realm name to authenticate against") + @Option(names = "--realm", description = "Realm name to authenticate against") String realm; - @Option(name = "client", description = "Realm name to authenticate against") + @Option(names = "--client", description = "Realm name to authenticate against") String clientId; - @Option(name = "user", description = "Username to login with") + @Option(names = "--user", description = "Username to login with") String user; - @Option(name = "password", description = "Password to login with (prompted for if not specified and --user is used)") + @Option(names = "--password", description = "Password to login with (prompted for if not specified and --user is used)") String password; - @Option(name = "secret", description = "Secret to authenticate the client (prompted for if no --user or --keystore is specified)") + @Option(names = "--secret", description = "Secret to authenticate the client (prompted for if no --user or --keystore is specified)") String secret; - @Option(name = "keystore", description = "Path to a keystore containing private key") + @Option(names = "--keystore", description = "Path to a keystore containing private key") String keystore; - @Option(name = "storepass", description = "Keystore password (prompted for if not specified and --keystore is used)") + @Option(names = "--storepass", description = "Keystore password (prompted for if not specified and --keystore is used)") String storePass; - @Option(name = "keypass", description = "Key password (prompted for if not specified and --keystore is used without --storepass, \n otherwise defaults to keystore password)") + @Option(names = "--keypass", description = "Key password (prompted for if not specified and --keystore is used without --storepass, \n otherwise defaults to keystore password)") String keyPass; - @Option(name = "alias", description = "Alias of the key inside a keystore (defaults to the value of ClientId)") + @Option(names = "--alias", description = "Alias of the key inside a keystore (defaults to the value of ClientId)") String alias; - @Option(name = "truststore", description = "Path to a truststore") + @Option(names = "--truststore", description = "Path to a truststore") String trustStore; - @Option(name = "trustpass", description = "Truststore password (prompted for if not specified and --truststore is used)") + @Option(names = "--trustpass", description = "Truststore password (prompted for if not specified and --truststore is used)") String trustPass; - @Option(name = "insecure", description = "Turns off TLS validation", hasValue = false) + @Option(names = "--insecure", description = "Turns off TLS validation") boolean insecure; - @Option(name = "token", description = "Token to use for invocations. With this option set, every other authentication option is ignored") + @Option(names = "--token", description = "Token to use for invocations. With this option set, every other authentication option is ignored") String externalToken; - protected void initFromParent(AbstractAuthOptionsCmd parent) { - - super.initFromParent(parent); - noconfig = parent.noconfig; config = parent.config; server = parent.server; @@ -124,11 +120,12 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd { } } - protected boolean noOptions() { + @Override + protected boolean nothingToDo() { return externalToken == null && server == null && realm == null && clientId == null && secret == null && user == null && password == null && keystore == null && storePass == null && keyPass == null && alias == null && - trustStore == null && trustPass == null && config == null && (args == null || args.size() == 0); + trustStore == null && trustPass == null && config == null; } @@ -136,12 +133,10 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd { return targetRealm != null ? targetRealm : config.getRealm(); } - 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) { @@ -156,7 +151,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; @@ -173,7 +168,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 { @@ -188,7 +183,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 @@ -204,7 +199,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(); @@ -269,22 +264,4 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd { rdata.setGrantTypeForAuthentication(grantTypeForAuthentication); } - 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/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractGlobalOptionsCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractGlobalOptionsCmd.java index c49c83c8d8..7190595021 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractGlobalOptionsCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractGlobalOptionsCmd.java @@ -16,56 +16,43 @@ */ package org.keycloak.client.admin.cli.commands; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.jboss.aesh.cl.Arguments; -import org.jboss.aesh.cl.Option; -import org.jboss.aesh.console.command.Command; -import org.keycloak.client.admin.cli.aesh.Globals; +import org.keycloak.client.admin.cli.Globals; import org.keycloak.client.admin.cli.util.FilterUtil; import org.keycloak.client.admin.cli.util.ReturnFields; import java.io.IOException; -import java.util.Iterator; -import java.util.List; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import picocli.CommandLine; +import picocli.CommandLine.Option; import static org.keycloak.client.admin.cli.util.HttpUtil.normalize; import static org.keycloak.client.admin.cli.util.IoUtil.printOut; -/** - * @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) - boolean dumpTrace; - - @Option(name = "help", description = "Print command specific help", hasValue = false) - boolean help; - - - // we don't want Aesh to handle illegal options - @Arguments - List args; - - - protected void initFromParent(AbstractGlobalOptionsCmd parent) { - dumpTrace = parent.dumpTrace; - help = parent.help; - args = parent.args; + @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() { @@ -80,13 +67,6 @@ public abstract class AbstractGlobalOptionsCmd implements Command { return normalize(server) + "admin"; } - - protected void requireValue(Iterator it, String option) { - if (!it.hasNext()) { - throw new IllegalArgumentException("Option " + option + " requires a value"); - } - } - protected String extractTypeNameFromUri(String resourceUrl) { String type = extractLastComponentOfUri(resourceUrl); if (type.endsWith("s")) { @@ -110,4 +90,47 @@ public abstract class AbstractGlobalOptionsCmd implements Command { throw new RuntimeException("Failed to apply fields filter", e); } } + + @Override + public void run() { + printHelpIfNeeded(); + + checkUnsupportedOptions(getUnsupportedOptions()); + + processOptions(); + + process(); + } + + protected String[] getUnsupportedOptions() { + return new String[0]; + } + + protected void processOptions() { + + } + + protected void process() { + + } + + protected void checkUnsupportedOptions(String ... options) { + if (options.length % 2 != 0) { + throw new IllegalArgumentException("Even number of argument required"); + } + + for (int i = 0; i < options.length; i++) { + String name = options[i]; + String value = options[++i]; + + if (value != null) { + throw new IllegalArgumentException("Unsupported option: " + name); + } + } + } + + protected static String booleanOptionForCheck(boolean value) { + return value ? "true" : null; + } + } diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractRequestCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractRequestCmd.java index f175f404b9..882a37b6ba 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractRequestCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractRequestCmd.java @@ -16,13 +16,7 @@ */ package org.keycloak.client.admin.cli.commands; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; - import org.apache.http.entity.ContentType; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; import org.keycloak.client.admin.cli.common.AttributeOperation; import org.keycloak.client.admin.cli.common.CmdStdinContext; import org.keycloak.client.admin.cli.config.ConfigData; @@ -44,13 +38,20 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.HashMap; -import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.DELETE; import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.SET; import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken; @@ -103,100 +104,57 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd { String httpVerb; - Headers headers = new Headers(); + @Option(names = {"-h", "--header"}, description = "Set request header NAME to VALUE") + List rawHeaders = new LinkedList<>(); - List attrs = new LinkedList<>(); - - Map filter = new HashMap<>(); - - String url = null; - - - @Override - public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - try { - initOptions(); - - if (printHelp()) { - return help ? CommandResult.SUCCESS : CommandResult.FAILURE; - } - - processGlobalOptions(); - - processOptions(commandInvocation); - - return process(commandInvocation); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e); - } finally { - commandInvocation.stop(); - } + // 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; } - abstract void initOptions(); + @ArgGroup(exclusive = true, multiplicity = "0..*") + List rawAttributeOperations = new ArrayList<>(); - abstract String suggestHelp(); + @Option(names = {"-q", "--query"}, description = "Add to request URI a NAME query parameter with value VALUE") + List rawFilters = new LinkedList<>(); + @Parameters(arity = "0..1") + String uri; - void processOptions(CommandInvocation commandInvocation) { + List attrs = new LinkedList<>(); + Headers headers = new Headers(); + Map filter = new HashMap<>(); - if (args == null || args.isEmpty()) { - throw new IllegalArgumentException("URI not specified"); - } + @Override + protected void processOptions() { + super.processOptions(); - 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; - } - case "-d": - case "--delete": { - attrs.add(new AttributeOperation(DELETE, it.next())); - break; - } - case "-h": - case "--header": { - requireValue(it, option); - String[] keyVal = parseKeyVal(it.next()); - headers.add(keyVal[0], keyVal[1]); - break; - } - case "-q": - case "--query": { - if (!it.hasNext()) { - throw new IllegalArgumentException("Option " + option + " requires a value"); - } - String arg = it.next(); - String[] keyVal; - if (arg.indexOf("=") == -1) { - keyVal = new String[] {"", arg}; - } else { - keyVal = parseKeyVal(arg); - } - filter.put(keyVal[0], keyVal[1]); - break; - } - default: { - if (url == null) { - url = option; - } else { - throw new IllegalArgumentException("Invalid option: " + option); - } - } + 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])); } } + for (String header : rawHeaders) { + String[] keyVal = parseKeyVal(header); + headers.add(keyVal[0], keyVal[1]); + } - if (url == null) { + for (String arg : rawFilters) { + String[] keyVal; + if (arg.indexOf("=") == -1) { + keyVal = new String[] {"", arg}; + } else { + keyVal = parseKeyVal(arg); + } + filter.put(keyVal[0], keyVal[1]); + } + + if (uri == null) { throw new IllegalArgumentException("Resource URI not specified"); } @@ -207,7 +165,7 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd { try { outputFormat = OutputFormat.valueOf(format.toUpperCase()); } catch (Exception e) { - throw new RuntimeException("Unsupported output format: " + format); + throw new IllegalArgumentException("Unsupported output format: " + format); } if (mergeMode && noMerge) { @@ -223,10 +181,14 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd { } } + @Override + protected boolean nothingToDo() { + return super.nothingToDo() && file == null && body == null && uri == null && fields == null + && rawAttributeOperations.isEmpty() && rawFilters.isEmpty() && rawHeaders.isEmpty(); + } - - public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - + @Override + protected void process() { // see if Content-Type header is explicitly set to non-json value Header ctype = headers.get("content-type"); @@ -255,11 +217,11 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd { ConfigData config = loadConfig(); config = copyWithServerInfo(config); - setupTruststore(config, commandInvocation); + setupTruststore(config); String auth = null; - config = ensureAuthInfo(config, commandInvocation); + config = ensureAuthInfo(config); config = copyWithServerInfo(config); if (credentialsAvailable(config)) { auth = ensureToken(config); @@ -277,7 +239,7 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd { final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server); - String resourceUrl = composeResourceUrl(adminRoot, realm, url); + String resourceUrl = composeResourceUrl(adminRoot, realm, uri); String typeName = extractTypeNameFromUri(resourceUrl); @@ -385,7 +347,7 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd { } if (outputResult) { - if (isCreateOrUpdate() && (response.getStatusCode() == 204 || id != null) && isGetByID(url)) { + if (isCreateOrUpdate() && (response.getStatusCode() == 204 || id != null) && isGetByID(uri)) { // get object for id headers = new Headers(); if (auth != null) { @@ -423,7 +385,7 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd { } else { if (outputFormat != OutputFormat.JSON || returnFields != null) { printErr("Cannot create CSV nor filter returned fields because the response is " + (compressed ? "compressed":"not json")); - return CommandResult.SUCCESS; + return; } // in theory the user could explicitly request json, but this could be a non-json response // since there's no option for raw and we don't differentiate the default, there's no error about this @@ -435,8 +397,6 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd { if (lastByte != -1 && lastByte != 13 && lastByte != 10) { printErr(""); } - - return CommandResult.SUCCESS; } private boolean isUpdate() { diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AddRolesCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AddRolesCmd.java index dcea7735ad..a4ef642df3 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AddRolesCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AddRolesCmd.java @@ -17,11 +17,10 @@ package org.keycloak.client.admin.cli.commands; import com.fasterxml.jackson.databind.node.ObjectNode; -import org.jboss.aesh.cl.CommandDefinition; -import org.jboss.aesh.cl.Option; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; + +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + import org.keycloak.client.admin.cli.config.ConfigData; import org.keycloak.client.admin.cli.operations.ClientOperations; import org.keycloak.client.admin.cli.operations.GroupOperations; @@ -33,8 +32,6 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -43,219 +40,184 @@ import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_ import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable; import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig; import static org.keycloak.client.admin.cli.util.OsUtil.CMD; -import static org.keycloak.client.admin.cli.util.OsUtil.EOL; import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT; /** * @author Marko Strukelj */ -@CommandDefinition(name = "add-roles", description = "[ARGUMENTS]") +@Command(name = "add-roles", description = "[ARGUMENTS]") public class AddRolesCmd extends AbstractAuthOptionsCmd { - @Option(name = "uusername", description = "Target user's 'username'") + @Option(names = "--uusername", description = "Target user's 'username'") String uusername; - @Option(name = "uid", description = "Target user's 'id'") + @Option(names = "--uid", description = "Target user's 'id'") String uid; - @Option(name = "gname", description = "Target group's 'name'") + @Option(names = "--gname", description = "Target group's 'name'") String gname; - @Option(name = "gpath", description = "Target group's 'path'") + @Option(names = "--gpath", description = "Target group's 'path'") String gpath; - @Option(name = "gid", description = "Target group's 'id'") + @Option(names = "--gid", description = "Target group's 'id'") String gid; - @Option(name = "rname", description = "Composite role's 'name'") + @Option(names = "--rname", description = "Composite role's 'name'") String rname; - @Option(name = "rid", description = "Composite role's 'id'") + @Option(names = "--rid", description = "Composite role's 'id'") String rid; - @Option(name = "cclientid", description = "Target client's 'clientId'") + @Option(names = "--cclientid", description = "Target client's 'clientId'") String cclientid; - @Option(name = "cid", description = "Target client's 'id'") + @Option(names = "--cid", description = "Target client's 'id'") String cid; + @Option(names = "--rolename", description = "Role's 'name' attribute") + List roleNames = new ArrayList<>(); + + @Option(names = "--roleid", description = "Role's 'id' attribute") + List roleIds = new ArrayList<>(); + @Override - public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { + protected void process() { + if (uid != null && uusername != null) { + throw new IllegalArgumentException("Incompatible options: --uid and --uusername are mutually exclusive"); + } - List roleNames = new LinkedList<>(); - List roleIds = new LinkedList<>(); + if ((gid != null && gname != null) || (gid != null && gpath != null) || (gname != null && gpath != null)) { + throw new IllegalArgumentException("Incompatible options: --gid, --gname and --gpath are mutually exclusive"); + } - try { - if (printHelp()) { - return help ? CommandResult.SUCCESS : CommandResult.FAILURE; + if (roleNames.isEmpty() && roleIds.isEmpty()) { + throw new IllegalArgumentException("No role to add specified. Use --rolename or --roleid to specify roles to add"); + } + + if (cid != null && cclientid != null) { + throw new IllegalArgumentException("Incompatible options: --cid and --cclientid are mutually exclusive"); + } + + if (rid != null && rname != null) { + throw new IllegalArgumentException("Incompatible options: --rid and --rname are mutually exclusive"); + } + + if (isUserSpecified() && isGroupSpecified()) { + throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --gname / --gid / --gpath"); + } + + if (isUserSpecified() && isCompositeRoleSpecified()) { + throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --rname / --rid"); + } + + if (isGroupSpecified() && isCompositeRoleSpecified()) { + throw new IllegalArgumentException("Incompatible options: --rname / --rid can't be used at the same time as --gname / --gid / --gpath"); + } + + if (!isUserSpecified() && !isGroupSpecified() && !isCompositeRoleSpecified()) { + throw new IllegalArgumentException("No user nor group nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role"); + } + + + ConfigData config = loadConfig(); + config = copyWithServerInfo(config); + + setupTruststore(config); + + String 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 = getTargetRealm(config); + final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server); + + + if (isUserSpecified()) { + if (uid == null) { + uid = UserOperations.getIdFromUsername(adminRoot, realm, auth, uusername); } - - processGlobalOptions(); - - Iterator it = args.iterator(); - - while (it.hasNext()) { - String option = it.next(); - switch (option) { - case "--rolename": { - optionRequiresValueCheck(it, option); - roleNames.add(it.next()); - break; - } - case "--roleid": { - optionRequiresValueCheck(it, option); - roleIds.add(it.next()); - break; - } - default: { - throw new IllegalArgumentException("Invalid option: " + option); - } - } - } - - if (uid != null && uusername != null) { - throw new IllegalArgumentException("Incompatible options: --uid and --uusername are mutually exclusive"); - } - - if ((gid != null && gname != null) || (gid != null && gpath != null) || (gname != null && gpath != null)) { - throw new IllegalArgumentException("Incompatible options: --gid, --gname and --gpath are mutually exclusive"); - } - - if (roleNames.isEmpty() && roleIds.isEmpty()) { - throw new IllegalArgumentException("No role to add specified. Use --rolename or --roleid to specify roles to add"); - } - - if (cid != null && cclientid != null) { - throw new IllegalArgumentException("Incompatible options: --cid and --cclientid are mutually exclusive"); - } - - if (rid != null && rname != null) { - throw new IllegalArgumentException("Incompatible options: --rid and --rname are mutually exclusive"); - } - - if (isUserSpecified() && isGroupSpecified()) { - throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --gname / --gid / --gpath"); - } - - if (isUserSpecified() && isCompositeRoleSpecified()) { - throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --rname / --rid"); - } - - if (isGroupSpecified() && isCompositeRoleSpecified()) { - throw new IllegalArgumentException("Incompatible options: --rname / --rid can't be used at the same time as --gname / --gid / --gpath"); - } - - if (!isUserSpecified() && !isGroupSpecified() && !isCompositeRoleSpecified()) { - throw new IllegalArgumentException("No user nor group nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role"); - } - - - ConfigData config = loadConfig(); - config = copyWithServerInfo(config); - - setupTruststore(config, commandInvocation); - - String auth = null; - - config = ensureAuthInfo(config, commandInvocation); - config = copyWithServerInfo(config); - if (credentialsAvailable(config)) { - auth = ensureToken(config); - } - - auth = auth != null ? "Bearer " + auth : null; - - final String server = config.getServerUrl(); - final String realm = getTargetRealm(config); - final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server); - - - if (isUserSpecified()) { - if (uid == null) { - uid = UserOperations.getIdFromUsername(adminRoot, realm, auth, uusername); - } - if (isClientSpecified()) { - // list client roles for a user - if (cid == null) { - cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid); - } - - List roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth); - Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles)); - - // now add all the roles - UserOperations.addClientRoles(adminRoot, realm, auth, uid, cid, new ArrayList<>(rolesToAdd)); - - } else { - - Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, - new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth))); - - // now add all the roles - UserOperations.addRealmRoles(adminRoot, realm, auth, uid, new ArrayList<>(rolesToAdd)); + if (isClientSpecified()) { + // list client roles for a user + if (cid == null) { + cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid); } - } else if (isGroupSpecified()) { - if (gname != null) { - gid = GroupOperations.getIdFromName(adminRoot, realm, auth, gname); - } else if (gpath != null) { - gid = GroupOperations.getIdFromPath(adminRoot, realm, auth, gpath); - } - if (isClientSpecified()) { - // list client roles for a group - if (cid == null) { - cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid); - } + List roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth); + Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles)); - List roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth); - Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles)); - - // now add all the roles - GroupOperations.addClientRoles(adminRoot, realm, auth, gid, cid, new ArrayList<>(rolesToAdd)); - - } else { - - Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, - new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth))); - - // now add all the roles - GroupOperations.addRealmRoles(adminRoot, realm, auth, gid, new ArrayList<>(rolesToAdd)); - } - - } else if (isCompositeRoleSpecified()) { - if (rid == null) { - rid = RoleOperations.getIdFromRoleName(adminRoot, realm, auth, rname); - } - if (isClientSpecified()) { - // list client roles for a composite role - if (cid == null) { - cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid); - } - - List roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth); - Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles)); - - // now add all the roles - RoleOperations.addClientRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd)); - - } else { - Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, - new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth))); - - // now add all the roles - RoleOperations.addRealmRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd)); - } + // now add all the roles + UserOperations.addClientRoles(adminRoot, realm, auth, uid, cid, new ArrayList<>(rolesToAdd)); } else { - throw new IllegalArgumentException("No user nor group, nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role"); + + Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, + new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth))); + + // now add all the roles + UserOperations.addRealmRoles(adminRoot, realm, auth, uid, new ArrayList<>(rolesToAdd)); } - return CommandResult.SUCCESS; + } else if (isGroupSpecified()) { + if (gname != null) { + gid = GroupOperations.getIdFromName(adminRoot, realm, auth, gname); + } else if (gpath != null) { + gid = GroupOperations.getIdFromPath(adminRoot, realm, auth, gpath); + } + if (isClientSpecified()) { + // list client roles for a group + if (cid == null) { + cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid); + } - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e); - } finally { - commandInvocation.stop(); + List roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth); + Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles)); + + // now add all the roles + GroupOperations.addClientRoles(adminRoot, realm, auth, gid, cid, new ArrayList<>(rolesToAdd)); + + } else { + + Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, + new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth))); + + // now add all the roles + GroupOperations.addRealmRoles(adminRoot, realm, auth, gid, new ArrayList<>(rolesToAdd)); + } + + } else if (isCompositeRoleSpecified()) { + if (rid == null) { + rid = RoleOperations.getIdFromRoleName(adminRoot, realm, auth, rname); + } + if (isClientSpecified()) { + // list client roles for a composite role + if (cid == null) { + cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid); + } + + List roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth); + Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles)); + + // now add all the roles + RoleOperations.addClientRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd)); + + } else { + Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, + new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth))); + + // now add all the roles + RoleOperations.addRealmRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd)); + } + + } else { + throw new IllegalArgumentException("No user nor group, nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role"); } } @@ -280,12 +242,6 @@ public class AddRolesCmd extends AbstractAuthOptionsCmd { return rolesToAdd; } - private void optionRequiresValueCheck(Iterator it, String option) { - if (!it.hasNext()) { - throw new IllegalArgumentException("Option " + option + " requires a value"); - } - } - private boolean isClientSpecified() { return cid != null || cclientid != null; } @@ -304,13 +260,10 @@ public class AddRolesCmd extends AbstractAuthOptionsCmd { @Override protected boolean nothingToDo() { - return noOptions() && uusername == null && uid == null && cclientid == null && (args == null || args.size() == 0); - } - - protected String suggestHelp() { - return EOL + "Try '" + CMD + " help add-roles' for more information"; + return super.nothingToDo() && uusername == null && uid == null && cclientid == null && roleIds.isEmpty() && roleNames.isEmpty(); } + @Override protected String help() { return usage(); } diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCmd.java index f2975616f0..9bc0c346e5 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCmd.java @@ -16,66 +16,35 @@ */ package org.keycloak.client.admin.cli.commands; -import org.jboss.aesh.cl.GroupCommandDefinition; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; - import java.io.PrintWriter; import java.io.StringWriter; +import picocli.CommandLine.Command; + import static org.keycloak.client.admin.cli.util.OsUtil.CMD; -import static org.keycloak.client.admin.cli.util.OsUtil.EOL; /** * @author Marko Strukelj */ -@GroupCommandDefinition(name = "config", description = "COMMAND [ARGUMENTS]", groupCommands = {ConfigCredentialsCmd.class} ) +@Command(name = "config", description = "COMMAND [ARGUMENTS]", subcommands = { + ConfigCredentialsCmd.class, + ConfigTruststoreCmd.class +} ) public class ConfigCmd extends AbstractAuthOptionsCmd { - public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - try { - if (args != null && args.size() > 0) { - String cmd = args.get(0); - switch (cmd) { - case "credentials": { - args.remove(0); - ConfigCredentialsCmd command = new ConfigCredentialsCmd(); - command.initFromParent(this); - return command.execute(commandInvocation); - } - case "truststore": { - args.remove(0); - ConfigTruststoreCmd command = new ConfigTruststoreCmd(); - command.initFromParent(this); - return command.execute(commandInvocation); - } - default: { - if (printHelp()) { - return help ? CommandResult.SUCCESS : CommandResult.FAILURE; - } - throw new IllegalArgumentException("Unknown sub-command: " + cmd + suggestHelp()); - } - } - } + @Override + protected void process() { - if (printHelp()) { - return help ? CommandResult.SUCCESS : CommandResult.FAILURE; - } - - throw new IllegalArgumentException("Sub-command required by '" + CMD + " config' - one of: 'credentials', 'truststore'"); - - } finally { - commandInvocation.stop(); - } } - protected String suggestHelp() { - return EOL + "Try '" + CMD + " help config' for more information"; + @Override + protected boolean nothingToDo() { + return true; } + @Override protected String help() { return usage(); } diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCredentialsCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCredentialsCmd.java index eff6252e65..f62b448c1c 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCredentialsCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigCredentialsCmd.java @@ -16,10 +16,6 @@ */ package org.keycloak.client.admin.cli.commands; -import org.jboss.aesh.cl.CommandDefinition; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; import org.keycloak.OAuth2Constants; import org.keycloak.client.admin.cli.config.ConfigData; import org.keycloak.client.admin.cli.config.RealmConfigData; @@ -31,6 +27,8 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.net.URL; +import picocli.CommandLine.Command; + import static org.keycloak.client.admin.cli.util.AuthUtil.getAuthTokens; import static org.keycloak.client.admin.cli.util.AuthUtil.getAuthTokensByJWT; import static org.keycloak.client.admin.cli.util.AuthUtil.getAuthTokensBySecret; @@ -41,7 +39,6 @@ import static org.keycloak.client.admin.cli.util.ConfigUtil.saveTokens; import static org.keycloak.client.admin.cli.util.IoUtil.printErr; import static org.keycloak.client.admin.cli.util.IoUtil.readSecret; import static org.keycloak.client.admin.cli.util.OsUtil.CMD; -import static org.keycloak.client.admin.cli.util.OsUtil.EOL; import static org.keycloak.client.admin.cli.util.OsUtil.OS_ARCH; import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT; @@ -49,12 +46,11 @@ import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT; /** * @author Marko Strukelj */ -@CommandDefinition(name = "credentials", description = "--server SERVER_URL --realm REALM [ARGUMENTS]") +@Command(name = "credentials", description = "--server SERVER_URL --realm REALM [ARGUMENTS]") public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd { private int sigLifetime = 600; - public void init(ConfigData configData) { if (server == null) { server = configData.getServerUrl(); @@ -76,33 +72,13 @@ public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd { } } - @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 - protected boolean nothingToDo() { - return noOptions(); - } - - public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - + public void process() { // check server if (server == null) { throw new IllegalArgumentException("Required option not specified: --server"); @@ -129,18 +105,18 @@ public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd { // 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: "); } } } @@ -155,8 +131,8 @@ public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd { } 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) { @@ -179,10 +155,10 @@ public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd { 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 ? @@ -195,14 +171,9 @@ public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd { // 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/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigTruststoreCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigTruststoreCmd.java index 36dde42def..2b86df05f6 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigTruststoreCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/ConfigTruststoreCmd.java @@ -16,93 +16,41 @@ */ package org.keycloak.client.admin.cli.commands; -import org.jboss.aesh.cl.CommandDefinition; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; - import java.io.File; import java.io.PrintWriter; import java.io.StringWriter; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; + +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.admin.cli.util.ConfigUtil.saveMergeConfig; import static org.keycloak.client.admin.cli.util.IoUtil.readSecret; import static org.keycloak.client.admin.cli.util.OsUtil.CMD; -import static org.keycloak.client.admin.cli.util.OsUtil.EOL; import static org.keycloak.client.admin.cli.util.OsUtil.OS_ARCH; import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT; /** * @author Marko Strukelj */ -@CommandDefinition(name = "truststore", description = "PATH [ARGUMENTS]") +@Command(name = "truststore", description = "PATH [ARGUMENTS]") public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd { - private ConfigCmd parent; + @Parameters(arity = "0..1") + private String store; + @Option(names = {"-d", "--delete"}, description = "Remove truststore configuration") private boolean delete; - - protected void initFromParent(ConfigCmd parent) { - this.parent = parent; - super.initFromParent(parent); - } - - @Override - public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - try { - if (printHelp()) { - return help ? CommandResult.SUCCESS : CommandResult.FAILURE; - } - - return process(commandInvocation); - - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e); - } finally { - commandInvocation.stop(); - } - } - @Override protected boolean nothingToDo() { - return noOptions(); + return super.nothingToDo() && store == null && !delete; } - public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - - List args = new ArrayList<>(); - - Iterator it = parent.args.iterator(); - - while (it.hasNext()) { - String arg = it.next(); - switch (arg) { - case "-d": - case "--delete": { - delete = true; - break; - } - default: { - args.add(arg); - } - } - } - - if (args.size() > 1) { - throw new IllegalArgumentException("Invalid option: " + args.get(1)); - } - - String truststore = null; - if (args.size() > 0) { - truststore = args.get(0); - } - - checkUnsupportedOptions("--server", server, + @Override + protected String[] getUnsupportedOptions() { + return new String[] {"--server", server, "--realm", realm, "--client", clientId, "--user", user, @@ -112,39 +60,36 @@ public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd { "--keystore", keystore, "--keypass", keyPass, "--alias", alias, - "--no-config", booleanOptionForCheck(noconfig)); + "--no-config", booleanOptionForCheck(noconfig)}; + } - // now update the config - processGlobalOptions(); - - String store; + @Override + protected void process() { String pass; if (!delete) { - if (truststore == null) { + if (store == null) { throw new IllegalArgumentException("No truststore specified"); } - if (!new File(truststore).isFile()) { - throw new RuntimeException("Truststore file not found: " + truststore); + if (!new File(store).isFile()) { + throw new RuntimeException("Truststore file not found: " + store); } if ("-".equals(trustPass)) { - trustPass = readSecret("Enter truststore password: ", commandInvocation); + trustPass = readSecret("Enter truststore password: "); } - store = truststore; pass = trustPass; } else { - if (truststore != null) { + if (store != null) { throw new IllegalArgumentException("Option --delete is mutually exclusive with specifying a TRUSTSTORE"); } if (trustPass != null) { throw new IllegalArgumentException("Options --trustpass and --delete are mutually exclusive"); } - store = null; pass = null; } @@ -152,14 +97,9 @@ public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd { 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/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/CreateCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/CreateCmd.java index 28cd7d782f..a74ea74463 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/CreateCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/CreateCmd.java @@ -16,70 +16,63 @@ */ package org.keycloak.client.admin.cli.commands; -import org.jboss.aesh.cl.CommandDefinition; -import org.jboss.aesh.cl.Option; - import java.io.PrintWriter; import java.io.StringWriter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.admin.cli.util.OsUtil.CMD; -import static org.keycloak.client.admin.cli.util.OsUtil.EOL; import static org.keycloak.client.admin.cli.util.OsUtil.OS_ARCH; import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT; /** * @author Marko Strukelj */ -@CommandDefinition(name = "create", description = "Command to create new resources") +@Command(name = "create", description = "Command to create new resources") public class CreateCmd extends AbstractRequestCmd { - @Option(shortName = 'f', name = "file", description = "Read object from file or standard input if FILENAME is set to '-'") - String file; + public CreateCmd() { + this.httpVerb = "post"; + } - @Option(shortName = 'b', name = "body", description = "JSON object to be sent as-is or used as a template") - String body; + @Option(names = {"-f", "--file"}, description = "Read object from file or standard input if FILENAME is set to '-'") + public void setFile(String file) { + this.file = file; + } - @Option(shortName = 'F', name = "fields", description = "A pattern specifying which attributes of JSON response body to actually display as result - causes mismatch with Content-Length header", hasValue = true) - String fields; + @Option(names = {"-b", "--body"}, description = "JSON object to be sent as-is or used as a template") + public void setBody(String body) { + this.body = body; + } - @Option(shortName = 'H', name = "print-headers", description = "Print response headers", hasValue = false) - boolean printHeaders; + @Option(names = {"-F", "--fields"}, description = "A pattern specifying which attributes of JSON response body to actually display as result - causes mismatch with Content-Length header") + public void setFields(String fields) { + this.fields = fields; + } - @Option(shortName = 'i', name = "id", description = "After creation only print id of created resource to standard output", hasValue = false) - boolean returnId = false; + @Option(names = {"-H", "--print-headers"}, description = "Print response headers") + public void setPrintHeaders(boolean printHeaders) { + this.printHeaders = printHeaders; + } - @Option(shortName = 'o', name = "output", description = "After creation output the new resource to standard output", hasValue = false) - boolean outputResult = false; + @Option(names = {"-i", "--id"}, description = "After creation only print id of created resource to standard output") + public void setReturnId(boolean returnId) { + this.returnId = returnId; + } - @Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false) - boolean compressed = false; + @Option(names = {"-o", "--output"}, description = "After creation output the new resource to standard output") + public void setOutputResult(boolean outputResult) { + this.outputResult = outputResult; + } - //@OptionGroup(shortName = 's', name = "set", description = "Set attribute to the specified value") - //Map attributes = new LinkedHashMap<>(); - - @Override - void initOptions() { - // set options on parent - super.file = file; - super.body = body; - super.fields = fields; - super.printHeaders = printHeaders; - super.returnId = returnId; - super.outputResult = outputResult; - super.compressed = compressed; - super.httpVerb = "post"; + @Option(names = {"-c", "--compressed"}, description = "Don't pretty print the output") + public void setCompressed(boolean compressed) { + this.compressed = compressed; } @Override - protected boolean nothingToDo() { - return noOptions() && file == null && body == null && (args == null || args.size() == 0); - } - - protected String suggestHelp() { - return EOL + "Try '" + CMD + " help create' for more information"; - } - protected String help() { return usage(); } diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/DeleteCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/DeleteCmd.java index 185d0381ea..a8ba2c27fc 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/DeleteCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/DeleteCmd.java @@ -16,37 +16,27 @@ */ package org.keycloak.client.admin.cli.commands; -import org.jboss.aesh.cl.CommandDefinition; - import java.io.PrintWriter; import java.io.StringWriter; +import picocli.CommandLine.Command; + import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.admin.cli.util.OsUtil.CMD; -import static org.keycloak.client.admin.cli.util.OsUtil.EOL; import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT; /** * @author Marko Strukelj */ -@CommandDefinition(name = "delete", description = "CLIENT [GLOBAL_OPTIONS]") +@Command(name = "delete", description = "CLIENT [GLOBAL_OPTIONS]") public class DeleteCmd extends CreateCmd { - void initOptions() { - super.initOptions(); - httpVerb = "delete"; + public DeleteCmd() { + this.httpVerb = "delete"; } @Override - protected boolean nothingToDo() { - return noOptions() && (args == null || args.size() == 0); - } - - protected String suggestHelp() { - return EOL + "Try '" + CMD + " help delete' for more information"; - } - protected String help() { return usage(); } diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetCmd.java index 8405031233..367fb2cdd8 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetCmd.java @@ -16,69 +16,63 @@ */ package org.keycloak.client.admin.cli.commands; -import org.jboss.aesh.cl.CommandDefinition; -import org.jboss.aesh.cl.Option; - import java.io.PrintWriter; import java.io.StringWriter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.admin.cli.util.OsUtil.CMD; -import static org.keycloak.client.admin.cli.util.OsUtil.EOL; import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT; /** * @author Marko Strukelj */ -@CommandDefinition(name = "get", description = "[ARGUMENTS]") -public class GetCmd extends AbstractRequestCmd { +@Command(name = "get", description = "[ARGUMENTS]") +public class GetCmd extends AbstractRequestCmd { - @Option(name = "noquotes", description = "", hasValue = false) - boolean unquoted; + public GetCmd() { + this.httpVerb = "get"; + this.outputResult = true; + } - @Option(shortName = 'F', name = "fields", description = "A pattern specifying which attributes of JSON response body to actually display as result - causes mismatch with Content-Length header") - String fields; + @Option(names = "--noquotes", description = "") + public void setUnquoted(boolean unquoted) { + this.unquoted = unquoted; + } - @Option(shortName = 'H', name = "print-headers", description = "Print response headers", hasValue = false) - boolean printHeaders; + @Option(names = {"-F", "--fields"}, description = "A pattern specifying which attributes of JSON response body to actually display as result - causes mismatch with Content-Length header") + public void setFields(String fields) { + this.fields = fields; + } - @Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false) - boolean compressed; + @Option(names = {"-H", "--print-headers"}, description = "Print response headers") + public void setPrintHeaders(boolean printHeaders) { + this.printHeaders = printHeaders; + } - @Option(shortName = 'o', name = "offset", description = "Number of results from beginning of resultset to skip") - Integer offset; + @Option(names = {"-c", "--compressed"}, description = "Don't pretty print the output") + public void setCompressed(boolean compressed) { + this.compressed = compressed; + } - @Option(shortName = 'l', name = "limit", description = "Maksimum number of results to return") - Integer limit; + @Option(names = {"-o", "--offset"}, description = "Number of results from beginning of resultset to skip") + public void setOffset(Integer offset) { + this.offset = offset; + } - @Option(name = "format", description = "Output format - one of: json, csv", defaultValue = "json") - String format; + @Option(names = {"-l", "--limit"}, description = "Maksimum number of results to return") + public void setLimit(Integer limit) { + this.limit = limit; + } - - @Override - void initOptions() { - // set options on parent - super.fields = fields; - super.printHeaders = printHeaders; - super.returnId = false; - super.outputResult = true; - super.compressed = compressed; - super.offset = offset; - super.limit = limit; - super.format = format; - super.unquoted = unquoted; - super.httpVerb = "get"; + @Option(names = "--format", description = "Output format - one of: json, csv", defaultValue = "json") + public void setFormat(String format) { + this.format = format; } @Override - protected boolean nothingToDo() { - return noOptions() && (args == null || args.size() == 0); - } - - protected String suggestHelp() { - return EOL + "Try '" + CMD + " help get' for more information"; - } - protected String help() { return usage(); } diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetRolesCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetRolesCmd.java index c13082ac8e..92088fa9b2 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetRolesCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetRolesCmd.java @@ -16,11 +16,6 @@ */ package org.keycloak.client.admin.cli.commands; -import org.jboss.aesh.cl.CommandDefinition; -import org.jboss.aesh.cl.Option; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; import org.keycloak.client.admin.cli.config.ConfigData; import org.keycloak.client.admin.cli.operations.ClientOperations; import org.keycloak.client.admin.cli.operations.GroupOperations; @@ -29,7 +24,9 @@ import org.keycloak.client.admin.cli.operations.UserOperations; import java.io.PrintWriter; import java.io.StringWriter; -import java.util.ArrayList; + +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken; import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; @@ -42,69 +39,58 @@ import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT; /** * @author Marko Strukelj */ -@CommandDefinition(name = "get-roles", description = "[ARGUMENTS]") +@Command(name = "get-roles", description = "[ARGUMENTS]") public class GetRolesCmd extends GetCmd { - @Option(name = "uusername", description = "Target user's 'username'") + @Option(names = "--uusername", description = "Target user's 'username'") String uusername; - @Option(name = "uid", description = "Target user's 'id'") + @Option(names = "--uid", description = "Target user's 'id'") String uid; - @Option(name = "cclientid", description = "Target client's 'clientId'") + @Option(names = "--cclientid", description = "Target client's 'clientId'") String cclientid; - @Option(name = "cid", description = "Target client's 'id'") + @Option(names = "--cid", description = "Target client's 'id'") String cid; - @Option(name = "rname", description = "Composite role's 'name'") + @Option(names = "--rname", description = "Composite role's 'name'") String rname; - @Option(name = "rid", description = "Composite role's 'id'") + @Option(names = "--rid", description = "Composite role's 'id'") String rid; - @Option(name = "gname", description = "Target group's 'name'") + @Option(names = "--gname", description = "Target group's 'name'") String gname; - @Option(name = "gpath", description = "Target group's 'path'") + @Option(names = "--gpath", description = "Target group's 'path'") String gpath; - @Option(name = "gid", description = "Target group's 'id'") + @Option(names = "--gid", description = "Target group's 'id'") String gid; - @Option(name = "rolename", description = "Target role's 'name'") + @Option(names = "--rolename", description = "Target role's 'name'") String rolename; - @Option(name = "roleid", description = "Target role's 'id'") + @Option(names = "--roleid", description = "Target role's 'id'") String roleid; - @Option(name = "available", description = "List only available roles", hasValue = false) + @Option(names = "--available", description = "List only available roles") boolean available; - @Option(name = "effective", description = "List assigned roles including transitively included roles", hasValue = false) + @Option(names = "--effective", description = "List assigned roles including transitively included roles") boolean effective; - @Option(name = "all", description = "List roles for all clients in addition to realm roles", hasValue = false) + @Option(names = "--all", description = "List roles for all clients in addition to realm roles") boolean all; - - void initOptions() { - - super.initOptions(); - + @Override + protected void processOptions() { // hack args so that GetCmd option check doesn't fail // set a placeholder - if (args == null) { - args = new ArrayList(); + if (uri == null) { + uri = "uri"; } - if (args.size() == 0) { - args.add("uri"); - } else { - args.add(0, "uri"); - } - } - - void processOptions(CommandInvocation commandInvocation) { if (uid != null && uusername != null) { throw new IllegalArgumentException("Incompatible options: --uid and --uusername are mutually exclusive"); @@ -146,19 +132,19 @@ public class GetRolesCmd extends GetCmd { throw new IllegalArgumentException("Incompatible options: --all can't be used at the same time as --available"); } - super.processOptions(commandInvocation); + super.processOptions(); } - public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - + @Override + protected void process() { ConfigData config = loadConfig(); config = copyWithServerInfo(config); - setupTruststore(config, commandInvocation); + setupTruststore(config); String auth = null; - config = ensureAuthInfo(config, commandInvocation); + config = ensureAuthInfo(config); config = copyWithServerInfo(config); if (credentialsAvailable(config)) { auth = ensureToken(config); @@ -180,20 +166,20 @@ public class GetRolesCmd extends GetCmd { cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid); } if (available) { - super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/clients/" + cid + "/available"); + super.uri = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/clients/" + cid + "/available"); } else if (effective) { - super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/clients/" + cid + "/composite"); + super.uri = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/clients/" + cid + "/composite"); } else { - super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/clients/" + cid); + super.uri = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/clients/" + cid); } } else { // list realm roles for a user if (available) { - super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/realm/available"); + super.uri = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/realm/available"); } else if (effective) { - super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/realm/composite"); + super.uri = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/realm/composite"); } else { - super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + (all ? "/role-mappings" : "/role-mappings/realm")); + super.uri = composeResourceUrl(adminRoot, realm, "users/" + uid + (all ? "/role-mappings" : "/role-mappings/realm")); } } } else if (isGroupSpecified()) { @@ -208,20 +194,20 @@ public class GetRolesCmd extends GetCmd { cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid); } if (available) { - super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/clients/" + cid + "/available"); + super.uri = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/clients/" + cid + "/available"); } else if (effective) { - super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/clients/" + cid + "/composite"); + super.uri = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/clients/" + cid + "/composite"); } else { - super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/clients/" + cid); + super.uri = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/clients/" + cid); } } else { // list realm roles for a group if (available) { - super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/realm/available"); + super.uri = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/realm/available"); } else if (effective) { - super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/realm/composite"); + super.uri = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/realm/composite"); } else { - super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + (all ? "/role-mappings" : "/role-mappings/realm")); + super.uri = composeResourceUrl(adminRoot, realm, "groups/" + gid + (all ? "/role-mappings" : "/role-mappings/realm")); } } } else if (isCompositeRoleSpecified()) { @@ -248,7 +234,7 @@ public class GetRolesCmd extends GetCmd { uri += all ? "/composites" : "/composites/realm"; } - super.url = composeResourceUrl(adminRoot, realm, uri); + super.uri = composeResourceUrl(adminRoot, realm, uri); } else if (isClientSpecified()) { if (cid == null) { @@ -260,10 +246,10 @@ public class GetRolesCmd extends GetCmd { if (rolename == null) { rolename = RoleOperations.getClientRoleNameFromId(adminRoot, realm, auth, cid, roleid); } - super.url = composeResourceUrl(adminRoot, realm, "clients/" + cid + "/roles/" + rolename); + super.uri = composeResourceUrl(adminRoot, realm, "clients/" + cid + "/roles/" + rolename); } else { // list defined client roles - super.url = composeResourceUrl(adminRoot, realm, "clients/" + cid + "/roles"); + super.uri = composeResourceUrl(adminRoot, realm, "clients/" + cid + "/roles"); } } else { if (isRoleSpecified()) { @@ -271,14 +257,14 @@ public class GetRolesCmd extends GetCmd { if (rolename == null) { rolename = RoleOperations.getClientRoleNameFromId(adminRoot, realm, auth, cid, roleid); } - super.url = composeResourceUrl(adminRoot, realm, "roles/" + rolename); + super.uri = composeResourceUrl(adminRoot, realm, "roles/" + rolename); } else { // list defined realm roles - super.url = composeResourceUrl(adminRoot, realm, "roles"); + super.uri = composeResourceUrl(adminRoot, realm, "roles"); } } - return super.process(commandInvocation); + super.process(); } private boolean isRoleSpecified() { @@ -301,14 +287,12 @@ public class GetRolesCmd extends GetCmd { return uid != null || uusername != null; } - protected String suggestHelp() { - return ""; - } - + @Override protected boolean nothingToDo() { return false; } + @Override protected String help() { return usage(); } diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/HelpCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/HelpCmd.java index 191f7d9046..ff675ab4e2 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/HelpCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/HelpCmd.java @@ -16,92 +16,81 @@ */ package org.keycloak.client.admin.cli.commands; -import org.jboss.aesh.cl.Arguments; -import org.jboss.aesh.cl.CommandDefinition; -import org.jboss.aesh.console.command.Command; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; - import java.util.List; +import picocli.CommandLine.Command; +import picocli.CommandLine.Parameters; + import static org.keycloak.client.admin.cli.util.IoUtil.printOut; +@Command(name = "help", description = "This Help") +public class HelpCmd implements Runnable { -/** - * @author Marko Strukelj - */ -@CommandDefinition(name = "help", description = "This help") -public class HelpCmd implements Command { - - @Arguments + @Parameters List args; @Override - public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - try { - if (args == null || args.size() == 0) { - printOut(KcAdmCmd.usage()); - } else { - outer: - switch (args.get(0)) { - case "config": { - if (args.size() > 1) { - switch (args.get(1)) { - case "credentials": { - printOut(ConfigCredentialsCmd.usage()); - break outer; - } - case "truststore": { - printOut(ConfigTruststoreCmd.usage()); - break outer; - } - } - } - printOut(ConfigCmd.usage()); - break; + public void run() { + if (args == null || args.size() == 0) { + printOut(KcAdmCmd.usage()); + } else { + outer: switch (args.get(0)) { + case "config": { + if (args.size() > 1) { + switch (args.get(1)) { + case "credentials": { + printOut(ConfigCredentialsCmd.usage()); + break outer; } - case "create": { - printOut(CreateCmd.usage()); - break; + case "truststore": { + printOut(ConfigTruststoreCmd.usage()); + break outer; } - case "get": { - printOut(GetCmd.usage()); - break; - } - case "update": { - printOut(UpdateCmd.usage()); - break; - } - case "delete": { - printOut(DeleteCmd.usage()); - break; - } - case "get-roles": { - printOut(GetRolesCmd.usage()); - break; - } - case "add-roles": { - printOut(AddRolesCmd.usage()); - break; - } - case "remove-roles": { - printOut(RemoveRolesCmd.usage()); - break; - } - case "set-password": { - printOut(SetPasswordCmd.usage()); - break; - } - default: { - throw new RuntimeException("Unknown command: " + args.get(0)); } } + printOut(ConfigCmd.usage()); + break; + } + case "create": { + printOut(CreateCmd.usage()); + break; + } + case "get": { + printOut(GetCmd.usage()); + break; + } + case "update": { + printOut(UpdateCmd.usage()); + break; + } + case "delete": { + printOut(DeleteCmd.usage()); + break; + } + case "get-roles": { + printOut(GetRolesCmd.usage()); + break; + } + case "add-roles": { + printOut(AddRolesCmd.usage()); + break; + } + case "remove-roles": { + printOut(RemoveRolesCmd.usage()); + break; + } + case "set-password": { + printOut(SetPasswordCmd.usage()); + break; + } + case "new-object": { + printOut(NewObjectCmd.usage()); + break; + } + default: { + throw new IllegalArgumentException("Unknown command: " + args.get(0)); + } } - - return CommandResult.SUCCESS; - } finally { - commandInvocation.stop(); } } } diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/KcAdmCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/KcAdmCmd.java index 84258f33fa..904c4e9209 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/KcAdmCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/KcAdmCmd.java @@ -16,47 +16,42 @@ */ package org.keycloak.client.admin.cli.commands; -import org.jboss.aesh.cl.GroupCommandDefinition; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; - import java.io.PrintWriter; import java.io.StringWriter; +import picocli.CommandLine.Command; + import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; -import static org.keycloak.client.admin.cli.util.IoUtil.printErr; -import static org.keycloak.client.admin.cli.util.IoUtil.printOut; import static org.keycloak.client.admin.cli.util.OsUtil.CMD; import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT; - -/** - * @author Marko Strukelj - */ - -@GroupCommandDefinition(name = "kcadm", description = "COMMAND [ARGUMENTS]", groupCommands = { - HelpCmd.class, ConfigCmd.class, NewObjectCmd.class, CreateCmd.class, GetCmd.class, UpdateCmd.class, DeleteCmd.class, - AddRolesCmd.class, RemoveRolesCmd.class, GetRolesCmd.class, SetPasswordCmd.class} ) +@Command(name = "kcadm", +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, + NewObjectCmd.class, + CreateCmd.class, + GetCmd.class, + UpdateCmd.class, + DeleteCmd.class, + AddRolesCmd.class, + RemoveRolesCmd.class, + GetRolesCmd.class, + SetPasswordCmd.class +}) public class KcAdmCmd extends AbstractGlobalOptionsCmd { @Override - public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - try { - // if --help was requested then status is SUCCESS - // if not we print help anyway, but status is FAILURE - if (printHelp()) { - return CommandResult.SUCCESS; - } else if (args != null && args.size() > 0) { - printErr("Unknown command: " + args.get(0)); - return CommandResult.FAILURE; - } else { - printOut(usage()); - return CommandResult.FAILURE; - } - } finally { - commandInvocation.stop(); - } + protected boolean nothingToDo() { + return true; } public static String usage() { diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/NewObjectCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/NewObjectCmd.java index 2d4520c106..47bc1e884e 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/NewObjectCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/NewObjectCmd.java @@ -17,11 +17,10 @@ package org.keycloak.client.admin.cli.commands; import com.fasterxml.jackson.databind.JsonNode; -import org.jboss.aesh.cl.CommandDefinition; -import org.jboss.aesh.cl.Option; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; + +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + import org.keycloak.client.admin.cli.common.AttributeOperation; import org.keycloak.client.admin.cli.common.CmdStdinContext; import org.keycloak.client.admin.cli.util.AccessibleBufferOutputStream; @@ -32,15 +31,14 @@ import java.io.InputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.nio.charset.StandardCharsets; -import java.util.Iterator; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.SET; import static org.keycloak.client.admin.cli.util.IoUtil.copyStream; import static org.keycloak.client.admin.cli.util.IoUtil.printErr; import static org.keycloak.client.admin.cli.util.OsUtil.CMD; -import static org.keycloak.client.admin.cli.util.OsUtil.EOL; import static org.keycloak.client.admin.cli.util.OsUtil.OS_ARCH; import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT; import static org.keycloak.client.admin.cli.util.OutputUtil.MAPPER; @@ -51,59 +49,24 @@ import static org.keycloak.client.admin.cli.util.ParseUtil.parseKeyVal; /** * @author Marko Strukelj */ -@CommandDefinition(name = "new-object", description = "Command to create new JSON objects locally") +@Command(name = "new-object", description = "Command to create new JSON objects locally") public class NewObjectCmd extends AbstractGlobalOptionsCmd { - @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 '-'") String file; - @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") boolean compressed; - //@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 values = new ArrayList<>(); @Override - public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - try { - if (printHelp()) { - return help ? CommandResult.SUCCESS : CommandResult.FAILURE; - } - - processGlobalOptions(); - - return process(commandInvocation); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e); - } finally { - commandInvocation.stop(); - } - } - - public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - - List attrs = new LinkedList<>(); - - 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("Invalid option: " + option); - } - } - } + public void process() { + List attrs = values.stream().map(it -> { + String[] keyVal = parseKeyVal(it); + return new AttributeOperation(SET, keyVal[0], keyVal[1]); + }).collect(Collectors.toList()); InputStream body = null; @@ -142,20 +105,14 @@ public class NewObjectCmd extends AbstractGlobalOptionsCmd { if (lastByte != -1 && lastByte != 13 && lastByte != 10) { printErr(""); } - - return CommandResult.SUCCESS; } - @Override protected boolean nothingToDo() { - return file == null && (args == null || args.size() == 0); - } - - protected String suggestHelp() { - return EOL + "Try '" + CMD + " help create' for more information"; + return file == null && values.isEmpty(); } + @Override protected String help() { return usage(); } diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/RemoveRolesCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/RemoveRolesCmd.java index 251b3bbe0a..959833f8a0 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/RemoveRolesCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/RemoveRolesCmd.java @@ -16,250 +16,218 @@ */ package org.keycloak.client.admin.cli.commands; -import com.fasterxml.jackson.databind.node.ObjectNode; -import org.jboss.aesh.cl.CommandDefinition; -import org.jboss.aesh.cl.Option; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; import org.keycloak.client.admin.cli.config.ConfigData; import org.keycloak.client.admin.cli.operations.ClientOperations; import org.keycloak.client.admin.cli.operations.GroupOperations; -import org.keycloak.client.admin.cli.operations.RoleOperations; import org.keycloak.client.admin.cli.operations.LocalSearch; +import org.keycloak.client.admin.cli.operations.RoleOperations; import org.keycloak.client.admin.cli.operations.UserOperations; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Set; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken; import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable; import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig; import static org.keycloak.client.admin.cli.util.OsUtil.CMD; -import static org.keycloak.client.admin.cli.util.OsUtil.EOL; import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT; /** * @author Marko Strukelj */ -@CommandDefinition(name = "remove-roles", description = "[ARGUMENTS]") +@Command(name = "remove-roles", description = "[ARGUMENTS]") public class RemoveRolesCmd extends AbstractAuthOptionsCmd { - @Option(name = "uusername", description = "Target user's 'username'") + @Option(names = "--uusername", description = "Target user's 'username'") String uusername; - @Option(name = "uid", description = "Target user's 'id'") + @Option(names = "--uid", description = "Target user's 'id'") String uid; - @Option(name = "gname", description = "Target group's 'name'") + @Option(names = "--gname", description = "Target group's 'name'") String gname; - @Option(name = "gpath", description = "Target group's 'path'") + @Option(names = "--gpath", description = "Target group's 'path'") String gpath; - @Option(name = "gid", description = "Target group's 'id'") + @Option(names = "--gid", description = "Target group's 'id'") String gid; - @Option(name = "rname", description = "Composite role's 'name'") + @Option(names = "--rname", description = "Composite role's 'name'") String rname; - @Option(name = "rid", description = "Composite role's 'id'") + @Option(names = "--rid", description = "Composite role's 'id'") String rid; - @Option(name = "cclientid", description = "Target client's 'clientId'") + @Option(names = "--cclientid", description = "Target client's 'clientId'") String cclientid; - @Option(name = "cid", description = "Target client's 'id'") + @Option(names = "--cid", description = "Target client's 'id'") String cid; + @Option(names = "--rolename", description = "Role's 'name' attribute") + List roleNames = new ArrayList<>(); + + @Option(names = "--roleid", description = "Role's 'id' attribute") + List roleIds = new ArrayList<>(); + @Override - public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { + protected void process() { + if (uid != null && uusername != null) { + throw new IllegalArgumentException("Incompatible options: --uid and --uusername are mutually exclusive"); + } - List roleNames = new LinkedList<>(); - List roleIds = new LinkedList<>(); + if ((gid != null && gname != null) || (gid != null && gpath != null) || (gname != null && gpath != null)) { + throw new IllegalArgumentException( + "Incompatible options: --gid, --gname and --gpath are mutually exclusive"); + } - try { - if (printHelp()) { - return help ? CommandResult.SUCCESS : CommandResult.FAILURE; + if (roleNames.isEmpty() && roleIds.isEmpty()) { + throw new IllegalArgumentException( + "No role to remove specified. Use --rolename or --roleid to specify roles to remove"); + } + + if (cid != null && cclientid != null) { + throw new IllegalArgumentException("Incompatible options: --cid and --cclientid are mutually exclusive"); + } + + if (rid != null && rname != null) { + throw new IllegalArgumentException("Incompatible options: --rid and --rname are mutually exclusive"); + } + + if (isUserSpecified() && isGroupSpecified()) { + throw new IllegalArgumentException( + "Incompatible options: --uusername / --uid can't be used at the same time as --gname / --gid / --gpath"); + } + + if (isUserSpecified() && isCompositeRoleSpecified()) { + throw new IllegalArgumentException( + "Incompatible options: --uusername / --uid can't be used at the same time as --rname / --rid"); + } + + if (isGroupSpecified() && isCompositeRoleSpecified()) { + throw new IllegalArgumentException( + "Incompatible options: --rname / --rid can't be used at the same time as --gname / --gid / --gpath"); + } + + if (!isUserSpecified() && !isGroupSpecified() && !isCompositeRoleSpecified()) { + throw new IllegalArgumentException( + "No user nor group nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role"); + } + + ConfigData config = loadConfig(); + config = copyWithServerInfo(config); + + setupTruststore(config); + + String 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 = getTargetRealm(config); + final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server); + + if (isUserSpecified()) { + if (uid == null) { + uid = UserOperations.getIdFromUsername(adminRoot, realm, auth, uusername); } - - processGlobalOptions(); - - Iterator it = args.iterator(); - - while (it.hasNext()) { - String option = it.next(); - switch (option) { - case "--rolename": { - optionRequiresValueCheck(it, option); - roleNames.add(it.next()); - break; - } - case "--roleid": { - optionRequiresValueCheck(it, option); - roleIds.add(it.next()); - break; - } - default: { - throw new IllegalArgumentException("Invalid option: " + option); - } - } - } - - if (uid != null && uusername != null) { - throw new IllegalArgumentException("Incompatible options: --uid and --uusername are mutually exclusive"); - } - - if ((gid != null && gname != null) || (gid != null && gpath != null) || (gname != null && gpath != null)) { - throw new IllegalArgumentException("Incompatible options: --gid, --gname and --gpath are mutually exclusive"); - } - - if (roleNames.isEmpty() && roleIds.isEmpty()) { - throw new IllegalArgumentException("No role to remove specified. Use --rolename or --roleid to specify roles to remove"); - } - - if (cid != null && cclientid != null) { - throw new IllegalArgumentException("Incompatible options: --cid and --cclientid are mutually exclusive"); - } - - if (rid != null && rname != null) { - throw new IllegalArgumentException("Incompatible options: --rid and --rname are mutually exclusive"); - } - - if (isUserSpecified() && isGroupSpecified()) { - throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --gname / --gid / --gpath"); - } - - if (isUserSpecified() && isCompositeRoleSpecified()) { - throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --rname / --rid"); - } - - if (isGroupSpecified() && isCompositeRoleSpecified()) { - throw new IllegalArgumentException("Incompatible options: --rname / --rid can't be used at the same time as --gname / --gid / --gpath"); - } - - if (!isUserSpecified() && !isGroupSpecified() && !isCompositeRoleSpecified()) { - throw new IllegalArgumentException("No user nor group nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role"); - } - - - ConfigData config = loadConfig(); - config = copyWithServerInfo(config); - - setupTruststore(config, commandInvocation); - - String auth = null; - - config = ensureAuthInfo(config, commandInvocation); - config = copyWithServerInfo(config); - if (credentialsAvailable(config)) { - auth = ensureToken(config); - } - - auth = auth != null ? "Bearer " + auth : null; - - final String server = config.getServerUrl(); - final String realm = getTargetRealm(config); - final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server); - - - if (isUserSpecified()) { - if (uid == null) { - uid = UserOperations.getIdFromUsername(adminRoot, realm, auth, uusername); - } - if (isClientSpecified()) { - // remove client roles from a user - if (cid == null) { - cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid); - } - - List roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth); - Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles)); - - // now remove the roles - UserOperations.removeClientRoles(adminRoot, realm, auth, uid, cid, new ArrayList<>(rolesToAdd)); - - } else { - - Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, - new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth))); - - // now remove the roles - UserOperations.removeRealmRoles(adminRoot, realm, auth, uid, new ArrayList<>(rolesToAdd)); + if (isClientSpecified()) { + // remove client roles from a user + if (cid == null) { + cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid); } - } else if (isGroupSpecified()) { - if (gname != null) { - gid = GroupOperations.getIdFromName(adminRoot, realm, auth, gname); - } else if (gpath != null) { - gid = GroupOperations.getIdFromPath(adminRoot, realm, auth, gpath); - } - if (isClientSpecified()) { - // remove client roles from a group - if (cid == null) { - cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid); - } + List roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth); + Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles)); - List roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth); - Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles)); - - // now remove the roles - GroupOperations.removeClientRoles(adminRoot, realm, auth, gid, cid, new ArrayList<>(rolesToAdd)); - - } else { - - Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, - new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth))); - - // now remove the roles - GroupOperations.removeRealmRoles(adminRoot, realm, auth, gid, new ArrayList<>(rolesToAdd)); - } - - } else if (isCompositeRoleSpecified()) { - if (rid == null) { - rid = RoleOperations.getIdFromRoleName(adminRoot, realm, auth, rname); - } - if (isClientSpecified()) { - // remove client roles from a role - if (cid == null) { - cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid); - } - - List roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth); - Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles)); - - // now remove the roles - RoleOperations.removeClientRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd)); - - } else { - Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, - new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth))); - - // now remove the roles - RoleOperations.removeRealmRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd)); - } + // now remove the roles + UserOperations.removeClientRoles(adminRoot, realm, auth, uid, cid, new ArrayList<>(rolesToAdd)); } else { - throw new IllegalArgumentException("No user nor group, nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role"); + + Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, + new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth))); + + // now remove the roles + UserOperations.removeRealmRoles(adminRoot, realm, auth, uid, new ArrayList<>(rolesToAdd)); } - return CommandResult.SUCCESS; + } else if (isGroupSpecified()) { + if (gname != null) { + gid = GroupOperations.getIdFromName(adminRoot, realm, auth, gname); + } else if (gpath != null) { + gid = GroupOperations.getIdFromPath(adminRoot, realm, auth, gpath); + } + if (isClientSpecified()) { + // remove client roles from a group + if (cid == null) { + cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid); + } - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e); - } finally { - commandInvocation.stop(); + List roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth); + Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles)); + + // now remove the roles + GroupOperations.removeClientRoles(adminRoot, realm, auth, gid, cid, new ArrayList<>(rolesToAdd)); + + } else { + + Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, + new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth))); + + // now remove the roles + GroupOperations.removeRealmRoles(adminRoot, realm, auth, gid, new ArrayList<>(rolesToAdd)); + } + + } else if (isCompositeRoleSpecified()) { + if (rid == null) { + rid = RoleOperations.getIdFromRoleName(adminRoot, realm, auth, rname); + } + if (isClientSpecified()) { + // remove client roles from a role + if (cid == null) { + cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid); + } + + List roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth); + Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles)); + + // now remove the roles + RoleOperations.removeClientRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd)); + + } else { + Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, + new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth))); + + // now remove the roles + RoleOperations.removeRealmRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd)); + } + + } else { + throw new IllegalArgumentException( + "No user nor group, nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role"); } } - private Set getRoleRepresentations(List roleNames, List roleIds, LocalSearch roleSearch) { + private Set getRoleRepresentations(List roleNames, List roleIds, + LocalSearch roleSearch) { Set rolesToAdd = new HashSet<>(); // now we process roles @@ -280,12 +248,6 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd { return rolesToAdd; } - private void optionRequiresValueCheck(Iterator it, String option) { - if (!it.hasNext()) { - throw new IllegalArgumentException("Option " + option + " requires a value"); - } - } - private boolean isClientSpecified() { return cid != null || cclientid != null; } @@ -304,13 +266,11 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd { @Override protected boolean nothingToDo() { - return noOptions() && uusername == null && uid == null && cclientid == null && (args == null || args.size() == 0); - } - - protected String suggestHelp() { - return EOL + "Try '" + CMD + " help remove-roles' for more information"; + return super.nothingToDo() && uusername == null && uid == null && cclientid == null + && roleIds.isEmpty() && roleNames.isEmpty(); } + @Override protected String help() { return usage(); } diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/SetPasswordCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/SetPasswordCmd.java index 8f047c7b83..038b305f9f 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/SetPasswordCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/SetPasswordCmd.java @@ -16,16 +16,14 @@ */ package org.keycloak.client.admin.cli.commands; -import org.jboss.aesh.cl.CommandDefinition; -import org.jboss.aesh.cl.Option; -import org.jboss.aesh.console.command.CommandException; -import org.jboss.aesh.console.command.CommandResult; -import org.jboss.aesh.console.command.invocation.CommandInvocation; import org.keycloak.client.admin.cli.config.ConfigData; import java.io.PrintWriter; import java.io.StringWriter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + import static org.keycloak.client.admin.cli.operations.UserOperations.getIdFromUsername; import static org.keycloak.client.admin.cli.operations.UserOperations.resetUserPassword; import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken; @@ -34,52 +32,28 @@ import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig; import static org.keycloak.client.admin.cli.util.IoUtil.readSecret; import static org.keycloak.client.admin.cli.util.OsUtil.CMD; -import static org.keycloak.client.admin.cli.util.OsUtil.EOL; import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT; /** * @author Marko Strukelj */ -@CommandDefinition(name = "set-password", description = "[ARGUMENTS]") +@Command(name = "set-password", description = "[ARGUMENTS]") public class SetPasswordCmd extends AbstractAuthOptionsCmd { - @Option(name = "username", description = "Username") + @Option(names = "--username", description = "Username") String username; - @Option(name = "userid", description = "User ID") + @Option(names = "--userid", description = "User ID") String userid; - @Option(shortName = 'p', name = "new-password", description = "New password") + @Option(names = {"-p", "--new-password"}, description = "New password") String pass; - @Option(shortName = 't', name = "temporary", description = "is password temporary", hasValue = false) + @Option(names = {"-t", "--temporary"}, description = "is password temporary") boolean temporary; - @Override - public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - try { - if (printHelp()) { - return help ? CommandResult.SUCCESS : CommandResult.FAILURE; - } - - processGlobalOptions(); - - return process(commandInvocation); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e); - } finally { - commandInvocation.stop(); - } - } - - - public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException { - - if (args != null && args.size() > 0) { - throw new IllegalArgumentException("Invalid option: " + args.get(0)); - } - + protected void process() { if (userid == null && username == null) { throw new IllegalArgumentException("No user specified. Use --username or --userid to specify user"); } @@ -89,17 +63,17 @@ public class SetPasswordCmd extends AbstractAuthOptionsCmd { } if (pass == null) { - pass = readSecret("Enter password: ", commandInvocation); + pass = readSecret("Enter password: "); } ConfigData config = loadConfig(); config = copyWithServerInfo(config); - setupTruststore(config, commandInvocation); + setupTruststore(config); String auth = null; - config = ensureAuthInfo(config, commandInvocation); + config = ensureAuthInfo(config); config = copyWithServerInfo(config); if (credentialsAvailable(config)) { auth = ensureToken(config); @@ -117,20 +91,14 @@ public class SetPasswordCmd extends AbstractAuthOptionsCmd { } resetUserPassword(adminRoot, realm, auth, userid, pass, temporary); - - return CommandResult.SUCCESS; } @Override protected boolean nothingToDo() { - return noOptions() && username == null && userid == null && pass == null; + return super.nothingToDo() && username == null && userid == null && pass == null; } - protected String suggestHelp() { - return EOL + "Try '" + CMD + " help set-password' for more information"; - } - - + @Override protected String help() { return usage(); } diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/UpdateCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/UpdateCmd.java index 63c8f905ba..c503ab9b21 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/UpdateCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/UpdateCmd.java @@ -17,77 +17,68 @@ package org.keycloak.client.admin.cli.commands; -import org.jboss.aesh.cl.CommandDefinition; -import org.jboss.aesh.cl.Option; - import java.io.PrintWriter; import java.io.StringWriter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.admin.cli.util.OsUtil.CMD; -import static org.keycloak.client.admin.cli.util.OsUtil.EOL; import static org.keycloak.client.admin.cli.util.OsUtil.OS_ARCH; import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT; /** * @author Marko Strukelj */ -@CommandDefinition(name = "update", description = "CLIENT_ID [ARGUMENTS]") +@Command(name = "update", description = "CLIENT_ID [ARGUMENTS]") public class UpdateCmd extends AbstractRequestCmd { - @Option(shortName = 'f', name = "file", description = "Read object from file or standard input if FILENAME is set to '-'") - String file; + public UpdateCmd() { + this.httpVerb = "put"; + } - @Option(shortName = 'b', name = "body", description = "JSON object to be sent as-is or used as a template") - String body; + @Option(names = {"-f", "--file"}, description = "Read object from file or standard input if FILENAME is set to '-'") + public void setFile(String file) { + this.file = file; + } - @Option(shortName = 'F', name = "fields", description = "A pattern specifying which attributes of JSON response body to actually display as result - causes mismatch with Content-Length header") - String fields; + @Option(names = {"-b", "--body"}, description = "JSON object to be sent as-is or used as a template") + public void setBody(String body) { + this.body = body; + } - @Option(shortName = 'H', name = "print-headers", description = "Print response headers", hasValue = false) - boolean printHeaders; + @Option(names = {"-F", "--fields"}, description = "A pattern specifying which attributes of JSON response body to actually display as result - causes mismatch with Content-Length header") + public void setFields(String fields) { + this.fields = fields; + } - @Option(shortName = 'm', name = "merge", description = "Merge new values with existing configuration on the server - for when the default is not to merge (i.e. if --file is used)", hasValue = false) - boolean mergeMode; + @Option(names = {"-H", "--print-headers"}, description = "Print response headers") + public void setPrintHeaders(boolean printHeaders) { + this.printHeaders = printHeaders; + } - @Option(shortName = 'n', name = "no-merge", description = "Don't merge new values with existing configuration on the server - for when the default is to merge (i.e. is --set is used while --file is not used)", hasValue = false) - boolean noMerge; + @Option(names = {"-m", "--merge"}, description = "Merge new values with existing configuration on the server - for when the default is not to merge (i.e. if --file is used)") + public void setMergeMode(boolean mergeMode) { + this.mergeMode = mergeMode; + } - @Option(shortName = 'o', name = "output", description = "After update output the new client configuration", hasValue = false) - boolean outputResult; + @Option(names = {"-n", "--no-merge"}, description = "Don't merge new values with existing configuration on the server - for when the default is to merge (i.e. is --set is used while --file is not used)") + public void setNoMerge(boolean noMerge) { + this.noMerge = noMerge; + } - @Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false) - boolean compressed; + @Option(names = {"-o", "--output"}, description = "After update output the new client configuration") + public void setOutputResult(boolean outputResult) { + this.outputResult = outputResult; + } - //@GroupOption(shortName = 's', name = "set", description = "Set specific attribute to a specified value", hasValue = true) - //private List attributes = new ArrayList<>(); - - - @Override - void initOptions() { - // set options on parent - super.file = file; - super.body = body; - super.fields = fields; - super.printHeaders = printHeaders; - super.returnId = false; - super.outputResult = true; - super.compressed = compressed; - super.mergeMode = mergeMode; - super.noMerge = noMerge; - super.outputResult = outputResult; - super.httpVerb = "put"; + @Option(names = {"-c", "--compressed"}, description = "Don't pretty print the output") + public void setCompressed(boolean compressed) { + this.compressed = compressed; } @Override - protected boolean nothingToDo() { - return noOptions() && file == null && body == null && (args == null || args.size() == 0); - } - - protected String suggestHelp() { - return EOL + "Try '" + CMD + " help update' for more information"; - } - protected String help() { return usage(); } diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ConfigUtil.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ConfigUtil.java index ff99eb328a..9d6033cb38 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ConfigUtil.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ConfigUtil.java @@ -69,7 +69,7 @@ public class ConfigUtil { public static void checkServerInfo(ConfigData config) { if (config.getServerUrl() == null) { - throw new RuntimeException("No server specified. Use --server, or '" + OsUtil.CMD + " config credentials or connection'."); + throw new RuntimeException("No server specified. Use --server, or '" + OsUtil.CMD + " config credentials'."); } if (config.getRealm() == null && config.getExternalToken() == null) { throw new RuntimeException("No realm or token specified. Use --realm, --token, or '" + OsUtil.CMD + " config credentials'."); diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/IoUtil.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/IoUtil.java index 7b5215d92b..c2e49870b8 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/IoUtil.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/IoUtil.java @@ -16,14 +16,7 @@ */ package org.keycloak.client.admin.cli.util; -import org.jboss.aesh.console.AeshConsoleBufferBuilder; -import org.jboss.aesh.console.AeshInputProcessorBuilder; -import org.jboss.aesh.console.ConsoleBuffer; -import org.jboss.aesh.console.InputProcessor; -import org.jboss.aesh.console.Prompt; -import org.jboss.aesh.console.command.invocation.CommandInvocation; -import org.keycloak.client.admin.cli.aesh.Globals; - +import java.io.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.admin.cli.util.OsUtil.OS_ARCH; /** * @author Marko Strukelj @@ -81,43 +73,16 @@ 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 = System.console(); + if (cons == null) { + throw new RuntimeException("Console is not active, but a password is required"); } - /* - if (!Globals.stdin.isStdinAvailable()) { - try { - return readLine(new InputStreamReader(System.in)); - } catch (IOException e) { - throw new RuntimeException("Standard input not available"); - } + char[] passwd; + if ((passwd = cons.readPassword("%s", prompt)) != null) { + return new String(passwd); } - */ - // Windows hack - get rid of any \n - result = result.replaceAll("\\n", ""); - return result; + throw new RuntimeException("No password provided"); } public static String readFully(InputStream is) { diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ParseUtil.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ParseUtil.java index 28a4dac422..7d7399ec01 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ParseUtil.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/util/ParseUtil.java @@ -38,7 +38,7 @@ public class ParseUtil { // we expect = as a separator int pos = keyval.indexOf("="); if (pos <= 0) { - throw new RuntimeException("Invalid key=value parameter: [" + keyval + "]"); + throw new IllegalArgumentException("Invalid key=value parameter: [" + keyval + "]"); } String [] parsed = new String[2]; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/AbstractCliTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/AbstractCliTest.java index 4850e3563b..d021648d9e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/AbstractCliTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/AbstractCliTest.java @@ -43,20 +43,12 @@ public abstract class AbstractCliTest extends AbstractKeycloakTest { public void assertExitCodeAndStreamSizes(AbstractExec exe, int exitCode, int stdOutLineCount, int stdErrLineCount) { Assert.assertEquals("exitCode == " + exitCode, exitCode, exe.exitCode()); if (stdOutLineCount != -1) { - try { - assertLineCount("stdout output", exe.stdoutLines(), stdOutLineCount); - } catch (Throwable e) { - throw new AssertionError("STDOUT: " + exe.stdoutString(), e); - } + assertLineCount("STDOUT: " + exe.stdoutString(), exe.stdoutLines(), stdOutLineCount); } // There is additional logging in case that BC FIPS libraries are used, so the count of logged lines don't match with the case with plain BC used // Hence we test count of lines just with FIPS disabled if (stdErrLineCount != -1 && isFipsDisabled()) { - try { - assertLineCount("stderr output", exe.stderrLines(), stdErrLineCount); - } catch (Throwable e) { - throw new AssertionError("STDERR: " + exe.stderrString(), e); - } + assertLineCount("STDERR: " + exe.stderrString(), exe.stderrLines(), stdErrLineCount); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmSessionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmSessionTest.java index 68e3d3e36d..d6c7611108 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmSessionTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmSessionTest.java @@ -29,9 +29,9 @@ public class KcAdmSessionTest extends AbstractAdmCliTest { @Test public void test() throws IOException { - FileConfigHandler handler = initCustomConfigFile(); + initCustomConfigFile(); - try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) { + try (TempFileResource configFile = new TempFileResource(FileConfigHandler.getConfigFile())) { // login as admin loginAsUser(configFile.getFile(), serverUrl, "master", "admin", "admin"); @@ -196,8 +196,8 @@ public class KcAdmSessionTest extends AbstractAdmCliTest { @Test public void testCompositeRoleCreationWithHigherVolumeOfRoles() throws Exception { - FileConfigHandler handler = initCustomConfigFile(); - try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) { + initCustomConfigFile(); + try (TempFileResource configFile = new TempFileResource(FileConfigHandler.getConfigFile())) { // login as admin loginAsUser(configFile.getFile(), serverUrl, "master", "admin", "admin"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTest.java index f019150e75..9641aa7d84 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTest.java @@ -34,8 +34,8 @@ public class KcAdmTest extends AbstractAdmCliTest { */ KcAdmExec 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)); } @@ -46,7 +46,7 @@ public class KcAdmTest extends AbstractAdmCliTest { */ KcAdmExec exe = KcAdmExec.execute(""); - assertExitCodeAndStdErrSize(exe, 1, 0); + assertExitCodeAndStdErrSize(exe, 2, 0); List lines = exe.stdoutLines(); Assert.assertTrue("stdout output not empty", lines.size() > 0); @@ -59,41 +59,41 @@ public class KcAdmTest extends AbstractAdmCliTest { * Test commands without arguments */ exe = KcAdmExec.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'", - exe.stderrLines().get(0)); + "Usage: kcadm.sh config SUB_COMMAND [ARGUMENTS]", + exe.stdoutLines().get(0)); exe = KcAdmExec.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 --user USER [--password PASSWORD] [ARGUMENTS]", exe.stdoutLines().get(0)); exe = KcAdmExec.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 = KcAdmExec.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 ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0)); //Assert.assertEquals("error message", "No file nor attribute values specified", exe.stderrLines().get(0)); exe = KcAdmExec.execute("get"); - assertExitCodeAndStdErrSize(exe, 1, 0); + assertExitCodeAndStdErrSize(exe, 2, 0); Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10); Assert.assertEquals("help message", "Usage: " + CMD + " get ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0)); //Assert.assertEquals("error message", "CLIENT not specified", exe.stderrLines().get(0)); exe = KcAdmExec.execute("update"); - assertExitCodeAndStdErrSize(exe, 1, 0); + assertExitCodeAndStdErrSize(exe, 2, 0); Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10); Assert.assertEquals("help message", "Usage: " + CMD + " update ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0)); //Assert.assertEquals("error message", "No file nor attribute values specified", exe.stderrLines().get(0)); exe = KcAdmExec.execute("delete"); - assertExitCodeAndStdErrSize(exe, 1, 0); + assertExitCodeAndStdErrSize(exe, 2, 0); Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10); Assert.assertEquals("help message", "Usage: " + CMD + " delete ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0)); //Assert.assertEquals("error message", "CLIENT not specified", exe.stderrLines().get(0)); @@ -112,18 +112,18 @@ public class KcAdmTest extends AbstractAdmCliTest { //Assert.assertEquals("help message", "Usage: " + CMD + " get-roles [--cclientid CLIENT_ID | --cid ID] [ARGUMENTS]", exe.stdoutLines().get(0)); exe = KcAdmExec.execute("add-roles"); - assertExitCodeAndStdErrSize(exe, 1, 0); + assertExitCodeAndStdErrSize(exe, 2, 0); Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10); Assert.assertEquals("help message", "Usage: " + CMD + " add-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]", exe.stdoutLines().get(0)); //Assert.assertEquals("error message", "CLIENT not specified", exe.stderrLines().get(0)); exe = KcAdmExec.execute("remove-roles"); - assertExitCodeAndStdErrSize(exe, 1, 0); + assertExitCodeAndStdErrSize(exe, 2, 0); Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10); Assert.assertEquals("help message", "Usage: " + CMD + " remove-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]", exe.stdoutLines().get(0)); exe = KcAdmExec.execute("set-password"); - assertExitCodeAndStdErrSize(exe, 1, 0); + assertExitCodeAndStdErrSize(exe, 2, 0); Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10); Assert.assertEquals("help message", "Usage: " + CMD + " set-password (--username USERNAME | --userid ID) [--new-password PASSWORD] [ARGUMENTS]", exe.stdoutLines().get(0)); //Assert.assertEquals("error message", "CLIENT not specified", exe.stderrLines().get(0)); @@ -202,8 +202,8 @@ public class KcAdmTest extends AbstractAdmCliTest { */ KcAdmExec exe = KcAdmExec.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 @@ -214,25 +214,23 @@ public class KcAdmTest extends AbstractAdmCliTest { KcAdmExec exe = KcAdmExec.execute("get users --nonexistent"); - assertExitCodeAndStreamSizes(exe, 1, 0, 2); - Assert.assertEquals("stderr first line", "Invalid option: --nonexistent", exe.stderrLines().get(0)); - Assert.assertEquals("try help", "Try '" + CMD + " help get' for more information", exe.stderrLines().get(1)); + 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)); - // set-password doesn't use @Arguments injection thus unsupported options are handled by Aesh exe = KcAdmExec.execute("set-password --nonexistent"); - assertExitCodeAndStreamSizes(exe, 1, 0, 2); - Assert.assertEquals("stderr first line", "Invalid option: --nonexistent", exe.stderrLines().get(0)); - Assert.assertEquals("try help", "Try '" + CMD + " help set-password' for more information", exe.stderrLines().get(1)); + assertExitCodeAndStreamSizes(exe, 2, 0, 3); + Assert.assertEquals("stderr first line", "Unknown option: '--nonexistent'", exe.stderrLines().get(0)); + Assert.assertEquals("try help", "Try '" + CMD + " set-password --help' for more information on the available options.", exe.stderrLines().get(2)); } @Test public void testBadOverlappingOption() { KcAdmExec exe = KcAdmExec.execute("config credentials --server http://localhost:8080 --realm master --username admin --password admin"); - assertExitCodeAndStreamSizes(exe, 1, 0, 1); - Assert.assertEquals("stderr first line", "Please double check your command options, one or more of them are not specified correctly. " - + "It is possible to have unintentional overlap with other options. e.g. using --clientid will get mistaken for --client, however --cclientid is needed.", exe.stderrLines().get(0)); + assertExitCodeAndStreamSizes(exe, 2, 0, 3); + Assert.assertEquals("stderr first line", "Unknown options: '--username', 'admin'", exe.stderrLines().get(0)); } @Test @@ -252,9 +250,9 @@ public class KcAdmTest extends AbstractAdmCliTest { */ KcAdmExec exe = KcAdmExec.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 @@ -264,9 +262,9 @@ public class KcAdmTest extends AbstractAdmCliTest { */ KcAdmExec exe = KcAdmExec.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 @@ -276,9 +274,9 @@ public class KcAdmTest extends AbstractAdmCliTest { */ KcAdmExec exe = KcAdmExec.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 @@ -396,7 +394,7 @@ public class KcAdmTest extends AbstractAdmCliTest { */ FileConfigHandler handler = initCustomConfigFile(); - File configFile = new File(handler.getConfigFile()); + File configFile = new File(FileConfigHandler.getConfigFile()); try { KcAdmExec exe = KcAdmExec.execute("config credentials --server " + serverUrl + " --realm master" + " --user admin --password admin --config '" + configFile.getName() + "'"); @@ -434,7 +432,7 @@ public class KcAdmTest extends AbstractAdmCliTest { // prepare for loading a config file FileConfigHandler handler = initCustomConfigFile(); - try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) { + try (TempFileResource configFile = new TempFileResource(FileConfigHandler.getConfigFile())) { KcAdmExec exe = KcAdmExec.execute("config credentials --server " + serverUrl + " --realm master --user admin --password admin --config '" + configFile.getName() + "'"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTruststoreTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTruststoreTest.java index 2cb41f66b8..2e64c72517 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTruststoreTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmTruststoreTest.java @@ -6,6 +6,7 @@ import org.keycloak.client.admin.cli.config.ConfigData; import org.keycloak.client.admin.cli.config.FileConfigHandler; import org.keycloak.client.admin.cli.util.OsUtil; import org.keycloak.testsuite.cli.KcAdmExec; +import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.TempFileResource; import java.io.File; @@ -14,7 +15,6 @@ import java.io.IOException; import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_PATH; import static org.keycloak.client.admin.cli.util.OsUtil.EOL; import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_SSL_REQUIRED; -import static org.keycloak.testsuite.cli.KcAdmExec.CMD; import static org.keycloak.testsuite.cli.KcAdmExec.execute; /** @@ -29,9 +29,9 @@ public class KcAdmTruststoreTest extends AbstractAdmCliTest { KcAdmExec 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 this test if ssl protected keycloak server is available if (!AUTH_SERVER_SSL_REQUIRED) { @@ -39,9 +39,9 @@ public class KcAdmTruststoreTest extends AbstractAdmCliTest { 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 +52,7 @@ public class KcAdmTruststoreTest extends AbstractAdmCliTest { // perform authentication against server - asks for password, then for truststore password exe = KcAdmExec.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 +72,7 @@ public class KcAdmTruststoreTest extends AbstractAdmCliTest { // perform authentication against server - asks for password, then for truststore password exe = KcAdmExec.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 +99,17 @@ public class KcAdmTruststoreTest extends AbstractAdmCliTest { 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 '" + OsUtil.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 '" + OsUtil.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/admin/KcAdmUpdateTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmUpdateTest.java index ff668ee8fa..3f537d8353 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmUpdateTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmUpdateTest.java @@ -33,10 +33,10 @@ public class KcAdmUpdateTest extends AbstractAdmCliTest { @Test public void testUpdateIDPWithoutInternalId() throws IOException { - + final String realm = "test"; final RealmResource realmResource = adminClient.realm(realm); - + IdentityProviderRepresentation identityProvider = IdentityProviderBuilder.create() .providerId(SAMLIdentityProviderFactory.PROVIDER_ID) .alias("idpAlias") @@ -48,10 +48,10 @@ public class KcAdmUpdateTest extends AbstractAdmCliTest { .setAttribute(SAMLIdentityProviderConfig.POST_BINDING_AUTHN_REQUEST, "false") .setAttribute(SAMLIdentityProviderConfig.BACKCHANNEL_SUPPORTED, "false") .build(); - + try (Closeable ipc = new IdentityProviderCreator(realmResource, identityProvider)) { - FileConfigHandler handler = initCustomConfigFile(); - try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) { + initCustomConfigFile(); + try (TempFileResource configFile = new TempFileResource(FileConfigHandler.getConfigFile())) { loginAsUser(configFile.getFile(), serverUrl, realm, "user1", "userpass"); KcAdmExec exe = execute("get identity-provider/instances/idpAlias -r " + realm + " --config " + configFile.getFile()); @@ -69,9 +69,9 @@ public class KcAdmUpdateTest extends AbstractAdmCliTest { @Test public void testUpdateThoroughly() throws IOException { - FileConfigHandler handler = initCustomConfigFile(); + initCustomConfigFile(); - try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) { + try (TempFileResource configFile = new TempFileResource(FileConfigHandler.getConfigFile())) { final String realm = "test"; @@ -136,9 +136,9 @@ public class KcAdmUpdateTest extends AbstractAdmCliTest { // check that using an invalid attribute key is not ignored exe = execute("update clients/" + client.getId() + " --nonexisting --config '" + configFile.getName() + "'"); - assertExitCodeAndStreamSizes(exe, 1, 0, 2); - Assert.assertEquals("error message", "Invalid option: --nonexisting", exe.stderrLines().get(0)); - Assert.assertEquals("try help", "Try '" + CMD + " help update' for more information", exe.stderrLines().get(1)); + 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)); // test overwrite from file