fix: replace aesh with picocli (#27458)

* fix: replace aesh with picocli

closes: #27388

Signed-off-by: Steve Hawkins <shawkins@redhat.com>

* Update integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractRequestCmd.java

Co-authored-by: Martin Bartoš <mabartos@redhat.com>

* splitting the error handling for password input

Signed-off-by: Steve Hawkins <shawkins@redhat.com>

* adding a change note about kcadm

Signed-off-by: Steve Hawkins <shawkins@redhat.com>

* Update docs/documentation/upgrading/topics/changes/changes-25_0_0.adoc

Co-authored-by: Martin Bartoš <mabartos@redhat.com>

---------

Signed-off-by: Steve Hawkins <shawkins@redhat.com>
Co-authored-by: Martin Bartoš <mabartos@redhat.com>
This commit is contained in:
Steven Hawkins 2024-03-28 09:34:06 -04:00 committed by GitHub
parent a74d833f22
commit e9ad9d0564
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 975 additions and 1614 deletions

View file

@ -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.

View file

@ -29,20 +29,10 @@
<name>Keycloak Admin CLI</name>
<description/>
<properties>
<jansi.version>1.18</jansi.version>
</properties>
<dependencies>
<dependency>
<groupId>org.jboss.aesh</groupId>
<artifactId>aesh</artifactId>
</dependency>
<!-- Jansi library version needs to be overridden due to the backwards compatibility - see #21851 -->
<dependency>
<groupId>org.fusesource.jansi</groupId>
<artifactId>jansi</artifactId>
<version>${jansi.version}</version>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>

View file

@ -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;
}
}

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -25,7 +23,6 @@ public class Globals {
public static boolean dumpTrace = false;
public static ValveInputStream stdin;
public static boolean help = false;
public static List<String> args;
}

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -48,52 +41,21 @@ public class KcAdmMain {
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<String> 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('\'');
CommandLine cli = createCommandLine();
int exitCode = cli.execute(args);
System.exit(exitCode);
}
console.setEcho(false);
console.execute("kcadm" + b.toString());
public static CommandLine createCommandLine() {
CommandSpec spec = CommandSpec.forAnnotatedObject(new KcAdmCmd()).name(OsUtil.CMD);
console.start();
}
CommandLine cmd = new CommandLine(spec);
cmd.setExecutionExceptionHandler(new ExecutionExceptionHandler());
cmd.setParameterExceptionHandler(new ShortErrorMessageHandler());
cmd.setErr(new PrintWriter(System.err, true));
return cmd;
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}
}

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
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);
}
}
}

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class ValveInputStream extends InputStream {
private BlockingQueue<InputStream> 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();
}
}

View file

@ -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;
}
}

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
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<String> 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<String> 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;
}
}

View file

@ -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,79 +104,47 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd {
String httpVerb;
Headers headers = new Headers();
@Option(names = {"-h", "--header"}, description = "Set request header NAME to VALUE")
List<String> rawHeaders = new LinkedList<>();
// to maintain relative positions of set and delete operations
static class AttributeOperations {
@Option(names = {"-s", "--set"}, required = true) String set;
@Option(names = {"-d", "--delete"}, required = true) String delete;
}
@ArgGroup(exclusive = true, multiplicity = "0..*")
List<AttributeOperations> rawAttributeOperations = new ArrayList<>();
@Option(names = {"-q", "--query"}, description = "Add to request URI a NAME query parameter with value VALUE")
List<String> rawFilters = new LinkedList<>();
@Parameters(arity = "0..1")
String uri;
List<AttributeOperation> attrs = new LinkedList<>();
Headers headers = new Headers();
Map<String, String> filter = new HashMap<>();
String url = null;
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
try {
initOptions();
protected void processOptions() {
super.processOptions();
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();
}
}
abstract void initOptions();
abstract String suggestHelp();
void processOptions(CommandInvocation commandInvocation) {
if (args == null || args.isEmpty()) {
throw new IllegalArgumentException("URI not specified");
}
Iterator<String> 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());
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]));
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());
for (String header : rawHeaders) {
String[] keyVal = parseKeyVal(header);
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();
for (String arg : rawFilters) {
String[] keyVal;
if (arg.indexOf("=") == -1) {
keyVal = new String[] {"", arg};
@ -183,20 +152,9 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd {
keyVal = parseKeyVal(arg);
}
filter.put(keyVal[0], keyVal[1]);
break;
}
default: {
if (url == null) {
url = option;
} else {
throw new IllegalArgumentException("Invalid option: " + option);
}
}
}
}
if (url == null) {
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() {

View file

@ -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,76 +40,49 @@ 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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@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<String> roleNames = new ArrayList<>();
@Option(names = "--roleid", description = "Role's 'id' attribute")
List<String> roleIds = new ArrayList<>();
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
List<String> roleNames = new LinkedList<>();
List<String> roleIds = new LinkedList<>();
try {
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
}
processGlobalOptions();
Iterator<String> 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);
}
}
}
protected void process() {
if (uid != null && uusername != null) {
throw new IllegalArgumentException("Incompatible options: --uid and --uusername are mutually exclusive");
}
@ -153,11 +123,11 @@ public class AddRolesCmd 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);
@ -249,14 +219,6 @@ public class AddRolesCmd extends AbstractAuthOptionsCmd {
} 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");
}
return CommandResult.SUCCESS;
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
} finally {
commandInvocation.stop();
}
}
private Set<ObjectNode> getRoleRepresentations(List<String> roleNames, List<String> roleIds, LocalSearch roleSearch) {
@ -280,12 +242,6 @@ public class AddRolesCmd extends AbstractAuthOptionsCmd {
return rolesToAdd;
}
private void optionRequiresValueCheck(Iterator<String> 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();
}

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@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();
}

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@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();
}

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@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<String> args = new ArrayList<>();
Iterator<String> 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();
}

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@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<String, String> 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();
}

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@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();
}

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "get", description = "[ARGUMENTS]")
@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();
}

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@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();
}

