fix: replaces aesh with picocli (#28276)

* fix: replaces aesh with picocli

closes: #28275

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

* fix: replaces aesh with picocli

closes: #28275

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

---------

Signed-off-by: Steve Hawkins <shawkins@redhat.com>
This commit is contained in:
Steven Hawkins 2024-04-15 09:04:58 -04:00 committed by GitHub
parent b428e7dc81
commit 58398d1f69
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 933 additions and 1591 deletions

View file

@ -69,9 +69,9 @@ Options `cache`, `cache-stack`, and `cache-config-file` are no longer build opti
This eliminates the need to execute the build phase and rebuild your image due to them.
Be aware that they will not be recognized during the `build` phase, so you need to remove them.
= kcadm Changes
= kcadm and kcreg changes
How kcadm parses and handles options and parameters has changed. Error messages from usage errors, the wrong option or parameter, may be slightly different than previous versions. Also usage errors will have an exit code of 2 instead of 1.
How kcadm and kcreg parse and handle options and parameters has changed. Error messages from usage errors, the wrong option or parameter, may be slightly different than previous versions. Also usage errors will have an exit code of 2 instead of 1.
= Removing custom user attribute indexes

View file

@ -31,8 +31,12 @@
<dependencies>
<dependency>
<groupId>org.jboss.aesh</groupId>
<artifactId>aesh</artifactId>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-cli</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>

View file

@ -0,0 +1,17 @@
package org.keycloak.client.registration.cli;
import org.keycloak.client.registration.cli.common.EndpointType;
import picocli.CommandLine.ITypeConverter;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class EndpointTypeConverter implements ITypeConverter<EndpointType> {
@Override
public EndpointType convert(String value) throws Exception {
return EndpointType.of(value);
}
}

View file

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

View file

@ -1,21 +1,16 @@
package org.keycloak.client.registration.cli;
import org.jboss.aesh.console.AeshConsoleBuilder;
import org.jboss.aesh.console.AeshConsoleImpl;
import org.jboss.aesh.console.Prompt;
import org.jboss.aesh.console.command.registry.AeshCommandRegistryBuilder;
import org.jboss.aesh.console.command.registry.CommandRegistry;
import org.jboss.aesh.console.settings.Settings;
import org.jboss.aesh.console.settings.SettingsBuilder;
import org.keycloak.client.registration.cli.aesh.AeshEnhancer;
import org.keycloak.client.registration.cli.aesh.ValveInputStream;
import org.keycloak.client.registration.cli.aesh.Globals;
import org.keycloak.client.admin.cli.ExecutionExceptionHandler;
import org.keycloak.client.admin.cli.ShortErrorMessageHandler;
import org.keycloak.client.registration.cli.commands.KcRegCmd;
import org.keycloak.client.registration.cli.util.ClassLoaderUtil;
import org.keycloak.client.registration.cli.util.OsUtil;
import org.keycloak.common.crypto.CryptoIntegration;
import java.util.ArrayList;
import java.util.Arrays;
import java.io.PrintWriter;
import picocli.CommandLine;
import picocli.CommandLine.Model.CommandSpec;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -32,54 +27,20 @@ public class KcRegMain {
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(KcRegCmd.class)
.create();
AeshConsoleImpl console = (AeshConsoleImpl) new AeshConsoleBuilder()
.settings(settings)
.commandRegistry(registry)
.prompt(new Prompt(""))
.create();
AeshEnhancer.enhance(console);
// work around parser issues with quotes and brackets
ArrayList<String> arguments = new ArrayList<>();
arguments.add("kcreg");
arguments.addAll(Arrays.asList(args));
Globals.args = arguments;
StringBuilder b = new StringBuilder();
for (String s : args) {
// quote if necessary
boolean needQuote = false;
needQuote = s.indexOf(' ') != -1 || s.indexOf('\"') != -1 || s.indexOf('\'') != -1;
b.append(' ');
if (needQuote) {
b.append('\'');
CommandLine cli = createCommandLine();
int exitCode = cli.execute(args);
System.exit(exitCode);
}
b.append(s);
if (needQuote) {
b.append('\'');
}
}
console.setEcho(false);
console.execute("kcreg" + b.toString());
public static CommandLine createCommandLine() {
CommandSpec spec = CommandSpec.forAnnotatedObject(new KcRegCmd()).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

@ -1,101 +0,0 @@
package org.keycloak.client.registration.cli.aesh;
import org.jboss.aesh.cl.parser.OptionParserException;
import org.jboss.aesh.cl.result.ResultHandler;
import org.jboss.aesh.console.AeshConsoleCallback;
import org.jboss.aesh.console.AeshConsoleImpl;
import org.jboss.aesh.console.ConsoleOperation;
import org.jboss.aesh.console.command.CommandNotFoundException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.container.CommandContainer;
import org.jboss.aesh.console.command.container.CommandContainerResult;
import org.jboss.aesh.console.command.invocation.AeshCommandInvocation;
import org.jboss.aesh.console.command.invocation.AeshCommandInvocationProvider;
import org.jboss.aesh.parser.AeshLine;
import org.jboss.aesh.parser.ParserStatus;
import java.lang.reflect.Method;
class AeshConsoleCallbackImpl extends AeshConsoleCallback {
private final AeshConsoleImpl console;
private CommandResult result;
AeshConsoleCallbackImpl(AeshConsoleImpl aeshConsole) {
this.console = aeshConsole;
}
@Override
@SuppressWarnings("unchecked")
public int execute(ConsoleOperation output) throws InterruptedException {
if (output != null && output.getBuffer().trim().length() > 0) {
ResultHandler resultHandler = null;
//AeshLine aeshLine = Parser.findAllWords(output.getBuffer());
AeshLine aeshLine = new AeshLine(output.getBuffer(), Globals.args, ParserStatus.OK, "");
try (CommandContainer commandContainer = getCommand(output, aeshLine)) {
resultHandler = commandContainer.getParser().getProcessedCommand().getResultHandler();
CommandContainerResult ccResult =
commandContainer.executeCommand(aeshLine, console.getInvocationProviders(), console.getAeshContext(),
new AeshCommandInvocationProvider().enhanceCommandInvocation(
new AeshCommandInvocation(console,
output.getControlOperator(), output.getPid(), this)));
result = ccResult.getCommandResult();
if(result == CommandResult.SUCCESS && resultHandler != null)
resultHandler.onSuccess();
else if(resultHandler != null)
resultHandler.onFailure(result);
if (result == CommandResult.FAILURE) {
// we assume the command has already output any error messages
System.exit(1);
}
} catch (Exception e) {
console.stop();
if (e instanceof OptionParserException) {
System.err.println("Unknown command: " + aeshLine.getWords().get(0));
} else {
System.err.println(e.getMessage());
}
if (Globals.dumpTrace) {
e.printStackTrace();
}
System.exit(1);
}
}
// empty line
else if (output != null) {
result = CommandResult.FAILURE;
}
else {
//stop();
result = CommandResult.FAILURE;
}
if (result == CommandResult.SUCCESS) {
return 0;
} else {
return 1;
}
}
private CommandContainer getCommand(ConsoleOperation output, AeshLine aeshLine) throws CommandNotFoundException {
Method m;
try {
m = console.getClass().getDeclaredMethod("getCommand", AeshLine.class, String.class);
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unexpected error: ", e);
}
m.setAccessible(true);
try {
return (CommandContainer) m.invoke(console, aeshLine, output.getBuffer());
} catch (Exception e) {
throw new RuntimeException("Unexpected error: ", e);
}
}
}

View file

@ -1,25 +0,0 @@
package org.keycloak.client.registration.cli.aesh;
import org.jboss.aesh.console.AeshConsoleImpl;
import org.jboss.aesh.console.Console;
import java.lang.reflect.Field;
/**
* @author <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,17 +0,0 @@
package org.keycloak.client.registration.cli.aesh;
import org.jboss.aesh.cl.converter.Converter;
import org.jboss.aesh.cl.validator.OptionValidatorException;
import org.jboss.aesh.console.command.converter.ConverterInvocation;
import org.keycloak.client.registration.cli.common.EndpointType;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class EndpointTypeConverter implements Converter<EndpointType, ConverterInvocation> {
@Override
public EndpointType convert(ConverterInvocation converterInvocation) throws OptionValidatorException {
return EndpointType.of(converterInvocation.getInput());
}
}

View file

@ -1,15 +0,0 @@
package org.keycloak.client.registration.cli.aesh;
import java.util.List;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class Globals {
public static boolean dumpTrace = false;
public static ValveInputStream stdin;
public static List<String> args;
}

View file

@ -1,71 +0,0 @@
package org.keycloak.client.registration.cli.aesh;
import org.jboss.aesh.console.AeshConsoleImpl;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* This stream blocks and waits, until there is some stream in the queue.
* It reads all streams from the queue, and then blocks until it receives more.
*/
public class ValveInputStream extends InputStream {
private BlockingQueue<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

@ -1,7 +1,5 @@
package org.keycloak.client.registration.cli.commands;
import org.jboss.aesh.cl.Option;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.keycloak.OAuth2Constants;
import org.keycloak.client.registration.cli.config.ConfigData;
import org.keycloak.client.registration.cli.config.ConfigHandler;
@ -14,6 +12,8 @@ import org.keycloak.client.registration.cli.util.IoUtil;
import java.io.File;
import picocli.CommandLine.Option;
import static org.keycloak.client.registration.cli.config.FileConfigHandler.setConfigFile;
import static org.keycloak.client.registration.cli.util.ConfigUtil.checkAuthInfo;
import static org.keycloak.client.registration.cli.util.ConfigUtil.checkServerInfo;
@ -26,58 +26,55 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
static final String DEFAULT_CLIENT = "admin-cli";
@Option(name = "config", description = "Path to the config file (~/.keycloak/kcreg.config by default)", hasValue = true)
@Option(names = "--config", description = "Path to the config file (~/.keycloak/kcreg.config by default)")
protected String config;
@Option(name = "no-config", description = "No configuration file should be used, no authentication info is loaded or saved", hasValue = false)
@Option(names = "--no-config", description = "No configuration file should be used, no authentication info is loaded or saved")
protected boolean noconfig;
@Option(name = "server", description = "Server endpoint url (e.g. 'http://localhost:8080')", hasValue = true)
@Option(names = "--server", description = "Server endpoint url (e.g. 'http://localhost:8080')")
protected String server;
@Option(name = "realm", description = "Realm name to authenticate against", hasValue = true)
@Option(names = "--realm", description = "Realm name to authenticate against")
protected String realm;
@Option(name = "client", description = "Realm name to authenticate against", hasValue = true)
@Option(names = "--client", description = "Realm name to authenticate against")
protected String clientId;
@Option(name = "user", description = "Username to login with", hasValue = true)
@Option(names = "--user", description = "Username to login with")
protected String user;
@Option(name = "password", description = "Password to login with (prompted for if not specified and --user is used)", hasValue = true)
@Option(names = "--password", description = "Password to login with (prompted for if not specified and --user is used)")
protected String password;
@Option(name = "secret", description = "Secret to authenticate the client (prompted for if no --user or --keystore is specified)", hasValue = true)
@Option(names = "--secret", description = "Secret to authenticate the client (prompted for if no --user or --keystore is specified)")
protected String secret;
@Option(name = "keystore", description = "Path to a keystore containing private key", hasValue = true)
@Option(names = "--keystore", description = "Path to a keystore containing private key")
protected String keystore;
@Option(name = "storepass", description = "Keystore password (prompted for if not specified and --keystore is used)", hasValue = true)
@Option(names = "--storepass", description = "Keystore password (prompted for if not specified and --keystore is used)")
protected String storePass;
@Option(name = "keypass", description = "Key password (prompted for if not specified and --keystore is used without --storepass, \n otherwise defaults to keystore password)", hasValue = true)
@Option(names = "--keypass", description = "Key password (prompted for if not specified and --keystore is used without --storepass, \n otherwise defaults to keystore password)")
protected String keyPass;
@Option(name = "alias", description = "Alias of the key inside a keystore (defaults to the value of ClientId)", hasValue = true)
@Option(names = "--alias", description = "Alias of the key inside a keystore (defaults to the value of ClientId)")
protected String alias;
@Option(name = "truststore", description = "Path to a truststore", hasValue = true)
@Option(names = "--truststore", description = "Path to a truststore")
protected String trustStore;
@Option(name = "trustpass", description = "Truststore password (prompted for if not specified and --truststore is used)", hasValue = true)
@Option(names = "--trustpass", description = "Truststore password (prompted for if not specified and --truststore is used)")
protected String trustPass;
@Option(name = "insecure", description = "Turns off TLS validation", hasValue = false)
@Option(names = "--insecure", description = "Turns off TLS validation")
protected boolean insecure;
@Option(shortName = 't', name = "token", description = "Initial / Registration access token to use)", hasValue = true)
@Option(names = {"-t", "--token"}, description = "Initial / Registration access token to use)")
protected String token;
protected void initFromParent(AbstractAuthOptionsCmd parent) {
super.initFromParent(parent);
noconfig = parent.noconfig;
config = parent.config;
server = parent.server;
@ -110,12 +107,11 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
token == null && config == null;
}
protected void processGlobalOptions() {
super.processGlobalOptions();
@Override
protected void processOptions() {
if (config != null && noconfig) {
throw new RuntimeException("Options --config and --no-config are mutually exclusive");
throw new IllegalArgumentException("Options --config and --no-config are mutually exclusive");
}
if (!noconfig) {
@ -130,7 +126,7 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
}
}
protected void setupTruststore(ConfigData configData, CommandInvocation invocation ) {
protected void setupTruststore(ConfigData configData) {
if (!configData.getServerUrl().startsWith("https:")) {
return;
@ -147,7 +143,7 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
pass = configData.getTrustpass();
}
if (pass == null) {
pass = IoUtil.readSecret("Enter truststore password: ", invocation);
pass = IoUtil.readSecret("Enter truststore password: ");
}
try {
@ -162,7 +158,7 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
}
}
protected ConfigData ensureAuthInfo(ConfigData config, CommandInvocation commandInvocation) {
protected ConfigData ensureAuthInfo(ConfigData config) {
if (requiresLogin()) {
// make sure current handler is in-memory handler
@ -178,7 +174,7 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
ConfigCredentialsCmd login = new ConfigCredentialsCmd();
login.initFromParent(this);
login.init(config);
login.process(commandInvocation);
login.process();
// this must be executed before finally block which restores config handler
return loadConfig();
@ -237,6 +233,7 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
rdata.setGrantTypeForAuthentication(grantTypeForAuthentication);
}
@Override
protected void checkUnsupportedOptions(String ... options) {
if (options.length % 2 != 0) {
throw new IllegalArgumentException("Even number of argument required");

View file

@ -1,38 +1,37 @@
package org.keycloak.client.registration.cli.commands;
import org.jboss.aesh.cl.Option;
import org.jboss.aesh.console.command.Command;
import org.keycloak.client.registration.cli.aesh.Globals;
import org.keycloak.client.registration.cli.Globals;
import picocli.CommandLine;
import picocli.CommandLine.Option;
import static org.keycloak.client.registration.cli.util.IoUtil.printOut;
/**
* @author <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)
protected boolean dumpTrace;
@Option(name = "help", description = "Print command specific help", hasValue = false)
protected boolean help;
protected void initFromParent(AbstractGlobalOptionsCmd parent) {
dumpTrace = parent.dumpTrace;
help = parent.help;
@Option(names = "--help",
description = "Print command specific help")
public void setHelp(boolean help) {
Globals.help = help;
}
protected void processGlobalOptions() {
@Option(names = "-x",
description = "Print full stack trace when exiting with error")
public void setDumpTrace(boolean dumpTrace) {
Globals.dumpTrace = dumpTrace;
}
protected boolean printHelp() {
if (help || nothingToDo()) {
protected void printHelpIfNeeded() {
if (Globals.help) {
printOut(help());
return true;
System.exit(CommandLine.ExitCode.OK);
} else if (nothingToDo()) {
printOut(help());
System.exit(CommandLine.ExitCode.USAGE);
}
return false;
}
protected boolean nothingToDo() {
@ -42,4 +41,47 @@ public abstract class AbstractGlobalOptionsCmd implements Command {
protected String help() {
return KcRegCmd.usage();
}
@Override
public void run() {
printHelpIfNeeded();
checkUnsupportedOptions(getUnsupportedOptions());
processOptions();
process();
}
protected String[] getUnsupportedOptions() {
return new String[0];
}
protected void processOptions() {
}
protected void process() {
}
protected void checkUnsupportedOptions(String ... options) {
if (options.length % 2 != 0) {
throw new IllegalArgumentException("Even number of argument required");
}
for (int i = 0; i < options.length; i++) {
String name = options[i];
String value = options[++i];
if (value != null) {
throw new IllegalArgumentException("Unsupported option: " + name);
}
}
}
protected static String booleanOptionForCheck(boolean value) {
return value ? "true" : null;
}
}

View file

@ -1,11 +1,5 @@
package org.keycloak.client.registration.cli.commands;
import org.jboss.aesh.cl.Arguments;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.cl.Option;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.keycloak.client.registration.cli.common.AttributeKey;
import org.keycloak.client.registration.cli.common.EndpointType;
import org.keycloak.client.registration.cli.util.ReflectionUtil;
@ -19,9 +13,13 @@ import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
import static org.keycloak.client.registration.cli.util.ReflectionUtil.getAttributeListWithJSonTypes;
@ -32,40 +30,26 @@ import static org.keycloak.client.registration.cli.util.ReflectionUtil.isMapType
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "attrs", description = "[ATTRIBUTE] [--endpoint TYPE]")
@Command(name = "attrs", description = "[ATTRIBUTE] [--endpoint TYPE]")
public class AttrsCmd extends AbstractGlobalOptionsCmd {
@Option(shortName = 'e', name = "endpoint", description = "Endpoint type to use", hasValue = true)
CommandLine.Model.CommandSpec spec;
@Option(names = {"-e", "--endpoint"}, description = "Endpoint type to use")
protected String endpoint;
@Arguments
protected List<String> args;
@Parameters(arity = "0..1")
protected String attr;
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
try {
processGlobalOptions();
if (printHelp()) {
return CommandResult.SUCCESS;
}
protected void process() {
EndpointType regType = EndpointType.DEFAULT;
PrintStream out = commandInvocation.getShell().out();
PrintStream out = System.out;
if (endpoint != null) {
regType = EndpointType.of(endpoint);
}
if (args != null) {
if (args.size() > 1) {
throw new IllegalArgumentException("Invalid option: " + args.get(1));
}
attr = args.get(0);
}
Class type = regType == EndpointType.DEFAULT ? ClientRepresentation.class : (regType == EndpointType.OIDC ? OIDCClientRepresentation.class : null);
if (type == null) {
throw new IllegalArgumentException("Endpoint not supported: " + regType);
@ -92,7 +76,7 @@ public class AttrsCmd extends AbstractGlobalOptionsCmd {
t = ((ParameterizedType) t).getActualTypeArguments()[0];
if (!isBasicType(t) && t instanceof Class) {
eol = true;
System.out.printf(", where value is:\n", ts);
out.printf(", where value is:\n", ts);
LinkedHashMap<String, String> items = ReflectionUtil.getAttributeListWithJSonTypes((Class) t, null);
for (Map.Entry<String, String> item : items.entrySet()) {
out.printf(" %-36s %s\n", item.getKey(), item.getValue());
@ -115,14 +99,9 @@ public class AttrsCmd extends AbstractGlobalOptionsCmd {
out.println();
}
}
return CommandResult.SUCCESS;
} finally {
commandInvocation.stop();
}
}
@Override
protected String help() {
return usage();
}

View file

@ -17,80 +17,36 @@
package org.keycloak.client.registration.cli.commands;
import org.jboss.aesh.cl.Arguments;
import org.jboss.aesh.cl.GroupCommandDefinition;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.Command;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.List;
import picocli.CommandLine.Command;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.EOL;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@GroupCommandDefinition(name = "config", description = "COMMAND [ARGUMENTS]", groupCommands = {ConfigCredentialsCmd.class} )
public class ConfigCmd extends AbstractAuthOptionsCmd implements Command {
@Command(name = "config", description = "COMMAND [ARGUMENTS]", subcommands = {
ConfigCredentialsCmd.class,
ConfigInitialTokenCmd.class,
ConfigRegistrationTokenCmd.class,
ConfigTruststoreCmd.class
})
public class ConfigCmd extends AbstractAuthOptionsCmd {
@Arguments
protected List<String> args;
@Override
protected void process() {
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
try {
if (args != null && args.size() > 0) {
String cmd = args.get(0);
switch (cmd) {
case "credentials": {
ConfigCredentialsCmd command = new ConfigCredentialsCmd();
command.initFromParent(this);
return command.execute(commandInvocation);
}
case "truststore": {
ConfigTruststoreCmd command = new ConfigTruststoreCmd();
command.initFromParent(this);
return command.execute(commandInvocation);
}
case "initial-token": {
ConfigInitialTokenCmd command = new ConfigInitialTokenCmd();
command.initFromParent(this);
return command.execute(commandInvocation);
}
case "registration-token": {
ConfigRegistrationTokenCmd command = new ConfigRegistrationTokenCmd();
command.initFromParent(this);
return command.execute(commandInvocation);
}
default: {
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
}
throw new IllegalArgumentException("Unknown sub-command: " + cmd + suggestHelp());
}
}
}
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
}
throw new IllegalArgumentException("Sub-command required by '" + CMD + " config' - one of: 'credentials', 'truststore', 'initial-token', 'registration-token'");
} finally {
commandInvocation.stop();
}
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help config' for more information";
@Override
protected boolean nothingToDo() {
return true;
}
@Override
protected String help() {
return usage();
}

View file

@ -1,10 +1,5 @@
package org.keycloak.client.registration.cli.commands;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.console.command.Command;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.keycloak.OAuth2Constants;
import org.keycloak.client.registration.cli.config.ConfigData;
import org.keycloak.client.registration.cli.config.RealmConfigData;
@ -16,6 +11,8 @@ import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
import picocli.CommandLine.Command;
import static org.keycloak.client.registration.cli.util.AuthUtil.getAuthTokens;
import static org.keycloak.client.registration.cli.util.AuthUtil.getAuthTokensByJWT;
import static org.keycloak.client.registration.cli.util.AuthUtil.getAuthTokensBySecret;
@ -26,15 +23,14 @@ import static org.keycloak.client.registration.cli.util.ConfigUtil.saveTokens;
import static org.keycloak.client.registration.cli.util.IoUtil.printErr;
import static org.keycloak.client.registration.cli.util.IoUtil.readSecret;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.EOL;
import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH;
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "credentials", description = "--server SERVER_URL --realm REALM [ARGUMENTS]")
public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd implements Command {
@Command(name = "credentials", description = "--server SERVER_URL --realm REALM [ARGUMENTS]")
public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd {
private int sigLifetime = 600;
@ -60,24 +56,9 @@ public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd implements Comm
}
}
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
try {
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
}
checkUnsupportedOptions("--no-config", booleanOptionForCheck(noconfig));
processGlobalOptions();
return process(commandInvocation);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
} finally {
commandInvocation.stop();
}
protected String[] getUnsupportedOptions() {
return new String[] {"--no-config", booleanOptionForCheck(noconfig)};
}
@Override
@ -85,8 +66,8 @@ public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd implements Comm
return noOptions();
}
public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
@Override
protected void process() {
// check server
if (server == null) {
throw new IllegalArgumentException("Required option not specified: --server");
@ -113,18 +94,18 @@ public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd implements Comm
// if user was set there needs to be a password so we can authenticate
if (password == null) {
password = readSecret("Enter password: ", commandInvocation);
password = readSecret("Enter password: ");
}
// if secret was set to be read from stdin, then ask for it
if ("-".equals(secret) && keystore == null) {
secret = readSecret("Enter client secret: ", commandInvocation);
secret = readSecret("Enter client secret: ");
}
} else if (keystore != null || secret != null || clientSet) {
grantTypeForAuthentication = OAuth2Constants.CLIENT_CREDENTIALS;
printErr("Logging into " + server + " as " + "service-account-" + clientId + " of realm " + realm);
if (keystore == null) {
if (secret == null) {
secret = readSecret("Enter client secret: ", commandInvocation);
secret = readSecret("Enter client secret: ");
}
}
}
@ -139,8 +120,8 @@ public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd implements Comm
}
if (storePass == null) {
storePass = readSecret("Enter keystore password: ", commandInvocation);
keyPass = readSecret("Enter key password: ", commandInvocation);
storePass = readSecret("Enter keystore password: ");
keyPass = readSecret("Enter key password: ");
}
if (keyPass == null) {
@ -163,10 +144,10 @@ public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd implements Comm
config.setServerUrl(server);
config.setRealm(realm);
});
return CommandResult.SUCCESS;
return;
}
setupTruststore(copyWithServerInfo(loadConfig()), commandInvocation);
setupTruststore(copyWithServerInfo(loadConfig()));
// now use the token endpoint to retrieve access token, and refresh token
AccessTokenResponse tokens = signedRequestToken != null ?
@ -179,14 +160,9 @@ public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd implements Comm
// save tokens to config file
saveTokens(tokens, server, realm, clientId, signedRequestToken, sigExpiresAt, secret, grantTypeForAuthentication);
return CommandResult.SUCCESS;
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help config credentials' for more information";
}
@Override
protected String help() {
return usage();
}

View file

@ -1,107 +1,45 @@
package org.keycloak.client.registration.cli.commands;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.console.command.Command;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.keycloak.client.registration.cli.config.RealmConfigData;
import org.keycloak.client.registration.cli.util.IoUtil;
import org.keycloak.client.registration.cli.util.ParseUtil;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
import static org.keycloak.client.registration.cli.util.IoUtil.warnfOut;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.EOL;
import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH;
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "initial-token", description = "[--server SERVER] --realm REALM [--delete | TOKEN] [ARGUMENTS]")
public class ConfigInitialTokenCmd extends AbstractAuthOptionsCmd implements Command {
private ConfigCmd parent;
@Command(name = "initial-token", description = "[--server SERVER] --realm REALM [--delete | TOKEN] [ARGUMENTS]")
public class ConfigInitialTokenCmd extends AbstractAuthOptionsCmd {
@Option(names = {"-d", "--delete"}, description = "Indicates that initial access token should be removed")
private boolean delete;
@Option(names = {"-k", "--keep-domain"}, description = "Don't overwrite default server and realm")
private boolean keepDomain;
protected void initFromParent(ConfigCmd parent) {
this.parent = parent;
super.initFromParent(parent);
}
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
try {
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
}
return process(commandInvocation);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
} finally {
commandInvocation.stop();
}
}
@Parameters(arity = "0..1")
private String token;
@Override
protected boolean nothingToDo() {
return noOptions() && parent.args.size() == 1;
return noOptions() && token == null && !delete && !keepDomain;
}
public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
List<String> args = new ArrayList<>();
Iterator<String> it = parent.args.iterator();
// skip the first argument 'initial-token'
it.next();
while (it.hasNext()) {
String arg = it.next();
switch (arg) {
case "-d":
case "--delete": {
delete = true;
break;
}
case "-k":
case "--keep-domain": {
keepDomain = true;
break;
}
default: {
args.add(arg);
}
}
}
if (args.size() > 1) {
throw new IllegalArgumentException("Invalid option: " + args.get(1));
}
String token = args.size() == 1 ? args.get(0) : null;
if (realm == null) {
throw new IllegalArgumentException("Realm not specified");
}
if (token != null && token.startsWith("-")) {
warnfOut(ParseUtil.TOKEN_OPTION_WARN, token);
}
checkUnsupportedOptions(
@Override
protected String[] getUnsupportedOptions() {
return new String[] {
"--client", clientId,
"--user", user,
"--password", password,
@ -112,15 +50,24 @@ public class ConfigInitialTokenCmd extends AbstractAuthOptionsCmd implements Com
"--alias", alias,
"--truststore", trustStore,
"--trustpass", keyPass,
"--no-config", booleanOptionForCheck(noconfig));
"--no-config", booleanOptionForCheck(noconfig)};
}
@Override
protected void process() {
if (realm == null) {
throw new IllegalArgumentException("Realm not specified");
}
if (token != null && token.startsWith("-")) {
warnfOut(ParseUtil.TOKEN_OPTION_WARN, token);
}
if (!delete && token == null) {
token = IoUtil.readSecret("Enter Initial Access Token: ", commandInvocation);
token = IoUtil.readSecret("Enter Initial Access Token: ");
}
// now update the config
processGlobalOptions();
String initialToken = token;
saveMergeConfig(config -> {
@ -138,14 +85,9 @@ public class ConfigInitialTokenCmd extends AbstractAuthOptionsCmd implements Com
rdata.setInitialToken(initialToken);
}
});
return CommandResult.SUCCESS;
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help config initial-token' for more information";
}
@Override
protected String help() {
return usage();
}

View file

@ -1,91 +1,40 @@
package org.keycloak.client.registration.cli.commands;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.console.command.Command;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.keycloak.client.registration.cli.config.RealmConfigData;
import org.keycloak.client.registration.cli.util.IoUtil;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.EOL;
import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH;
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "registration-token", description = "[--server SERVER] --realm REALM --client CLIENT [--delete | TOKEN] [ARGUMENTS]")
public class ConfigRegistrationTokenCmd extends AbstractAuthOptionsCmd implements Command {
private ConfigCmd parent;
@Command(name = "registration-token", description = "[--server SERVER] --realm REALM --client CLIENT [--delete | TOKEN] [ARGUMENTS]")
public class ConfigRegistrationTokenCmd extends AbstractAuthOptionsCmd {
@Option(names = {"-d", "--delete"}, description = "Indicates that initial access token should be removed")
private boolean delete;
protected void initFromParent(ConfigCmd parent) {
this.parent = parent;
super.initFromParent(parent);
}
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
try {
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
}
return process(commandInvocation);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
} finally {
commandInvocation.stop();
}
}
@Parameters(arity = "0..1")
private String token;
@Override
protected boolean nothingToDo() {
return noOptions() && parent.args.size() == 1;
return noOptions() && token == null && !delete;
}
public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
List<String> args = new ArrayList<>();
Iterator<String> it = parent.args.iterator();
// skip the first argument 'registration-token'
it.next();
while (it.hasNext()) {
String arg = it.next();
switch (arg) {
case "-d":
case "--delete": {
delete = true;
break;
}
default: {
args.add(arg);
}
}
}
if (args.size() > 1) {
throw new IllegalArgumentException("Invalid option: " + args.get(1));
}
String token = args.size() == 1 ? args.get(0) : null;
@Override
protected void process() {
if (server == null) {
throw new IllegalArgumentException("Required option not specified: --server");
}
@ -112,11 +61,10 @@ public class ConfigRegistrationTokenCmd extends AbstractAuthOptionsCmd implement
if (!delete && token == null) {
token = IoUtil.readSecret("Enter Registration Access Token: ", commandInvocation);
token = IoUtil.readSecret("Enter Registration Access Token: ");
}
// now update the config
processGlobalOptions();
String registrationToken = token;
saveMergeConfig(config -> {
@ -129,14 +77,9 @@ public class ConfigRegistrationTokenCmd extends AbstractAuthOptionsCmd implement
config.ensureRealmConfigData(server, realm).getClients().put(clientId, registrationToken);
}
});
return CommandResult.SUCCESS;
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help config registration-token' for more information";
}
@Override
protected String help() {
return usage();
}

View file

@ -16,96 +16,41 @@
*/
package org.keycloak.client.registration.cli.commands;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.console.command.Command;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
import static org.keycloak.client.registration.cli.util.IoUtil.readSecret;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.EOL;
import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH;
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "truststore", description = "PATH [ARGUMENTS]")
public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd implements Command {
private ConfigCmd parent;
@Command(name = "truststore", description = "PATH [ARGUMENTS]")
public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd {
@Option(names = {"-d", "--delete"}, description = "Indicates that initial access token should be removed")
private boolean delete;
protected void initFromParent(ConfigCmd parent) {
this.parent = parent;
super.initFromParent(parent);
}
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
try {
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
}
return process(commandInvocation);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
} finally {
commandInvocation.stop();
}
}
@Parameters(arity = "0..1")
private String truststorePath;
@Override
protected boolean nothingToDo() {
return noOptions() && parent.args.size() == 1;
return noOptions() && truststorePath == null && !delete;
}
public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
List<String> args = new ArrayList<>();
Iterator<String> it = parent.args.iterator();
// skip the first argument 'truststore'
it.next();
while (it.hasNext()) {
String arg = it.next();
switch (arg) {
case "-d":
case "--delete": {
delete = true;
break;
}
default: {
args.add(arg);
}
}
}
if (args.size() > 1) {
throw new IllegalArgumentException("Invalid option: " + args.get(1));
}
String truststore = null;
if (args.size() > 0) {
truststore = args.get(0);
}
checkUnsupportedOptions("--server", server,
@Override
protected String[] getUnsupportedOptions() {
return new String[] {"--server", server,
"--realm", realm,
"--client", clientId,
"--user", user,
@ -115,33 +60,35 @@ public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd implements Comma
"--keystore", keystore,
"--keypass", keyPass,
"--alias", alias,
"--no-config", booleanOptionForCheck(noconfig));
"--no-config", booleanOptionForCheck(noconfig)};
}
@Override
protected void process() {
// now update the config
processGlobalOptions();
String store;
String pass;
if (!delete) {
if (truststore == null) {
if (truststorePath == null) {
throw new IllegalArgumentException("No truststore specified");
}
if (!new File(truststore).isFile()) {
throw new RuntimeException("Truststore file not found: " + truststore);
if (!new File(truststorePath).isFile()) {
throw new RuntimeException("Truststore file not found: " + truststorePath);
}
if ("-".equals(trustPass)) {
trustPass = readSecret("Enter truststore password: ", commandInvocation);
trustPass = readSecret("Enter truststore password: ");
}
store = truststore;
store = truststorePath;
pass = trustPass;
} else {
if (truststore != null) {
if (truststorePath != null) {
throw new IllegalArgumentException("Option --delete is mutually exclusive with specifying a TRUSTSTORE");
}
if (trustPass != null) {
@ -155,14 +102,9 @@ public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd implements Comma
config.setTruststore(store);
config.setTrustpass(pass);
});
return CommandResult.SUCCESS;
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help config truststore' for more information";
}
@Override
protected String help() {
return usage();
}

View file

@ -17,19 +17,11 @@
package org.keycloak.client.registration.cli.commands;
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
import org.jboss.aesh.cl.Arguments;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.cl.Option;
import org.jboss.aesh.console.command.Command;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.keycloak.client.registration.cli.aesh.EndpointTypeConverter;
import org.keycloak.client.registration.cli.EndpointTypeConverter;
import org.keycloak.client.registration.cli.common.AttributeOperation;
import org.keycloak.client.registration.cli.config.ConfigData;
import org.keycloak.client.registration.cli.common.CmdStdinContext;
import org.keycloak.client.registration.cli.common.EndpointType;
import org.keycloak.client.registration.cli.config.ConfigData;
import org.keycloak.client.registration.cli.util.HttpUtil;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
@ -39,95 +31,75 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import static org.keycloak.client.registration.cli.common.AttributeOperation.Type.SET;
import static org.keycloak.client.registration.cli.common.EndpointType.DEFAULT;
import static org.keycloak.client.registration.cli.common.EndpointType.OIDC;
import static org.keycloak.client.registration.cli.common.EndpointType.SAML2;
import static org.keycloak.client.registration.cli.util.AuthUtil.ensureToken;
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
import static org.keycloak.client.registration.cli.util.ConfigUtil.credentialsAvailable;
import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig;
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
import static org.keycloak.client.registration.cli.util.ConfigUtil.setRegistrationToken;
import static org.keycloak.client.registration.cli.util.HttpUtil.doPost;
import static org.keycloak.client.registration.cli.util.HttpUtil.getExpectedContentType;
import static org.keycloak.client.registration.cli.util.IoUtil.printErr;
import static org.keycloak.client.registration.cli.util.IoUtil.printOut;
import static org.keycloak.client.registration.cli.util.IoUtil.readFully;
import static org.keycloak.client.registration.cli.util.IoUtil.readSecret;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.EOL;
import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH;
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
import static org.keycloak.client.registration.cli.util.ParseUtil.mergeAttributes;
import static org.keycloak.client.registration.cli.util.ParseUtil.parseFileOrStdin;
import static org.keycloak.client.registration.cli.util.AuthUtil.ensureToken;
import static org.keycloak.client.registration.cli.util.ConfigUtil.setRegistrationToken;
import static org.keycloak.client.registration.cli.util.HttpUtil.doPost;
import static org.keycloak.client.registration.cli.util.IoUtil.printOut;
import static org.keycloak.client.registration.cli.util.ParseUtil.parseKeyVal;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "create", description = "[ARGUMENTS]")
public class CreateCmd extends AbstractAuthOptionsCmd implements Command {
@Command(name = "create", description = "[ARGUMENTS]")
public class CreateCmd extends AbstractAuthOptionsCmd {
@Option(shortName = 'i', name = "clientId", description = "After creation only print clientId to standard output", hasValue = false)
@Option(names = {"-i", "--clientId"}, description = "After creation only print clientId to standard output")
protected boolean returnClientId = false;
@Option(shortName = 'e', name = "endpoint", description = "Endpoint type / document format to use - one of: 'default', 'oidc', 'saml2'",
hasValue = true, converter = EndpointTypeConverter.class)
@Option(names = {"-e", "--endpoint"}, description = "Endpoint type / document format to use - one of: 'default', 'oidc', 'saml2'", converter = EndpointTypeConverter.class)
protected EndpointType regType;
@Option(shortName = 'f', name = "file", description = "Read object from file or standard input if FILENAME is set to '-'", hasValue = true)
@Option(names = {"-f", "--file"}, description = "Read object from file or standard input if FILENAME is set to '-'")
protected String file;
@Option(shortName = 'o', name = "output", description = "After creation output the new client configuration to standard output", hasValue = false)
@Option(names = {"-o", "--output"}, description = "After creation output the new client configuration to standard output")
protected boolean outputClient = false;
@Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false)
@Option(names = {"-c", "--compressed"}, description = "Don't pretty print the output")
protected boolean compressed = false;
//@OptionGroup(shortName = 's', name = "set", description = "Set attribute to the specified value")
//Map<String, String> attributes = new LinkedHashMap<>();
@Option(names = {"-s", "--set"}, description = "Set a specific attribute NAME to a specified value VALUE")
List<String> rawSets = new ArrayList<>();
@Arguments
protected List<String> args;
List<AttributeOperation> attrs = new ArrayList<>();
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
protected void processOptions() {
super.processOptions();
List<AttributeOperation> attrs = new LinkedList<>();
try {
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
}
processGlobalOptions();
if (args != null) {
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 (String set : rawSets) {
String[] keyVal = parseKeyVal(set);
attrs.add(new AttributeOperation(SET, keyVal[0], keyVal[1]));
break;
}
default: {
throw new IllegalArgumentException("Unsupported option: " + option);
}
}
}
}
@Override
protected void process() {
if (file == null && attrs.size() == 0) {
throw new IllegalArgumentException("No file nor attribute values specified");
}
@ -138,7 +110,7 @@ public class CreateCmd extends AbstractAuthOptionsCmd implements Command {
// if --token is specified read it
if ("-".equals(token)) {
token = readSecret("Enter Initial Access Token: ", commandInvocation);
token = readSecret("Enter Initial Access Token: ");
}
CmdStdinContext ctx = new CmdStdinContext();
@ -167,11 +139,11 @@ public class CreateCmd extends AbstractAuthOptionsCmd implements Command {
token = config.sessionRealmConfigData().getInitialToken();
}
setupTruststore(config, commandInvocation);
setupTruststore(config);
String auth = token;
if (auth == null) {
config = ensureAuthInfo(config, commandInvocation);
config = ensureAuthInfo(config);
config = copyWithServerInfo(config);
if (credentialsAvailable(config)) {
auth = ensureToken(config);
@ -209,14 +181,6 @@ public class CreateCmd extends AbstractAuthOptionsCmd implements Command {
} catch (IOException e) {
throw new RuntimeException("Failed to process HTTP response", e);
}
return CommandResult.SUCCESS;
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
} finally {
commandInvocation.stop();
}
}
private void outputResult(String clientId, Object result) throws IOException {
@ -235,13 +199,10 @@ public class CreateCmd extends AbstractAuthOptionsCmd implements Command {
@Override
protected boolean nothingToDo() {
return noOptions() && regType == null && file == null && (args == null || args.size() == 0);
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help create' for more information";
return noOptions() && regType == null && file == null && rawSets.isEmpty();
}
@Override
protected String help() {
return usage();
}

View file

@ -17,17 +17,14 @@
package org.keycloak.client.registration.cli.commands;
import org.jboss.aesh.cl.Arguments;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.keycloak.client.registration.cli.config.ConfigData;
import org.keycloak.client.registration.cli.util.ParseUtil;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.List;
import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters;
import static org.keycloak.client.registration.cli.util.AuthUtil.ensureToken;
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
@ -39,38 +36,24 @@ import static org.keycloak.client.registration.cli.util.HttpUtil.doDelete;
import static org.keycloak.client.registration.cli.util.HttpUtil.urlencode;
import static org.keycloak.client.registration.cli.util.IoUtil.warnfErr;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.EOL;
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
/**
* @author <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 AbstractAuthOptionsCmd {
@Arguments
private List<String> args;
@Parameters(arity = "0..1")
String clientId;
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
try {
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
}
processGlobalOptions();
if (args == null || args.isEmpty()) {
protected void process() {
if (clientId == null) {
throw new IllegalArgumentException("CLIENT not specified");
}
if (args.size() > 1) {
throw new IllegalArgumentException("Invalid option: " + args.get(1));
}
String clientId = args.get(0);
if (clientId.startsWith("-")) {
warnfErr(ParseUtil.CLIENT_OPTION_WARN, clientId);
}
@ -85,11 +68,11 @@ public class DeleteCmd extends AbstractAuthOptionsCmd {
token = getRegistrationToken(config.sessionRealmConfigData(), clientId);
}
setupTruststore(config, commandInvocation);
setupTruststore(config);
String auth = token;
if (auth == null) {
config = ensureAuthInfo(config, commandInvocation);
config = ensureAuthInfo(config);
config = copyWithServerInfo(config);
if (credentialsAvailable(config)) {
auth = ensureToken(config);
@ -107,24 +90,14 @@ public class DeleteCmd extends AbstractAuthOptionsCmd {
saveMergeConfig(cfg -> {
cfg.ensureRealmConfigData(server, realm).getClients().remove(clientId);
});
return CommandResult.SUCCESS;
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
} finally {
commandInvocation.stop();
}
}
@Override
protected boolean nothingToDo() {
return noOptions() && (args == null || args.size() == 0);
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help delete' for more information";
return noOptions() && clientId == null;
}
@Override
protected String help() {
return usage();
}

View file

@ -17,12 +17,6 @@
package org.keycloak.client.registration.cli.commands;
import org.jboss.aesh.cl.Arguments;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.cl.Option;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.keycloak.client.registration.cli.config.ConfigData;
import org.keycloak.client.registration.cli.common.EndpointType;
import org.keycloak.client.registration.cli.util.ParseUtil;
@ -35,7 +29,10 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.List;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import static org.keycloak.client.registration.cli.util.AuthUtil.ensureToken;
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
@ -51,43 +48,29 @@ import static org.keycloak.client.registration.cli.util.IoUtil.warnfErr;
import static org.keycloak.client.registration.cli.util.IoUtil.printOut;
import static org.keycloak.client.registration.cli.util.IoUtil.readFully;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.EOL;
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "get", description = "[ARGUMENTS]")
@Command(name = "get", description = "[ARGUMENTS]")
public class GetCmd extends AbstractAuthOptionsCmd {
@Option(shortName = 'c', name = "compressed", description = "Print full stack trace when exiting with error", hasValue = false)
@Option(names = {"-c", "--compressed"}, description = "Print full stack trace when exiting with error")
private boolean compressed = false;
@Option(shortName = 'e', name = "endpoint", description = "Endpoint type to use", hasValue = true)
@Option(names = {"-e", "--endpoint"}, description = "Endpoint type to use")
private String endpoint;
@Arguments
private List<String> args;
@Parameters(arity = "0..1")
String clientId;
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
try {
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
}
processGlobalOptions();
if (args == null || args.isEmpty()) {
protected void process() {
if (clientId == null) {
throw new IllegalArgumentException("CLIENT not specified");
}
if (args.size() > 1) {
throw new IllegalArgumentException("Invalid option: " + args.get(1));
}
String clientId = args.get(0);
EndpointType regType = endpoint != null ? EndpointType.of(endpoint) : EndpointType.DEFAULT;
@ -103,11 +86,11 @@ public class GetCmd extends AbstractAuthOptionsCmd {
token = getRegistrationToken(config.sessionRealmConfigData(), clientId);
}
setupTruststore(config, commandInvocation);
setupTruststore(config);
String auth = token;
if (auth == null) {
config = ensureAuthInfo(config, commandInvocation);
config = ensureAuthInfo(config);
config = copyWithServerInfo(config);
if (credentialsAvailable(config)) {
auth = ensureToken(config);
@ -169,24 +152,14 @@ public class GetCmd extends AbstractAuthOptionsCmd {
} catch (IOException e) {
throw new RuntimeException("Failed to process HTTP response", e);
}
return CommandResult.SUCCESS;
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
} finally {
commandInvocation.stop();
}
}
@Override
protected boolean nothingToDo() {
return noOptions() && endpoint == null && (args == null || args.size() == 0);
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help get' for more information";
return noOptions() && endpoint == null && clientId == null;
}
@Override
protected String help() {
return usage();
}

View file

@ -1,28 +1,23 @@
package org.keycloak.client.registration.cli.commands;
import org.jboss.aesh.cl.Arguments;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.console.command.Command;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import java.util.List;
import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters;
import static org.keycloak.client.registration.cli.util.IoUtil.printOut;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "help", description = "This help")
public class HelpCmd implements Command {
@Command(name = "help", description = "This help")
public class HelpCmd implements Runnable {
@Arguments
@Parameters
private List<String> args;
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
try {
public void run() {
if (args == null || args.size() == 0) {
printOut(KcRegCmd.usage());
} else {
@ -77,14 +72,9 @@ public class HelpCmd implements Command {
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,14 +16,11 @@
*/
package org.keycloak.client.registration.cli.commands;
import org.jboss.aesh.cl.GroupCommandDefinition;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import java.io.PrintWriter;
import java.io.StringWriter;
import picocli.CommandLine.Command;
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
import static org.keycloak.client.registration.cli.util.IoUtil.printOut;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
@ -33,27 +30,30 @@ import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@GroupCommandDefinition(name = "kcreg", description = "COMMAND [ARGUMENTS]", groupCommands = {
HelpCmd.class, ConfigCmd.class, CreateCmd.class, UpdateCmd.class, GetCmd.class, DeleteCmd.class, AttrsCmd.class, UpdateTokenCmd.class} )
@Command(name = "kcreg",
header = {
"Keycloak - Open Source Identity and Access Management",
"",
"Find more information at: https://www.keycloak.org/docs/latest"
},
description = {
"%nCOMMAND [ARGUMENTS]"
},
subcommands = {
HelpCmd.class,
ConfigCmd.class,
CreateCmd.class,
GetCmd.class,
UpdateCmd.class,
DeleteCmd.class,
AttrsCmd.class,
UpdateTokenCmd.class
})
public class KcRegCmd extends AbstractGlobalOptionsCmd {
//@Arguments
//private List<String> args;
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
try {
// if --help was requested then status is SUCCESS
// if not we print help anyway, but status is FAILURE
if (printHelp()) {
return CommandResult.SUCCESS;
} else {
printOut(usage());
return CommandResult.FAILURE;
}
} finally {
commandInvocation.stop();
}
protected boolean nothingToDo() {
return true;
}
public static String usage() {

View file

@ -18,13 +18,13 @@
package org.keycloak.client.registration.cli.commands;
import com.fasterxml.jackson.core.JsonParseException;
import org.jboss.aesh.cl.Arguments;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.cl.Option;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.keycloak.client.registration.cli.aesh.EndpointTypeConverter;
import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import org.keycloak.client.registration.cli.EndpointTypeConverter;
import org.keycloak.client.registration.cli.common.AttributeOperation;
import org.keycloak.client.registration.cli.config.ConfigData;
import org.keycloak.client.registration.cli.common.CmdStdinContext;
@ -39,7 +39,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Iterator;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
@ -62,7 +62,6 @@ import static org.keycloak.client.registration.cli.util.IoUtil.warnfErr;
import static org.keycloak.client.registration.cli.util.IoUtil.readFully;
import static org.keycloak.client.registration.cli.util.HttpUtil.APPLICATION_JSON;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.EOL;
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
import static org.keycloak.client.registration.cli.util.ParseUtil.mergeAttributes;
import static org.keycloak.client.registration.cli.util.ParseUtil.parseFileOrStdin;
@ -71,81 +70,62 @@ import static org.keycloak.client.registration.cli.util.ParseUtil.parseKeyVal;
/**
* @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 AbstractAuthOptionsCmd {
@Option(shortName = 'e', name = "endpoint", description = "Endpoint type to use - one of: 'default', 'oidc'", hasValue = true, converter = EndpointTypeConverter.class)
@Option(names = {"-e", "--endpoint"}, description = "Endpoint type to use - one of: 'default', 'oidc'", converter = EndpointTypeConverter.class)
private EndpointType regType = null;
//@GroupOption(shortName = 's', name = "set", description = "Set specific attribute to a specified value", hasValue = true)
//private List<String> attributes = new ArrayList<>();
@Option(shortName = 'f', name = "file", description = "Use the file or standard input if '-' is specified", hasValue = true)
@Option(names = {"-f", "--file"}, description = "Use the file or standard input if '-' is specified")
private String file = null;
@Option(shortName = 'm', name = "merge", description = "Merge new values with existing configuration on the server", hasValue = false)
private boolean mergeMode = true;
@Option(names = {"-m", "--merge"}, description = "Merge new values with existing configuration on the server")
private boolean mergeMode = false;
@Option(shortName = 'o', name = "output", description = "After update output the new client configuration", hasValue = false)
@Option(names = {"-o", "--output"}, description = "After update output the new client configuration")
private boolean outputClient = false;
@Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false)
@Option(names = {"-c", "--compressed"}, description = "Don't pretty print the output")
private boolean compressed = false;
@Arguments
private List<String> args;
@Parameters(arity = "0..1")
String clientId;
// to maintain relative positions of set and delete operations
static class AttributeOperations {
@Option(names = {"-s", "--set"}, required = true) String set;
@Option(names = {"-d", "--delete"}, required = true) String delete;
}
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
@ArgGroup(exclusive = true, multiplicity = "0..*")
List<AttributeOperations> rawAttributeOperations = new ArrayList<>();
List<AttributeOperation> attrs = new LinkedList<>();
try {
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
@Override
protected void processOptions() {
super.processOptions();
for (AttributeOperations entry : rawAttributeOperations) {
if (entry.delete != null) {
attrs.add(new AttributeOperation(DELETE, entry.delete));
} else {
String[] keyVal = parseKeyVal(entry.set);
attrs.add(new AttributeOperation(SET, keyVal[0], keyVal[1]));
}
}
}
processGlobalOptions();
String clientId = null;
if (args != null) {
Iterator<String> it = args.iterator();
if (!it.hasNext()) {
@Override
protected void process() {
if (clientId == null) {
throw new IllegalArgumentException("CLIENT_ID not specified");
}
clientId = it.next();
if (clientId.startsWith("-")) {
warnfErr(ParseUtil.CLIENT_OPTION_WARN, clientId);
}
while (it.hasNext()) {
String option = it.next();
switch (option) {
case "-s":
case "--set": {
if (!it.hasNext()) {
throw new IllegalArgumentException("Option " + option + " requires a value");
}
String[] keyVal = parseKeyVal(it.next());
attrs.add(new AttributeOperation(SET, keyVal[0], keyVal[1]));
break;
}
case "-d":
case "--delete": {
attrs.add(new AttributeOperation(DELETE, it.next()));
break;
}
default: {
throw new IllegalArgumentException("Unsupported option: " + option);
}
}
}
}
if (file == null && attrs.size() == 0) {
throw new IllegalArgumentException("No file nor attribute values specified");
}
@ -229,11 +209,11 @@ public class UpdateCmd extends AbstractAuthOptionsCmd {
token = getRegistrationToken(config.sessionRealmConfigData(), clientId);
}
setupTruststore(config, commandInvocation);
setupTruststore(config);
String auth = token;
if (auth == null) {
config = ensureAuthInfo(config, commandInvocation);
config = ensureAuthInfo(config);
config = copyWithServerInfo(config);
if (credentialsAvailable(config)) {
auth = ensureToken(config);
@ -316,14 +296,6 @@ public class UpdateCmd extends AbstractAuthOptionsCmd {
} catch (IOException e) {
throw new RuntimeException("Failed to process HTTP response", e);
}
return CommandResult.SUCCESS;
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
} finally {
commandInvocation.stop();
}
}
private void outputResult(Object result) throws IOException {
@ -338,13 +310,10 @@ public class UpdateCmd extends AbstractAuthOptionsCmd {
@Override
protected boolean nothingToDo() {
return noOptions() && regType == null && file == null && (args == null || args.size() == 0);
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help update' for more information";
return noOptions() && regType == null && file == null && rawAttributeOperations.isEmpty() && clientId == null;
}
@Override
protected String help() {
return usage();
}

View file

@ -18,11 +18,10 @@
package org.keycloak.client.registration.cli.commands;
import com.fasterxml.jackson.core.type.TypeReference;
import org.jboss.aesh.cl.Arguments;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters;
import org.keycloak.client.registration.cli.config.ConfigData;
import org.keycloak.client.registration.cli.util.ParseUtil;
import org.keycloak.representations.idm.ClientRepresentation;
@ -45,43 +44,32 @@ import static org.keycloak.client.registration.cli.util.HttpUtil.doPost;
import static org.keycloak.client.registration.cli.util.IoUtil.printOut;
import static org.keycloak.client.registration.cli.util.IoUtil.warnfOut;
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
import static org.keycloak.client.registration.cli.util.OsUtil.EOL;
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "update-token", description = "CLIENT [ARGUMENTS]")
@Command(name = "update-token", description = "CLIENT [ARGUMENTS]")
public class UpdateTokenCmd extends AbstractAuthOptionsCmd {
@Arguments
private List<String> args;
@Parameters(arity = "0..1")
String clientId;
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
try {
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
}
processGlobalOptions();
if (args == null || args.isEmpty()) {
protected void process() {
if (clientId == null) {
throw new IllegalArgumentException("CLIENT not specified");
}
String clientId = args.get(0);
if (clientId.startsWith("-")) {
warnfOut(ParseUtil.CLIENT_OPTION_WARN, clientId);
}
ConfigData config = loadConfig();
config = copyWithServerInfo(config);
setupTruststore(config, commandInvocation);
setupTruststore(config);
config = ensureAuthInfo(config, commandInvocation);
config = ensureAuthInfo(config);
String auth = ensureToken(config);
String cid = null;
@ -124,26 +112,14 @@ public class UpdateTokenCmd extends AbstractAuthOptionsCmd {
} catch (IOException e) {
throw new RuntimeException("Failed to process response from server", e);
}
//System.out.println("Token updated for client " + clientId);
return CommandResult.SUCCESS;
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
} finally {
commandInvocation.stop();
}
}
@Override
protected boolean nothingToDo() {
return noOptions() && (args == null || args.size() == 0);
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help update-token' for more information";
return noOptions() && clientId == null;
}
@Override
protected String help() {
return usage();
}

View file

@ -17,14 +17,7 @@
package org.keycloak.client.registration.cli.util;
import org.jboss.aesh.console.AeshConsoleBufferBuilder;
import org.jboss.aesh.console.AeshInputProcessorBuilder;
import org.jboss.aesh.console.ConsoleBuffer;
import org.jboss.aesh.console.InputProcessor;
import org.jboss.aesh.console.Prompt;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.keycloak.client.registration.cli.aesh.Globals;
import java.io.Console;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
@ -50,7 +43,6 @@ import static java.nio.file.Files.createDirectories;
import static java.nio.file.Files.createFile;
import static java.nio.file.Files.isDirectory;
import static java.nio.file.Files.isRegularFile;
import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -81,43 +73,14 @@ public class IoUtil {
}
}
public static String readSecret(String prompt, CommandInvocation invocation) {
// TODO Windows hack - masking not working on Windows
char maskChar = OS_ARCH.isWindows() ? 0 : '*';
ConsoleBuffer consoleBuffer = new AeshConsoleBufferBuilder()
.shell(invocation.getShell())
.prompt(new Prompt(prompt, maskChar))
.create();
InputProcessor inputProcessor = new AeshInputProcessorBuilder()
.consoleBuffer(consoleBuffer)
.create();
consoleBuffer.displayPrompt();
// activate stdin
Globals.stdin.setInputStream(System.in);
String result;
try {
do {
result = inputProcessor.parseOperation(invocation.getInput());
} while (result == null);
} catch (Exception e) {
throw new RuntimeException("^C", e);
public static String readSecret(String prompt) {
Console cons;
char[] passwd;
if ((cons = System.console()) != null &&
(passwd = cons.readPassword("%s", prompt)) != null) {
return new String(passwd);
}
/*
if (!Globals.stdin.isStdinAvailable()) {
try {
return readLine(new InputStreamReader(System.in));
} catch (IOException e) {
throw new RuntimeException("Standard input not available");
}
}
*/
// Windows hack - get rid of any \n
result = result.replaceAll("\\n", "");
return result;
throw new RuntimeException("Console is not active, or no password provided");
}
public static String readFully(InputStream is) {

View file

@ -80,7 +80,6 @@
<jboss.as.subsystem.test.version>7.5.22.Final-redhat-1</jboss.as.subsystem.test.version>
<!-- Versions used mostly for Undertow server, aligned with WildFly -->
<jboss.aesh.version>0.66.19</jboss.aesh.version>
<apache.httpcomponents.version>4.5.14</apache.httpcomponents.version>
<jboss.dmr.version>1.5.1.Final</jboss.dmr.version>
@ -948,11 +947,6 @@
<artifactId>pax-web-spi</artifactId>
<version>${pax.web.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.aesh</groupId>
<artifactId>aesh</artifactId>
<version>${jboss.aesh.version}</version>
</dependency>
<!-- keycloak -->
<dependency>

View file

@ -20,27 +20,27 @@ public class KcRegConfigTest extends AbstractRegCliTest {
@Test
public void testRegistrationToken() throws IOException {
FileConfigHandler handler = initCustomConfigFile();
initCustomConfigFile();
try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
try (TempFileResource configFile = new TempFileResource(FileConfigHandler.getConfigFile())) {
// without --server
KcRegExec exe = execute("config registration-token --config '" + configFile.getName() + "' ");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
assertExitCodeAndStreamSizes(exe, 2, 0, 2);
Assert.assertEquals("error message", "Required option not specified: --server", exe.stderrLines().get(0));
Assert.assertEquals("try help", "Try '" + CMD + " help config registration-token' for more information", exe.stderrLines().get(1));
Assert.assertEquals("try help", "Try '" + CMD + " config registration-token --help' for more information on the available options.", exe.stderrLines().get(1));
// without --realm
exe = execute("config registration-token --config '" + configFile.getName() + "' --server http://localhost:8080/auth");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
assertExitCodeAndStreamSizes(exe, 2, 0, 2);
Assert.assertEquals("error message", "Required option not specified: --realm", exe.stderrLines().get(0));
Assert.assertEquals("try help", "Try '" + CMD + " help config registration-token' for more information", exe.stderrLines().get(1));
Assert.assertEquals("try help", "Try '" + CMD + " config registration-token --help' for more information on the available options.", exe.stderrLines().get(1));
// without --client
exe = execute("config registration-token --config '" + configFile.getName() + "' --server http://localhost:8080/auth --realm test");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
assertExitCodeAndStreamSizes(exe, 2, 0, 2);
Assert.assertEquals("error message", "Required option not specified: --client", exe.stderrLines().get(0));
Assert.assertEquals("try help", "Try '" + CMD + " help config registration-token' for more information", exe.stderrLines().get(1));
Assert.assertEquals("try help", "Try '" + CMD + " config registration-token --help' for more information on the available options.", exe.stderrLines().get(1));
// specify token on cmdline
exe = execute("config registration-token --config '" + configFile.getName() + "' --server http://localhost:8080/auth --realm test --client my_client NEWTOKEN");
@ -75,14 +75,14 @@ public class KcRegConfigTest extends AbstractRegCliTest {
public void testNoConfigOption() throws IOException {
KcRegExec exe = execute("config registration-token --no-config --server http://localhost:8080/auth --realm test --client my_client --delete");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
assertExitCodeAndStreamSizes(exe, 2, 0, 2);
Assert.assertEquals("stderr first line", "Unsupported option: --no-config", exe.stderrLines().get(0));
Assert.assertEquals("try help", "Try '" + CMD + " help config registration-token' for more information", exe.stderrLines().get(1));
Assert.assertEquals("try help", "Try '" + CMD + " config registration-token --help' for more information on the available options.", exe.stderrLines().get(1));
exe = execute("config initial-token --no-config --server http://localhost:8080/auth --realm test --delete");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
assertExitCodeAndStreamSizes(exe, 2, 0, 2);
Assert.assertEquals("stderr first line", "Unsupported option: --no-config", exe.stderrLines().get(0));
Assert.assertEquals("try help", "Try '" + CMD + " help config initial-token' for more information", exe.stderrLines().get(1));
Assert.assertEquals("try help", "Try '" + CMD + " config initial-token --help' for more information on the available options.", exe.stderrLines().get(1));
}
}

View file

@ -36,7 +36,7 @@ public class KcRegTest extends AbstractRegCliTest {
*/
KcRegExec exe = execute("");
assertExitCodeAndStdErrSize(exe, 1, 0);
assertExitCodeAndStdErrSize(exe, 2, 0);
List<String> lines = exe.stdoutLines();
Assert.assertTrue("stdout output not empty", lines.size() > 0);
@ -49,51 +49,51 @@ public class KcRegTest extends AbstractRegCliTest {
* Test commands without arguments
*/
exe = execute("config");
assertExitCodeAndStreamSizes(exe, 1, 0, 1);
assertExitCodeAndStreamSizes(exe, 2, 8, 0);
Assert.assertEquals("error message",
"Sub-command required by '" + CMD + " config' - one of: 'credentials', 'truststore', 'initial-token', 'registration-token'",
exe.stderrLines().get(0));
"Usage: kcreg.sh config SUB_COMMAND [ARGUMENTS]",
exe.stdoutLines().get(0));
exe = execute("config credentials");
assertExitCodeAndStdErrSize(exe, 1, 0);
assertExitCodeAndStdErrSize(exe, 2, 0);
Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
Assert.assertEquals("help message", "Usage: " + CMD + " config credentials --server SERVER_URL --realm REALM [ARGUMENTS]", exe.stdoutLines().get(0));
exe = execute("config initial-token");
assertExitCodeAndStdErrSize(exe, 1, 0);
assertExitCodeAndStdErrSize(exe, 2, 0);
Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
Assert.assertEquals("help message", "Usage: " + CMD + " config initial-token --server SERVER --realm REALM [--delete | TOKEN] [ARGUMENTS]", exe.stdoutLines().get(0));
exe = execute("config registration-token");
assertExitCodeAndStdErrSize(exe, 1, 0);
assertExitCodeAndStdErrSize(exe, 2, 0);
Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
Assert.assertEquals("help message", "Usage: " + CMD + " config registration-token --server SERVER --realm REALM --client CLIENT [--delete | TOKEN] [ARGUMENTS]", exe.stdoutLines().get(0));
exe = execute("config truststore");
assertExitCodeAndStdErrSize(exe, 1, 0);
assertExitCodeAndStdErrSize(exe, 2, 0);
Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
Assert.assertEquals("help message", "Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWORD] [ARGUMENTS]", exe.stdoutLines().get(0));
exe = execute("create");
assertExitCodeAndStdErrSize(exe, 1, 0);
assertExitCodeAndStdErrSize(exe, 2, 0);
Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
Assert.assertEquals("help message", "Usage: " + CMD + " create [ARGUMENTS]", exe.stdoutLines().get(0));
//Assert.assertEquals("error message", "No file nor attribute values specified", exe.stderrLines().get(0));
exe = execute("get");
assertExitCodeAndStdErrSize(exe, 1, 0);
assertExitCodeAndStdErrSize(exe, 2, 0);
Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
Assert.assertEquals("help message", "Usage: " + CMD + " get CLIENT [ARGUMENTS]", exe.stdoutLines().get(0));
//Assert.assertEquals("error message", "CLIENT not specified", exe.stderrLines().get(0));
exe = execute("update");
assertExitCodeAndStdErrSize(exe, 1, 0);
assertExitCodeAndStdErrSize(exe, 2, 0);
Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
Assert.assertEquals("help message", "Usage: " + CMD + " update CLIENT [ARGUMENTS]", exe.stdoutLines().get(0));
//Assert.assertEquals("error message", "No file nor attribute values specified", exe.stderrLines().get(0));
exe = execute("delete");
assertExitCodeAndStdErrSize(exe, 1, 0);
assertExitCodeAndStdErrSize(exe, 2, 0);
Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
Assert.assertEquals("help message", "Usage: " + CMD + " delete CLIENT [ARGUMENTS]", exe.stdoutLines().get(0));
//Assert.assertEquals("error message", "CLIENT not specified", exe.stderrLines().get(0));
@ -104,7 +104,7 @@ public class KcRegTest extends AbstractRegCliTest {
Assert.assertEquals("first line", "Attributes for default format:", exe.stdoutLines().get(0));
exe = execute("update-token");
assertExitCodeAndStdErrSize(exe, 1, 0);
assertExitCodeAndStdErrSize(exe, 2, 0);
Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
Assert.assertEquals("help message", "Usage: " + CMD + " update-token CLIENT [ARGUMENTS]", exe.stdoutLines().get(0));
//Assert.assertEquals("error message", "CLIENT not specified", exe.stderrLines().get(0));
@ -188,8 +188,8 @@ public class KcRegTest extends AbstractRegCliTest {
*/
KcRegExec exe = execute("nonexistent");
assertExitCodeAndStreamSizes(exe, 1, 0, 1);
Assert.assertEquals("stderr first line", "Unknown command: nonexistent", exe.stderrLines().get(0));
assertExitCodeAndStreamSizes(exe, 2, 0, 3);
Assert.assertEquals("stderr first line", "Unmatched argument at index 0: 'nonexistent'", exe.stderrLines().get(0));
}
@Test
@ -199,8 +199,8 @@ public class KcRegTest extends AbstractRegCliTest {
*/
KcRegExec exe = execute("--nonexistent");
assertExitCodeAndStreamSizes(exe, 1, 0, 1);
Assert.assertEquals("stderr first line", "Unknown command: --nonexistent", exe.stderrLines().get(0));
assertExitCodeAndStreamSizes(exe, 2, 0, 2);
Assert.assertEquals("stderr first line", "Unknown option: '--nonexistent'", exe.stderrLines().get(0));
}
@Test
@ -211,9 +211,9 @@ public class KcRegTest extends AbstractRegCliTest {
KcRegExec exe = execute("get my_client --nonexistent");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
Assert.assertEquals("stderr first line", "Invalid option: --nonexistent", exe.stderrLines().get(0));
Assert.assertEquals("try help", "Try '" + CMD + " help get' for more information", exe.stderrLines().get(1));
assertExitCodeAndStreamSizes(exe, 2, 0, 3);
Assert.assertEquals("stderr first line", "Unknown option: '--nonexistent'", exe.stderrLines().get(0));
Assert.assertEquals("try help", "Try '" + CMD + " get --help' for more information on the available options.", exe.stderrLines().get(2));
}
@Test
@ -233,9 +233,9 @@ public class KcRegTest extends AbstractRegCliTest {
*/
KcRegExec exe = execute("config credentials --realm master --user admin --password admin");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
assertExitCodeAndStreamSizes(exe, 2, 0, 2);
Assert.assertEquals("stderr first line", "Required option not specified: --server", exe.stderrLines().get(0));
Assert.assertEquals("try help", "Try '" + CMD + " help config credentials' for more information", exe.stderrLines().get(1));
Assert.assertEquals("try help", "Try '" + CMD + " config credentials --help' for more information on the available options.", exe.stderrLines().get(1));
}
@Test
@ -245,9 +245,9 @@ public class KcRegTest extends AbstractRegCliTest {
*/
KcRegExec exe = execute("config credentials --server " + serverUrl + " --user admin --password admin");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
assertExitCodeAndStreamSizes(exe, 2, 0, 2);
Assert.assertEquals("stderr first line", "Required option not specified: --realm", exe.stderrLines().get(0));
Assert.assertEquals("try help", "Try '" + CMD + " help config credentials' for more information", exe.stderrLines().get(1));
Assert.assertEquals("try help", "Try '" + CMD + " config credentials --help' for more information on the available options.", exe.stderrLines().get(1));
}
@Test
@ -257,9 +257,9 @@ public class KcRegTest extends AbstractRegCliTest {
*/
KcRegExec exe = KcRegExec.execute("config credentials --no-config --server " + serverUrl + " --realm master --user admin --password admin");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
assertExitCodeAndStreamSizes(exe, 2, 0, 2);
Assert.assertEquals("stderr first line", "Unsupported option: --no-config", exe.stderrLines().get(0));
Assert.assertEquals("try help", "Try '" + CMD + " help config credentials' for more information", exe.stderrLines().get(1));
Assert.assertEquals("try help", "Try '" + CMD + " config credentials --help' for more information on the available options.", exe.stderrLines().get(1));
}
@Test

View file

@ -6,6 +6,7 @@ import org.keycloak.client.registration.cli.config.ConfigData;
import org.keycloak.client.registration.cli.config.FileConfigHandler;
import org.keycloak.client.registration.cli.util.OsUtil;
import org.keycloak.testsuite.cli.KcRegExec;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.TempFileResource;
import java.io.File;
@ -29,9 +30,9 @@ public class KcRegTruststoreTest extends AbstractRegCliTest {
KcRegExec exe = execute("config truststore --no-config '" + truststore.getAbsolutePath() + "'");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
assertExitCodeAndStreamSizes(exe, 2, 0, 2);
Assert.assertEquals("stderr first line", "Unsupported option: --no-config", exe.stderrLines().get(0));
Assert.assertEquals("try help", "Try '" + OsUtil.CMD + " help config truststore' for more information", exe.stderrLines().get(1));
Assert.assertEquals("try help", "Try '" + OsUtil.CMD + " config truststore --help' for more information on the available options.", exe.stderrLines().get(1));
// only run the rest of this test if ssl protected keycloak server is available
if (!AUTH_SERVER_SSL_REQUIRED) {
@ -39,9 +40,9 @@ public class KcRegTruststoreTest extends AbstractRegCliTest {
return;
}
FileConfigHandler handler = initCustomConfigFile();
initCustomConfigFile();
try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
try (TempFileResource configFile = new TempFileResource(FileConfigHandler.getConfigFile())) {
if (runIntermittentlyFailingTests()) {
// configure truststore
@ -52,7 +53,7 @@ public class KcRegTruststoreTest extends AbstractRegCliTest {
// perform authentication against server - asks for password, then for truststore password
exe = KcRegExec.newBuilder()
.argsLine("config credentials --server " + oauth.AUTH_SERVER_ROOT + " --realm test --user user1" +
.argsLine("config credentials --server " + OAuthClient.AUTH_SERVER_ROOT + " --realm test --user user1" +
" --config '" + configFile.getName() + "'")
.executeAsync();
@ -72,7 +73,7 @@ public class KcRegTruststoreTest extends AbstractRegCliTest {
// perform authentication against server - asks for password, then for truststore password
exe = KcRegExec.newBuilder()
.argsLine("config credentials --server " + oauth.AUTH_SERVER_ROOT + " --realm test --user user1" +
.argsLine("config credentials --server " + OAuthClient.AUTH_SERVER_ROOT + " --realm test --user user1" +
" --config '" + configFile.getName() + "'")
.executeAsync();
@ -99,17 +100,17 @@ public class KcRegTruststoreTest extends AbstractRegCliTest {
assertExitCodeAndStreamSizes(exe, 0, 0, 0);
exe = execute("config truststore --delete '" + truststore.getAbsolutePath() + "'");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
assertExitCodeAndStreamSizes(exe, 2, 0, 2);
Assert.assertEquals("incompatible", "Option --delete is mutually exclusive with specifying a TRUSTSTORE", exe.stderrLines().get(0));
Assert.assertEquals("try help", "Try '" + CMD + " help config truststore' for more information", exe.stderrLines().get(1));
Assert.assertEquals("try help", "Try '" + CMD + " config truststore --help' for more information on the available options.", exe.stderrLines().get(1));
exe = execute("config truststore --delete --trustpass secret");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
assertExitCodeAndStreamSizes(exe, 2, 0, 2);
Assert.assertEquals("no truststore error", "Options --trustpass and --delete are mutually exclusive", exe.stderrLines().get(0));
Assert.assertEquals("try help", "Try '" + CMD + " help config truststore' for more information", exe.stderrLines().get(1));
Assert.assertEquals("try help", "Try '" + CMD + " config truststore --help' for more information on the available options.", exe.stderrLines().get(1));
FileConfigHandler cfghandler = new FileConfigHandler();
cfghandler.setConfigFile(DEFAULT_CONFIG_FILE_PATH);
FileConfigHandler.setConfigFile(DEFAULT_CONFIG_FILE_PATH);
ConfigData config = cfghandler.loadConfig();
Assert.assertNull("truststore null", config.getTruststore());
Assert.assertNull("trustpass null", config.getTrustpass());

View file

@ -26,7 +26,7 @@ public class KcRegUpdateTest extends AbstractRegCliTest {
FileConfigHandler handler = initCustomConfigFile();
try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
try (TempFileResource configFile = new TempFileResource(FileConfigHandler.getConfigFile())) {
final String realm = "test";
@ -91,9 +91,9 @@ public class KcRegUpdateTest extends AbstractRegCliTest {
// check that using an invalid attribute key is not ignored
exe = execute("update my_client --nonexisting --config '" + configFile.getName() + "'");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
Assert.assertEquals("error message", "Unsupported option: --nonexisting", exe.stderrLines().get(0));
Assert.assertEquals("try help", "Try '" + CMD + " help update' for more information", exe.stderrLines().get(1));
assertExitCodeAndStreamSizes(exe, 2, 0, 3);
Assert.assertEquals("error message", "Unknown option: '--nonexisting'", exe.stderrLines().get(0));
Assert.assertEquals("try help", "Try '" + CMD + " update --help' for more information on the available options.", exe.stderrLines().get(2));
// try use incompatible endpoint