View file

@ -16,35 +16,25 @@
*/
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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "help", description = "This help")
public class HelpCmd implements Command {
@Arguments
@Parameters
List<String> args;
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
try {
public void run() {
if (args == null || args.size() == 0) {
printOut(KcAdmCmd.usage());
} else {
outer:
switch (args.get(0)) {
outer: switch (args.get(0)) {
case "config": {
if (args.size() > 1) {
switch (args.get(1)) {
@ -93,15 +83,14 @@ public class HelpCmd implements Command {
printOut(SetPasswordCmd.usage());
break;
}
case "new-object": {
printOut(NewObjectCmd.usage());
break;
}
default: {
throw new RuntimeException("Unknown command: " + args.get(0));
throw new IllegalArgumentException("Unknown command: " + args.get(0));
}
}
}
return CommandResult.SUCCESS;
} finally {
commandInvocation.stop();
}
}
}

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@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() {

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@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<String, String> attributes = new LinkedHashMap<>();
@Option(names = {"-s", "--set"}, description = "Set a specific attribute NAME to a specified value VALUE")
List<String> 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<AttributeOperation> attrs = new LinkedList<>();
Iterator<String> 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<AttributeOperation> 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();
}

View file

@ -16,113 +16,85 @@
*/
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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@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<String> roleNames = new ArrayList<>();
@Option(names = "--roleid", description = "Role's 'id' attribute")
List<String> roleIds = new ArrayList<>();
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
List<String> roleNames = new LinkedList<>();
List<String> roleIds = new LinkedList<>();
try {
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
}
processGlobalOptions();
Iterator<String> 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);
}
}
}
protected void process() {
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");
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");
throw new IllegalArgumentException(
"No role to remove specified. Use --rolename or --roleid to specify roles to remove");
}
if (cid != null && cclientid != null) {
@ -134,30 +106,33 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd {
}
if (isUserSpecified() && isGroupSpecified()) {
throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --gname / --gid / --gpath");
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");
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");
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");
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);
setupTruststore(config);
String auth = null;
config = ensureAuthInfo(config, commandInvocation);
config = ensureAuthInfo(config);
config = copyWithServerInfo(config);
if (credentialsAvailable(config)) {
auth = ensureToken(config);
@ -169,7 +144,6 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd {
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);
@ -247,19 +221,13 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd {
}
} 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");
}
return CommandResult.SUCCESS;
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
} finally {
commandInvocation.stop();
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<ObjectNode> getRoleRepresentations(List<String> roleNames, List<String> roleIds, LocalSearch roleSearch) {
private Set<ObjectNode> getRoleRepresentations(List<String> roleNames, List<String> roleIds,
LocalSearch roleSearch) {
Set<ObjectNode> rolesToAdd = new HashSet<>();
// now we process roles
@ -280,12 +248,6 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd {
return rolesToAdd;
}
private void optionRequiresValueCheck(Iterator<String> 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();
}

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@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();
}

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@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<String> 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();
}

View file

@ -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'.");

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -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) {

View file

@ -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];

View file

@ -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);
}
}

View file

@ -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");

View file

@ -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<String> 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() + "'");

View file

@ -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());

View file

@ -50,8 +50,8 @@ public class KcAdmUpdateTest extends AbstractAdmCliTest {
.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