fix: replace aesh with picocli (#27458)

* fix: replace aesh with picocli

closes: #27388

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

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

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

* splitting the error handling for password input

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

* adding a change note about kcadm

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

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

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

---------

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

View File

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

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.client.admin.cli;
import picocli.CommandLine;
import picocli.CommandLine.ParseResult;
public final class ExecutionExceptionHandler implements CommandLine.IExecutionExceptionHandler {
@Override
public int handleExecutionException(Exception cause, CommandLine cmd, ParseResult parseResult) {
int exitCode = ShortErrorMessageHandler.shortErrorMessage(cause, cmd);
if (Globals.dumpTrace) {
cause.printStackTrace();
}
return exitCode;
}
}

View File

@@ -14,9 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.client.admin.cli.aesh;
import java.util.List;
package org.keycloak.client.admin.cli;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@@ -25,7 +23,6 @@ public class Globals {
public static boolean dumpTrace = false;
public static ValveInputStream stdin;
public static boolean help = false;
public static List<String> args;
}

View File

@@ -16,22 +16,15 @@
*/
package org.keycloak.client.admin.cli;
import org.jboss.aesh.console.AeshConsoleBuilder;
import org.jboss.aesh.console.AeshConsoleImpl;
import org.jboss.aesh.console.Prompt;
import org.jboss.aesh.console.command.registry.AeshCommandRegistryBuilder;
import org.jboss.aesh.console.command.registry.CommandRegistry;
import org.jboss.aesh.console.settings.Settings;
import org.jboss.aesh.console.settings.SettingsBuilder;
import org.keycloak.client.admin.cli.aesh.AeshEnhancer;
import org.keycloak.client.admin.cli.aesh.Globals;
import org.keycloak.client.admin.cli.aesh.ValveInputStream;
import org.keycloak.client.admin.cli.commands.KcAdmCmd;
import org.keycloak.client.admin.cli.util.ClassLoaderUtil;
import org.keycloak.client.admin.cli.util.OsUtil;
import org.keycloak.common.crypto.CryptoIntegration;
import java.util.ArrayList;
import java.util.Arrays;
import java.io.PrintWriter;
import picocli.CommandLine;
import picocli.CommandLine.Model.CommandSpec;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@@ -47,53 +40,22 @@ public class KcAdmMain {
Thread.currentThread().setContextClassLoader(cl);
CryptoIntegration.init(cl);
Globals.stdin = new ValveInputStream();
Settings settings = new SettingsBuilder()
.logging(false)
.readInputrc(false)
.disableCompletion(true)
.disableHistory(true)
.enableAlias(false)
.enableExport(false)
.inputStream(Globals.stdin)
.create();
CommandRegistry registry = new AeshCommandRegistryBuilder()
.command(KcAdmCmd.class)
.create();
AeshConsoleImpl console = (AeshConsoleImpl) new AeshConsoleBuilder()
.settings(settings)
.commandRegistry(registry)
.prompt(new Prompt(""))
// .commandInvocationProvider(new CommandInvocationServices() {
//
// })
.create();
AeshEnhancer.enhance(console);
// work around parser issues with quotes and brackets
ArrayList<String> arguments = new ArrayList<>();
arguments.add("kcadm");
arguments.addAll(Arrays.asList(args));
Globals.args = arguments;
StringBuilder b = new StringBuilder();
for (String s : args) {
// quote if necessary
b.append(' ');
s = s.replace("'", "\\'");
b.append('\'');
b.append(s);
b.append('\'');
}
console.setEcho(false);
console.execute("kcadm" + b.toString());
console.start();
}
CommandLine cli = createCommandLine();
int exitCode = cli.execute(args);
System.exit(exitCode);
}
public static CommandLine createCommandLine() {
CommandSpec spec = CommandSpec.forAnnotatedObject(new KcAdmCmd()).name(OsUtil.CMD);
CommandLine cmd = new CommandLine(spec);
cmd.setExecutionExceptionHandler(new ExecutionExceptionHandler());
cmd.setParameterExceptionHandler(new ShortErrorMessageHandler());
cmd.setErr(new PrintWriter(System.err, true));
return cmd;
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.client.admin.cli;
import java.io.PrintWriter;
import picocli.CommandLine;
import picocli.CommandLine.IParameterExceptionHandler;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.ParameterException;
import picocli.CommandLine.UnmatchedArgumentException;
public class ShortErrorMessageHandler implements IParameterExceptionHandler {
@Override
public int handleParseException(ParameterException ex, String[] args) {
CommandLine cmd = ex.getCommandLine();
return shortErrorMessage(ex, cmd);
}
static int shortErrorMessage(Exception ex, CommandLine cmd) {
PrintWriter writer = cmd.getErr();
String errorMessage = ex.getMessage();
writer.println(cmd.getColorScheme().errorText(errorMessage));
if (ex instanceof ParameterException) {
UnmatchedArgumentException.printSuggestions((ParameterException)ex, writer);
}
if (ex instanceof ParameterException || ex instanceof IllegalArgumentException) {
CommandSpec spec = cmd.getCommandSpec();
writer.printf("Try '%s%s' for more information on the available options.%n", spec.qualifiedName(), "help".equals(spec.name())?"":" --help");
return cmd.getCommandSpec().exitCodeOnInvalidInput();
}
return cmd.getCommandSpec().exitCodeOnExecutionException();
}
}

View File

@@ -1,118 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.client.admin.cli.aesh;
import org.jboss.aesh.cl.parser.OptionParserException;
import org.jboss.aesh.cl.result.ResultHandler;
import org.jboss.aesh.console.AeshConsoleCallback;
import org.jboss.aesh.console.AeshConsoleImpl;
import org.jboss.aesh.console.ConsoleOperation;
import org.jboss.aesh.console.command.CommandNotFoundException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.container.CommandContainer;
import org.jboss.aesh.console.command.container.CommandContainerResult;
import org.jboss.aesh.console.command.invocation.AeshCommandInvocation;
import org.jboss.aesh.console.command.invocation.AeshCommandInvocationProvider;
import org.jboss.aesh.parser.AeshLine;
import org.jboss.aesh.parser.ParserStatus;
import java.lang.reflect.Method;
class AeshConsoleCallbackImpl extends AeshConsoleCallback {
private final AeshConsoleImpl console;
private CommandResult result;
AeshConsoleCallbackImpl(AeshConsoleImpl aeshConsole) {
this.console = aeshConsole;
}
@Override
@SuppressWarnings("unchecked")
public int execute(ConsoleOperation output) throws InterruptedException {
if (output != null && output.getBuffer().trim().length() > 0) {
ResultHandler resultHandler = null;
//AeshLine aeshLine = Parser.findAllWords(output.getBuffer());
AeshLine aeshLine = new AeshLine(output.getBuffer(), Globals.args, ParserStatus.OK, "");
try (CommandContainer commandContainer = getCommand(output, aeshLine)) {
resultHandler = commandContainer.getParser().getProcessedCommand().getResultHandler();
CommandContainerResult ccResult =
commandContainer.executeCommand(aeshLine, console.getInvocationProviders(), console.getAeshContext(),
new AeshCommandInvocationProvider().enhanceCommandInvocation(
new AeshCommandInvocation(console,
output.getControlOperator(), output.getPid(), this)));
result = ccResult.getCommandResult();
if(result == CommandResult.SUCCESS && resultHandler != null)
resultHandler.onSuccess();
else if(resultHandler != null)
resultHandler.onFailure(result);
if (result == CommandResult.FAILURE) {
// we assume the command has already output any error messages
System.exit(1);
}
} catch (Exception e) {
console.stop();
if (e instanceof OptionParserException && "Option: - must be followed by a valid operator".equals(e.getMessage())) {
System.err.println("Please double check your command options, one or more of them are not specified correctly. "
+ "It is possible to have unintentional overlap with other options. e.g. using --clientid will get mistaken for --client, however --cclientid is needed.");
} else {
System.err.println(e.getMessage());
}
if (Globals.dumpTrace) {
e.printStackTrace();
}
System.exit(1);
}
}
// empty line
else if (output != null) {
result = CommandResult.FAILURE;
}
else {
//stop();
result = CommandResult.FAILURE;
}
if (result == CommandResult.SUCCESS) {
return 0;
} else {
return 1;
}
}
private CommandContainer getCommand(ConsoleOperation output, AeshLine aeshLine) throws CommandNotFoundException {
Method m;
try {
m = console.getClass().getDeclaredMethod("getCommand", AeshLine.class, String.class);
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unexpected error: ", e);
}
m.setAccessible(true);
try {
return (CommandContainer) m.invoke(console, aeshLine, output.getBuffer());
} catch (Exception e) {
throw new RuntimeException("Unexpected error: ", e);
}
}
}

View File

@@ -1,41 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.client.admin.cli.aesh;
import org.jboss.aesh.console.AeshConsoleImpl;
import org.jboss.aesh.console.Console;
import java.lang.reflect.Field;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class AeshEnhancer {
public static void enhance(AeshConsoleImpl console) {
try {
Globals.stdin.setConsole(console);
Field field = AeshConsoleImpl.class.getDeclaredField("console");
field.setAccessible(true);
Console internalConsole = (Console) field.get(console);
internalConsole.setConsoleCallback(new AeshConsoleCallbackImpl(console));
} catch (Exception e) {
throw new RuntimeException("Failed to install Aesh enhancement", e);
}
}
}

View File

@@ -1,89 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.client.admin.cli.aesh;
import org.jboss.aesh.console.AeshConsoleImpl;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* This stream blocks and waits, until there is a stream in the queue.
* It reads the stream to the end, then stops Aesh console.
*
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class ValveInputStream extends InputStream {
private BlockingQueue<InputStream> queue = new LinkedBlockingQueue<>(10);
private InputStream current;
private AeshConsoleImpl console;
@Override
public int read() throws IOException {
if (current == null) {
try {
current = queue.take();
} catch (InterruptedException e) {
throw new InterruptedIOException("Signalled to exit");
}
}
int c = current.read();
if (c == -1) {
//current = null;
if (console != null) {
console.stop();
}
}
return c;
}
/**
* For some reason AeshInputStream wants to do blocking read of whole buffers, which for stdin
* results in blocked input.
*/
@Override
public int read(byte b[], int off, int len) throws IOException {
int c = read();
if (c == -1) {
return c;
}
b[off] = (byte) c;
return 1;
}
public void setInputStream(InputStream is) {
if (queue.contains(is)) {
return;
}
queue.add(is);
}
public void setConsole(AeshConsoleImpl console) {
this.console = console;
}
public boolean isStdinAvailable() {
return console.isRunning();
}
}

View File

@@ -16,8 +16,6 @@
*/
package org.keycloak.client.admin.cli.commands;
import org.jboss.aesh.cl.Option;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.keycloak.OAuth2Constants;
import org.keycloak.client.admin.cli.config.ConfigData;
import org.keycloak.client.admin.cli.config.ConfigHandler;
@@ -30,6 +28,8 @@ import org.keycloak.client.admin.cli.util.IoUtil;
import java.io.File;
import picocli.CommandLine.Option;
import static org.keycloak.client.admin.cli.config.FileConfigHandler.setConfigFile;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CLIENT;
import static org.keycloak.client.admin.cli.util.ConfigUtil.checkAuthInfo;
@@ -42,65 +42,61 @@ import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
*/
public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
@Option(shortName = 'a', name = "admin-root", description = "URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/admin")
@Option(names = {"-a", "--admin-root"}, description = "URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/admin")
String adminRestRoot;
@Option(name = "config", description = "Path to the config file (~/.keycloak/kcadm.config by default)")
@Option(names = "--config", description = "Path to the config file (~/.keycloak/kcadm.config by default)")
String config;
@Option(name = "no-config", description = "Don't use config file - no authentication info is loaded or saved", hasValue = false)
@Option(names = "--no-config", description = "Don't use config file - no authentication info is loaded or saved")
boolean noconfig;
@Option(name = "server", description = "Server endpoint url (e.g. 'http://localhost:8080')")
@Option(names = "--server", description = "Server endpoint url (e.g. 'http://localhost:8080')")
String server;
@Option(shortName = 'r', name = "target-realm", description = "Realm to target - when it's different than the realm we authenticate against")
@Option(names = {"-r", "--target-realm"}, description = "Realm to target - when it's different than the realm we authenticate against")
String targetRealm;
@Option(name = "realm", description = "Realm name to authenticate against")
@Option(names = "--realm", description = "Realm name to authenticate against")
String realm;
@Option(name = "client", description = "Realm name to authenticate against")
@Option(names = "--client", description = "Realm name to authenticate against")
String clientId;
@Option(name = "user", description = "Username to login with")
@Option(names = "--user", description = "Username to login with")
String user;
@Option(name = "password", description = "Password to login with (prompted for if not specified and --user is used)")
@Option(names = "--password", description = "Password to login with (prompted for if not specified and --user is used)")
String password;
@Option(name = "secret", description = "Secret to authenticate the client (prompted for if no --user or --keystore is specified)")
@Option(names = "--secret", description = "Secret to authenticate the client (prompted for if no --user or --keystore is specified)")
String secret;
@Option(name = "keystore", description = "Path to a keystore containing private key")
@Option(names = "--keystore", description = "Path to a keystore containing private key")
String keystore;
@Option(name = "storepass", description = "Keystore password (prompted for if not specified and --keystore is used)")
@Option(names = "--storepass", description = "Keystore password (prompted for if not specified and --keystore is used)")
String storePass;
@Option(name = "keypass", description = "Key password (prompted for if not specified and --keystore is used without --storepass, \n otherwise defaults to keystore password)")
@Option(names = "--keypass", description = "Key password (prompted for if not specified and --keystore is used without --storepass, \n otherwise defaults to keystore password)")
String keyPass;
@Option(name = "alias", description = "Alias of the key inside a keystore (defaults to the value of ClientId)")
@Option(names = "--alias", description = "Alias of the key inside a keystore (defaults to the value of ClientId)")
String alias;
@Option(name = "truststore", description = "Path to a truststore")
@Option(names = "--truststore", description = "Path to a truststore")
String trustStore;
@Option(name = "trustpass", description = "Truststore password (prompted for if not specified and --truststore is used)")
@Option(names = "--trustpass", description = "Truststore password (prompted for if not specified and --truststore is used)")
String trustPass;
@Option(name = "insecure", description = "Turns off TLS validation", hasValue = false)
@Option(names = "--insecure", description = "Turns off TLS validation")
boolean insecure;
@Option(name = "token", description = "Token to use for invocations. With this option set, every other authentication option is ignored")
@Option(names = "--token", description = "Token to use for invocations. With this option set, every other authentication option is ignored")
String externalToken;
protected void initFromParent(AbstractAuthOptionsCmd parent) {
super.initFromParent(parent);
noconfig = parent.noconfig;
config = parent.config;
server = parent.server;
@@ -124,11 +120,12 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
}
}
protected boolean noOptions() {
@Override
protected boolean nothingToDo() {
return externalToken == null && server == null && realm == null && clientId == null && secret == null &&
user == null && password == null &&
keystore == null && storePass == null && keyPass == null && alias == null &&
trustStore == null && trustPass == null && config == null && (args == null || args.size() == 0);
trustStore == null && trustPass == null && config == null;
}
@@ -136,12 +133,10 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
return targetRealm != null ? targetRealm : config.getRealm();
}
protected void processGlobalOptions() {
super.processGlobalOptions();
@Override
protected void processOptions() {
if (config != null && noconfig) {
throw new RuntimeException("Options --config and --no-config are mutually exclusive");
throw new IllegalArgumentException("Options --config and --no-config are mutually exclusive");
}
if (!noconfig) {
@@ -156,7 +151,7 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
}
}
protected void setupTruststore(ConfigData configData, CommandInvocation invocation ) {
protected void setupTruststore(ConfigData configData) {
if (!configData.getServerUrl().startsWith("https:")) {
return;
@@ -173,7 +168,7 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
pass = configData.getTrustpass();
}
if (pass == null) {
pass = IoUtil.readSecret("Enter truststore password: ", invocation);
pass = IoUtil.readSecret("Enter truststore password: ");
}
try {
@@ -188,7 +183,7 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
}
}
protected ConfigData ensureAuthInfo(ConfigData config, CommandInvocation commandInvocation) {
protected ConfigData ensureAuthInfo(ConfigData config) {
if (requiresLogin()) {
// make sure current handler is in-memory handler
@@ -204,7 +199,7 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
ConfigCredentialsCmd login = new ConfigCredentialsCmd();
login.initFromParent(this);
login.init(config);
login.process(commandInvocation);
login.process();
// this must be executed before finally block which restores config handler
return loadConfig();
@@ -269,22 +264,4 @@ public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
rdata.setGrantTypeForAuthentication(grantTypeForAuthentication);
}
protected void checkUnsupportedOptions(String ... options) {
if (options.length % 2 != 0) {
throw new IllegalArgumentException("Even number of argument required");
}
for (int i = 0; i < options.length; i++) {
String name = options[i];
String value = options[++i];
if (value != null) {
throw new IllegalArgumentException("Unsupported option: " + name);
}
}
}
protected static String booleanOptionForCheck(boolean value) {
return value ? "true" : null;
}
}

View File

@@ -16,56 +16,43 @@
*/
package org.keycloak.client.admin.cli.commands;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jboss.aesh.cl.Arguments;
import org.jboss.aesh.cl.Option;
import org.jboss.aesh.console.command.Command;
import org.keycloak.client.admin.cli.aesh.Globals;
import org.keycloak.client.admin.cli.Globals;
import org.keycloak.client.admin.cli.util.FilterUtil;
import org.keycloak.client.admin.cli.util.ReturnFields;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import picocli.CommandLine;
import picocli.CommandLine.Option;
import static org.keycloak.client.admin.cli.util.HttpUtil.normalize;
import static org.keycloak.client.admin.cli.util.IoUtil.printOut;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public abstract class AbstractGlobalOptionsCmd implements Command {
public abstract class AbstractGlobalOptionsCmd implements Runnable {
@Option(shortName = 'x', description = "Print full stack trace when exiting with error", hasValue = false)
boolean dumpTrace;
@Option(name = "help", description = "Print command specific help", hasValue = false)
boolean help;
// we don't want Aesh to handle illegal options
@Arguments
List<String> args;
protected void initFromParent(AbstractGlobalOptionsCmd parent) {
dumpTrace = parent.dumpTrace;
help = parent.help;
args = parent.args;
@Option(names = "--help",
description = "Print command specific help")
public void setHelp(boolean help) {
Globals.help = help;
}
protected void processGlobalOptions() {
@Option(names = "-x",
description = "Print full stack trace when exiting with error")
public void setDumpTrace(boolean dumpTrace) {
Globals.dumpTrace = dumpTrace;
}
protected boolean printHelp() {
if (help || nothingToDo()) {
protected void printHelpIfNeeded() {
if (Globals.help) {
printOut(help());
return true;
System.exit(CommandLine.ExitCode.OK);
} else if (nothingToDo()) {
printOut(help());
System.exit(CommandLine.ExitCode.USAGE);
}
return false;
}
protected boolean nothingToDo() {
@@ -80,13 +67,6 @@ public abstract class AbstractGlobalOptionsCmd implements Command {
return normalize(server) + "admin";
}
protected void requireValue(Iterator<String> it, String option) {
if (!it.hasNext()) {
throw new IllegalArgumentException("Option " + option + " requires a value");
}
}
protected String extractTypeNameFromUri(String resourceUrl) {
String type = extractLastComponentOfUri(resourceUrl);
if (type.endsWith("s")) {
@@ -110,4 +90,47 @@ public abstract class AbstractGlobalOptionsCmd implements Command {
throw new RuntimeException("Failed to apply fields filter", e);
}
}
@Override
public void run() {
printHelpIfNeeded();
checkUnsupportedOptions(getUnsupportedOptions());
processOptions();
process();
}
protected String[] getUnsupportedOptions() {
return new String[0];
}
protected void processOptions() {
}
protected void process() {
}
protected void checkUnsupportedOptions(String ... options) {
if (options.length % 2 != 0) {
throw new IllegalArgumentException("Even number of argument required");
}
for (int i = 0; i < options.length; i++) {
String name = options[i];
String value = options[++i];
if (value != null) {
throw new IllegalArgumentException("Unsupported option: " + name);
}
}
}
protected static String booleanOptionForCheck(boolean value) {
return value ? "true" : null;
}
}

View File

@@ -16,13 +16,7 @@
*/
package org.keycloak.client.admin.cli.commands;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.http.entity.ContentType;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.keycloak.client.admin.cli.common.AttributeOperation;
import org.keycloak.client.admin.cli.common.CmdStdinContext;
import org.keycloak.client.admin.cli.config.ConfigData;
@@ -44,13 +38,20 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.DELETE;
import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.SET;
import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken;
@@ -103,100 +104,57 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd {
String httpVerb;
Headers headers = new Headers();
@Option(names = {"-h", "--header"}, description = "Set request header NAME to VALUE")
List<String> rawHeaders = new LinkedList<>();
List<AttributeOperation> attrs = new LinkedList<>();
Map<String, String> filter = new HashMap<>();
String url = null;
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
try {
initOptions();
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
}
processGlobalOptions();
processOptions(commandInvocation);
return process(commandInvocation);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
} finally {
commandInvocation.stop();
}
// to maintain relative positions of set and delete operations
static class AttributeOperations {
@Option(names = {"-s", "--set"}, required = true) String set;
@Option(names = {"-d", "--delete"}, required = true) String delete;
}
abstract void initOptions();
@ArgGroup(exclusive = true, multiplicity = "0..*")
List<AttributeOperations> rawAttributeOperations = new ArrayList<>();
abstract String suggestHelp();
@Option(names = {"-q", "--query"}, description = "Add to request URI a NAME query parameter with value VALUE")
List<String> rawFilters = new LinkedList<>();
@Parameters(arity = "0..1")
String uri;
void processOptions(CommandInvocation commandInvocation) {
List<AttributeOperation> attrs = new LinkedList<>();
Headers headers = new Headers();
Map<String, String> filter = new HashMap<>();
if (args == null || args.isEmpty()) {
throw new IllegalArgumentException("URI not specified");
}
@Override
protected void processOptions() {
super.processOptions();
Iterator<String> it = args.iterator();
while (it.hasNext()) {
String option = it.next();
switch (option) {
case "-s":
case "--set": {
if (!it.hasNext()) {
throw new IllegalArgumentException("Option " + option + " requires a value");
}
String[] keyVal = parseKeyVal(it.next());
attrs.add(new AttributeOperation(SET, keyVal[0], keyVal[1]));
break;
}
case "-d":
case "--delete": {
attrs.add(new AttributeOperation(DELETE, it.next()));
break;
}
case "-h":
case "--header": {
requireValue(it, option);
String[] keyVal = parseKeyVal(it.next());
headers.add(keyVal[0], keyVal[1]);
break;
}
case "-q":
case "--query": {
if (!it.hasNext()) {
throw new IllegalArgumentException("Option " + option + " requires a value");
}
String arg = it.next();
String[] keyVal;
if (arg.indexOf("=") == -1) {
keyVal = new String[] {"", arg};
} else {
keyVal = parseKeyVal(arg);
}
filter.put(keyVal[0], keyVal[1]);
break;
}
default: {
if (url == null) {
url = option;
} else {
throw new IllegalArgumentException("Invalid option: " + option);
}
}
for (AttributeOperations entry : rawAttributeOperations) {
if (entry.delete != null) {
attrs.add(new AttributeOperation(DELETE, entry.delete));
} else {
String[] keyVal = parseKeyVal(entry.set);
attrs.add(new AttributeOperation(SET, keyVal[0], keyVal[1]));
}
}
for (String header : rawHeaders) {
String[] keyVal = parseKeyVal(header);
headers.add(keyVal[0], keyVal[1]);
}
if (url == null) {
for (String arg : rawFilters) {
String[] keyVal;
if (arg.indexOf("=") == -1) {
keyVal = new String[] {"", arg};
} else {
keyVal = parseKeyVal(arg);
}
filter.put(keyVal[0], keyVal[1]);
}
if (uri == null) {
throw new IllegalArgumentException("Resource URI not specified");
}
@@ -207,7 +165,7 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd {
try {
outputFormat = OutputFormat.valueOf(format.toUpperCase());
} catch (Exception e) {
throw new RuntimeException("Unsupported output format: " + format);
throw new IllegalArgumentException("Unsupported output format: " + format);
}
if (mergeMode && noMerge) {
@@ -223,10 +181,14 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd {
}
}
@Override
protected boolean nothingToDo() {
return super.nothingToDo() && file == null && body == null && uri == null && fields == null
&& rawAttributeOperations.isEmpty() && rawFilters.isEmpty() && rawHeaders.isEmpty();
}
public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
@Override
protected void process() {
// see if Content-Type header is explicitly set to non-json value
Header ctype = headers.get("content-type");
@@ -255,11 +217,11 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd {
ConfigData config = loadConfig();
config = copyWithServerInfo(config);
setupTruststore(config, commandInvocation);
setupTruststore(config);
String auth = null;
config = ensureAuthInfo(config, commandInvocation);
config = ensureAuthInfo(config);
config = copyWithServerInfo(config);
if (credentialsAvailable(config)) {
auth = ensureToken(config);
@@ -277,7 +239,7 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd {
final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server);
String resourceUrl = composeResourceUrl(adminRoot, realm, url);
String resourceUrl = composeResourceUrl(adminRoot, realm, uri);
String typeName = extractTypeNameFromUri(resourceUrl);
@@ -385,7 +347,7 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd {
}
if (outputResult) {
if (isCreateOrUpdate() && (response.getStatusCode() == 204 || id != null) && isGetByID(url)) {
if (isCreateOrUpdate() && (response.getStatusCode() == 204 || id != null) && isGetByID(uri)) {
// get object for id
headers = new Headers();
if (auth != null) {
@@ -423,7 +385,7 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd {
} else {
if (outputFormat != OutputFormat.JSON || returnFields != null) {
printErr("Cannot create CSV nor filter returned fields because the response is " + (compressed ? "compressed":"not json"));
return CommandResult.SUCCESS;
return;
}
// in theory the user could explicitly request json, but this could be a non-json response
// since there's no option for raw and we don't differentiate the default, there's no error about this
@@ -435,8 +397,6 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd {
if (lastByte != -1 && lastByte != 13 && lastByte != 10) {
printErr("");
}
return CommandResult.SUCCESS;
}
private boolean isUpdate() {

View File

@@ -17,11 +17,10 @@
package org.keycloak.client.admin.cli.commands;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.cl.Option;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import org.keycloak.client.admin.cli.config.ConfigData;
import org.keycloak.client.admin.cli.operations.ClientOperations;
import org.keycloak.client.admin.cli.operations.GroupOperations;
@@ -33,8 +32,6 @@ import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@@ -43,219 +40,184 @@ import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_
import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable;
import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "add-roles", description = "[ARGUMENTS]")
@Command(name = "add-roles", description = "[ARGUMENTS]")
public class AddRolesCmd extends AbstractAuthOptionsCmd {
@Option(name = "uusername", description = "Target user's 'username'")
@Option(names = "--uusername", description = "Target user's 'username'")
String uusername;
@Option(name = "uid", description = "Target user's 'id'")
@Option(names = "--uid", description = "Target user's 'id'")
String uid;
@Option(name = "gname", description = "Target group's 'name'")
@Option(names = "--gname", description = "Target group's 'name'")
String gname;
@Option(name = "gpath", description = "Target group's 'path'")
@Option(names = "--gpath", description = "Target group's 'path'")
String gpath;
@Option(name = "gid", description = "Target group's 'id'")
@Option(names = "--gid", description = "Target group's 'id'")
String gid;
@Option(name = "rname", description = "Composite role's 'name'")
@Option(names = "--rname", description = "Composite role's 'name'")
String rname;
@Option(name = "rid", description = "Composite role's 'id'")
@Option(names = "--rid", description = "Composite role's 'id'")
String rid;
@Option(name = "cclientid", description = "Target client's 'clientId'")
@Option(names = "--cclientid", description = "Target client's 'clientId'")
String cclientid;
@Option(name = "cid", description = "Target client's 'id'")
@Option(names = "--cid", description = "Target client's 'id'")
String cid;
@Option(names = "--rolename", description = "Role's 'name' attribute")
List<String> roleNames = new ArrayList<>();
@Option(names = "--roleid", description = "Role's 'id' attribute")
List<String> roleIds = new ArrayList<>();
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
protected void process() {
if (uid != null && uusername != null) {
throw new IllegalArgumentException("Incompatible options: --uid and --uusername are mutually exclusive");
}
List<String> roleNames = new LinkedList<>();
List<String> roleIds = new LinkedList<>();
if ((gid != null && gname != null) || (gid != null && gpath != null) || (gname != null && gpath != null)) {
throw new IllegalArgumentException("Incompatible options: --gid, --gname and --gpath are mutually exclusive");
}
try {
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
if (roleNames.isEmpty() && roleIds.isEmpty()) {
throw new IllegalArgumentException("No role to add specified. Use --rolename or --roleid to specify roles to add");
}
if (cid != null && cclientid != null) {
throw new IllegalArgumentException("Incompatible options: --cid and --cclientid are mutually exclusive");
}
if (rid != null && rname != null) {
throw new IllegalArgumentException("Incompatible options: --rid and --rname are mutually exclusive");
}
if (isUserSpecified() && isGroupSpecified()) {
throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --gname / --gid / --gpath");
}
if (isUserSpecified() && isCompositeRoleSpecified()) {
throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --rname / --rid");
}
if (isGroupSpecified() && isCompositeRoleSpecified()) {
throw new IllegalArgumentException("Incompatible options: --rname / --rid can't be used at the same time as --gname / --gid / --gpath");
}
if (!isUserSpecified() && !isGroupSpecified() && !isCompositeRoleSpecified()) {
throw new IllegalArgumentException("No user nor group nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role");
}
ConfigData config = loadConfig();
config = copyWithServerInfo(config);
setupTruststore(config);
String auth = null;
config = ensureAuthInfo(config);
config = copyWithServerInfo(config);
if (credentialsAvailable(config)) {
auth = ensureToken(config);
}
auth = auth != null ? "Bearer " + auth : null;
final String server = config.getServerUrl();
final String realm = getTargetRealm(config);
final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server);
if (isUserSpecified()) {
if (uid == null) {
uid = UserOperations.getIdFromUsername(adminRoot, realm, auth, uusername);
}
processGlobalOptions();
Iterator<String> it = args.iterator();
while (it.hasNext()) {
String option = it.next();
switch (option) {
case "--rolename": {
optionRequiresValueCheck(it, option);
roleNames.add(it.next());
break;
}
case "--roleid": {
optionRequiresValueCheck(it, option);
roleIds.add(it.next());
break;
}
default: {
throw new IllegalArgumentException("Invalid option: " + option);
}
}
}
if (uid != null && uusername != null) {
throw new IllegalArgumentException("Incompatible options: --uid and --uusername are mutually exclusive");
}
if ((gid != null && gname != null) || (gid != null && gpath != null) || (gname != null && gpath != null)) {
throw new IllegalArgumentException("Incompatible options: --gid, --gname and --gpath are mutually exclusive");
}
if (roleNames.isEmpty() && roleIds.isEmpty()) {
throw new IllegalArgumentException("No role to add specified. Use --rolename or --roleid to specify roles to add");
}
if (cid != null && cclientid != null) {
throw new IllegalArgumentException("Incompatible options: --cid and --cclientid are mutually exclusive");
}
if (rid != null && rname != null) {
throw new IllegalArgumentException("Incompatible options: --rid and --rname are mutually exclusive");
}
if (isUserSpecified() && isGroupSpecified()) {
throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --gname / --gid / --gpath");
}
if (isUserSpecified() && isCompositeRoleSpecified()) {
throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --rname / --rid");
}
if (isGroupSpecified() && isCompositeRoleSpecified()) {
throw new IllegalArgumentException("Incompatible options: --rname / --rid can't be used at the same time as --gname / --gid / --gpath");
}
if (!isUserSpecified() && !isGroupSpecified() && !isCompositeRoleSpecified()) {
throw new IllegalArgumentException("No user nor group nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role");
}
ConfigData config = loadConfig();
config = copyWithServerInfo(config);
setupTruststore(config, commandInvocation);
String auth = null;
config = ensureAuthInfo(config, commandInvocation);
config = copyWithServerInfo(config);
if (credentialsAvailable(config)) {
auth = ensureToken(config);
}
auth = auth != null ? "Bearer " + auth : null;
final String server = config.getServerUrl();
final String realm = getTargetRealm(config);
final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server);
if (isUserSpecified()) {
if (uid == null) {
uid = UserOperations.getIdFromUsername(adminRoot, realm, auth, uusername);
}
if (isClientSpecified()) {
// list client roles for a user
if (cid == null) {
cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
}
List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
// now add all the roles
UserOperations.addClientRoles(adminRoot, realm, auth, uid, cid, new ArrayList<>(rolesToAdd));
} else {
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
// now add all the roles
UserOperations.addRealmRoles(adminRoot, realm, auth, uid, new ArrayList<>(rolesToAdd));
if (isClientSpecified()) {
// list client roles for a user
if (cid == null) {
cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
}
} else if (isGroupSpecified()) {
if (gname != null) {
gid = GroupOperations.getIdFromName(adminRoot, realm, auth, gname);
} else if (gpath != null) {
gid = GroupOperations.getIdFromPath(adminRoot, realm, auth, gpath);
}
if (isClientSpecified()) {
// list client roles for a group
if (cid == null) {
cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
}
List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
// now add all the roles
GroupOperations.addClientRoles(adminRoot, realm, auth, gid, cid, new ArrayList<>(rolesToAdd));
} else {
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
// now add all the roles
GroupOperations.addRealmRoles(adminRoot, realm, auth, gid, new ArrayList<>(rolesToAdd));
}
} else if (isCompositeRoleSpecified()) {
if (rid == null) {
rid = RoleOperations.getIdFromRoleName(adminRoot, realm, auth, rname);
}
if (isClientSpecified()) {
// list client roles for a composite role
if (cid == null) {
cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
}
List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
// now add all the roles
RoleOperations.addClientRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd));
} else {
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
// now add all the roles
RoleOperations.addRealmRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd));
}
// now add all the roles
UserOperations.addClientRoles(adminRoot, realm, auth, uid, cid, new ArrayList<>(rolesToAdd));
} else {
throw new IllegalArgumentException("No user nor group, nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role");
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
// now add all the roles
UserOperations.addRealmRoles(adminRoot, realm, auth, uid, new ArrayList<>(rolesToAdd));
}
return CommandResult.SUCCESS;
} else if (isGroupSpecified()) {
if (gname != null) {
gid = GroupOperations.getIdFromName(adminRoot, realm, auth, gname);
} else if (gpath != null) {
gid = GroupOperations.getIdFromPath(adminRoot, realm, auth, gpath);
}
if (isClientSpecified()) {
// list client roles for a group
if (cid == null) {
cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
}
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
} finally {
commandInvocation.stop();
List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
// now add all the roles
GroupOperations.addClientRoles(adminRoot, realm, auth, gid, cid, new ArrayList<>(rolesToAdd));
} else {
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
// now add all the roles
GroupOperations.addRealmRoles(adminRoot, realm, auth, gid, new ArrayList<>(rolesToAdd));
}
} else if (isCompositeRoleSpecified()) {
if (rid == null) {
rid = RoleOperations.getIdFromRoleName(adminRoot, realm, auth, rname);
}
if (isClientSpecified()) {
// list client roles for a composite role
if (cid == null) {
cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
}
List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
// now add all the roles
RoleOperations.addClientRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd));
} else {
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
// now add all the roles
RoleOperations.addRealmRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd));
}
} else {
throw new IllegalArgumentException("No user nor group, nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role");
}
}
@@ -280,12 +242,6 @@ public class AddRolesCmd extends AbstractAuthOptionsCmd {
return rolesToAdd;
}
private void optionRequiresValueCheck(Iterator<String> it, String option) {
if (!it.hasNext()) {
throw new IllegalArgumentException("Option " + option + " requires a value");
}
}
private boolean isClientSpecified() {
return cid != null || cclientid != null;
}
@@ -304,13 +260,10 @@ public class AddRolesCmd extends AbstractAuthOptionsCmd {
@Override
protected boolean nothingToDo() {
return noOptions() && uusername == null && uid == null && cclientid == null && (args == null || args.size() == 0);
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help add-roles' for more information";
return super.nothingToDo() && uusername == null && uid == null && cclientid == null && roleIds.isEmpty() && roleNames.isEmpty();
}
@Override
protected String help() {
return usage();
}

View File

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

View File

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

View File

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

View File

@@ -16,70 +16,63 @@
*/
package org.keycloak.client.admin.cli.commands;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.cl.Option;
import java.io.PrintWriter;
import java.io.StringWriter;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
import static org.keycloak.client.admin.cli.util.OsUtil.OS_ARCH;
import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "create", description = "Command to create new resources")
@Command(name = "create", description = "Command to create new resources")
public class CreateCmd extends AbstractRequestCmd {
@Option(shortName = 'f', name = "file", description = "Read object from file or standard input if FILENAME is set to '-'")
String file;
public CreateCmd() {
this.httpVerb = "post";
}
@Option(shortName = 'b', name = "body", description = "JSON object to be sent as-is or used as a template")
String body;
@Option(names = {"-f", "--file"}, description = "Read object from file or standard input if FILENAME is set to '-'")
public void setFile(String file) {
this.file = file;
}
@Option(shortName = 'F', name = "fields", description = "A pattern specifying which attributes of JSON response body to actually display as result - causes mismatch with Content-Length header", hasValue = true)
String fields;
@Option(names = {"-b", "--body"}, description = "JSON object to be sent as-is or used as a template")
public void setBody(String body) {
this.body = body;
}
@Option(shortName = 'H', name = "print-headers", description = "Print response headers", hasValue = false)
boolean printHeaders;
@Option(names = {"-F", "--fields"}, description = "A pattern specifying which attributes of JSON response body to actually display as result - causes mismatch with Content-Length header")
public void setFields(String fields) {
this.fields = fields;
}
@Option(shortName = 'i', name = "id", description = "After creation only print id of created resource to standard output", hasValue = false)
boolean returnId = false;
@Option(names = {"-H", "--print-headers"}, description = "Print response headers")
public void setPrintHeaders(boolean printHeaders) {
this.printHeaders = printHeaders;
}
@Option(shortName = 'o', name = "output", description = "After creation output the new resource to standard output", hasValue = false)
boolean outputResult = false;
@Option(names = {"-i", "--id"}, description = "After creation only print id of created resource to standard output")
public void setReturnId(boolean returnId) {
this.returnId = returnId;
}
@Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false)
boolean compressed = false;
@Option(names = {"-o", "--output"}, description = "After creation output the new resource to standard output")
public void setOutputResult(boolean outputResult) {
this.outputResult = outputResult;
}
//@OptionGroup(shortName = 's', name = "set", description = "Set attribute to the specified value")
//Map<String, String> attributes = new LinkedHashMap<>();
@Override
void initOptions() {
// set options on parent
super.file = file;
super.body = body;
super.fields = fields;
super.printHeaders = printHeaders;
super.returnId = returnId;
super.outputResult = outputResult;
super.compressed = compressed;
super.httpVerb = "post";
@Option(names = {"-c", "--compressed"}, description = "Don't pretty print the output")
public void setCompressed(boolean compressed) {
this.compressed = compressed;
}
@Override
protected boolean nothingToDo() {
return noOptions() && file == null && body == null && (args == null || args.size() == 0);
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help create' for more information";
}
protected String help() {
return usage();
}

View File

@@ -16,37 +16,27 @@
*/
package org.keycloak.client.admin.cli.commands;
import org.jboss.aesh.cl.CommandDefinition;
import java.io.PrintWriter;
import java.io.StringWriter;
import picocli.CommandLine.Command;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "delete", description = "CLIENT [GLOBAL_OPTIONS]")
@Command(name = "delete", description = "CLIENT [GLOBAL_OPTIONS]")
public class DeleteCmd extends CreateCmd {
void initOptions() {
super.initOptions();
httpVerb = "delete";
public DeleteCmd() {
this.httpVerb = "delete";
}
@Override
protected boolean nothingToDo() {
return noOptions() && (args == null || args.size() == 0);
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help delete' for more information";
}
protected String help() {
return usage();
}

View File

@@ -16,69 +16,63 @@
*/
package org.keycloak.client.admin.cli.commands;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.cl.Option;
import java.io.PrintWriter;
import java.io.StringWriter;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "get", description = "[ARGUMENTS]")
public class GetCmd extends AbstractRequestCmd {
@Command(name = "get", description = "[ARGUMENTS]")
public class GetCmd extends AbstractRequestCmd {
@Option(name = "noquotes", description = "", hasValue = false)
boolean unquoted;
public GetCmd() {
this.httpVerb = "get";
this.outputResult = true;
}
@Option(shortName = 'F', name = "fields", description = "A pattern specifying which attributes of JSON response body to actually display as result - causes mismatch with Content-Length header")
String fields;
@Option(names = "--noquotes", description = "")
public void setUnquoted(boolean unquoted) {
this.unquoted = unquoted;
}
@Option(shortName = 'H', name = "print-headers", description = "Print response headers", hasValue = false)
boolean printHeaders;
@Option(names = {"-F", "--fields"}, description = "A pattern specifying which attributes of JSON response body to actually display as result - causes mismatch with Content-Length header")
public void setFields(String fields) {
this.fields = fields;
}
@Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false)
boolean compressed;
@Option(names = {"-H", "--print-headers"}, description = "Print response headers")
public void setPrintHeaders(boolean printHeaders) {
this.printHeaders = printHeaders;
}
@Option(shortName = 'o', name = "offset", description = "Number of results from beginning of resultset to skip")
Integer offset;
@Option(names = {"-c", "--compressed"}, description = "Don't pretty print the output")
public void setCompressed(boolean compressed) {
this.compressed = compressed;
}
@Option(shortName = 'l', name = "limit", description = "Maksimum number of results to return")
Integer limit;
@Option(names = {"-o", "--offset"}, description = "Number of results from beginning of resultset to skip")
public void setOffset(Integer offset) {
this.offset = offset;
}
@Option(name = "format", description = "Output format - one of: json, csv", defaultValue = "json")
String format;
@Option(names = {"-l", "--limit"}, description = "Maksimum number of results to return")
public void setLimit(Integer limit) {
this.limit = limit;
}
@Override
void initOptions() {
// set options on parent
super.fields = fields;
super.printHeaders = printHeaders;
super.returnId = false;
super.outputResult = true;
super.compressed = compressed;
super.offset = offset;
super.limit = limit;
super.format = format;
super.unquoted = unquoted;
super.httpVerb = "get";
@Option(names = "--format", description = "Output format - one of: json, csv", defaultValue = "json")
public void setFormat(String format) {
this.format = format;
}
@Override
protected boolean nothingToDo() {
return noOptions() && (args == null || args.size() == 0);
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help get' for more information";
}
protected String help() {
return usage();
}

View File

@@ -16,11 +16,6 @@
*/
package org.keycloak.client.admin.cli.commands;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.cl.Option;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.keycloak.client.admin.cli.config.ConfigData;
import org.keycloak.client.admin.cli.operations.ClientOperations;
import org.keycloak.client.admin.cli.operations.GroupOperations;
@@ -29,7 +24,9 @@ import org.keycloak.client.admin.cli.operations.UserOperations;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
@@ -42,69 +39,58 @@ import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "get-roles", description = "[ARGUMENTS]")
@Command(name = "get-roles", description = "[ARGUMENTS]")
public class GetRolesCmd extends GetCmd {
@Option(name = "uusername", description = "Target user's 'username'")
@Option(names = "--uusername", description = "Target user's 'username'")
String uusername;
@Option(name = "uid", description = "Target user's 'id'")
@Option(names = "--uid", description = "Target user's 'id'")
String uid;
@Option(name = "cclientid", description = "Target client's 'clientId'")
@Option(names = "--cclientid", description = "Target client's 'clientId'")
String cclientid;
@Option(name = "cid", description = "Target client's 'id'")
@Option(names = "--cid", description = "Target client's 'id'")
String cid;
@Option(name = "rname", description = "Composite role's 'name'")
@Option(names = "--rname", description = "Composite role's 'name'")
String rname;
@Option(name = "rid", description = "Composite role's 'id'")
@Option(names = "--rid", description = "Composite role's 'id'")
String rid;
@Option(name = "gname", description = "Target group's 'name'")
@Option(names = "--gname", description = "Target group's 'name'")
String gname;
@Option(name = "gpath", description = "Target group's 'path'")
@Option(names = "--gpath", description = "Target group's 'path'")
String gpath;
@Option(name = "gid", description = "Target group's 'id'")
@Option(names = "--gid", description = "Target group's 'id'")
String gid;
@Option(name = "rolename", description = "Target role's 'name'")
@Option(names = "--rolename", description = "Target role's 'name'")
String rolename;
@Option(name = "roleid", description = "Target role's 'id'")
@Option(names = "--roleid", description = "Target role's 'id'")
String roleid;
@Option(name = "available", description = "List only available roles", hasValue = false)
@Option(names = "--available", description = "List only available roles")
boolean available;
@Option(name = "effective", description = "List assigned roles including transitively included roles", hasValue = false)
@Option(names = "--effective", description = "List assigned roles including transitively included roles")
boolean effective;
@Option(name = "all", description = "List roles for all clients in addition to realm roles", hasValue = false)
@Option(names = "--all", description = "List roles for all clients in addition to realm roles")
boolean all;
void initOptions() {
super.initOptions();
@Override
protected void processOptions() {
// hack args so that GetCmd option check doesn't fail
// set a placeholder
if (args == null) {
args = new ArrayList();
if (uri == null) {
uri = "uri";
}
if (args.size() == 0) {
args.add("uri");
} else {
args.add(0, "uri");
}
}
void processOptions(CommandInvocation commandInvocation) {
if (uid != null && uusername != null) {
throw new IllegalArgumentException("Incompatible options: --uid and --uusername are mutually exclusive");
@@ -146,19 +132,19 @@ public class GetRolesCmd extends GetCmd {
throw new IllegalArgumentException("Incompatible options: --all can't be used at the same time as --available");
}
super.processOptions(commandInvocation);
super.processOptions();
}
public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
@Override
protected void process() {
ConfigData config = loadConfig();
config = copyWithServerInfo(config);
setupTruststore(config, commandInvocation);
setupTruststore(config);
String auth = null;
config = ensureAuthInfo(config, commandInvocation);
config = ensureAuthInfo(config);
config = copyWithServerInfo(config);
if (credentialsAvailable(config)) {
auth = ensureToken(config);
@@ -180,20 +166,20 @@ public class GetRolesCmd extends GetCmd {
cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
}
if (available) {
super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/clients/" + cid + "/available");
super.uri = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/clients/" + cid + "/available");
} else if (effective) {
super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/clients/" + cid + "/composite");
super.uri = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/clients/" + cid + "/composite");
} else {
super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/clients/" + cid);
super.uri = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/clients/" + cid);
}
} else {
// list realm roles for a user
if (available) {
super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/realm/available");
super.uri = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/realm/available");
} else if (effective) {
super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/realm/composite");
super.uri = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/realm/composite");
} else {
super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + (all ? "/role-mappings" : "/role-mappings/realm"));
super.uri = composeResourceUrl(adminRoot, realm, "users/" + uid + (all ? "/role-mappings" : "/role-mappings/realm"));
}
}
} else if (isGroupSpecified()) {
@@ -208,20 +194,20 @@ public class GetRolesCmd extends GetCmd {
cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
}
if (available) {
super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/clients/" + cid + "/available");
super.uri = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/clients/" + cid + "/available");
} else if (effective) {
super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/clients/" + cid + "/composite");
super.uri = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/clients/" + cid + "/composite");
} else {
super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/clients/" + cid);
super.uri = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/clients/" + cid);
}
} else {
// list realm roles for a group
if (available) {
super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/realm/available");
super.uri = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/realm/available");
} else if (effective) {
super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/realm/composite");
super.uri = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/realm/composite");
} else {
super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + (all ? "/role-mappings" : "/role-mappings/realm"));
super.uri = composeResourceUrl(adminRoot, realm, "groups/" + gid + (all ? "/role-mappings" : "/role-mappings/realm"));
}
}
} else if (isCompositeRoleSpecified()) {
@@ -248,7 +234,7 @@ public class GetRolesCmd extends GetCmd {
uri += all ? "/composites" : "/composites/realm";
}
super.url = composeResourceUrl(adminRoot, realm, uri);
super.uri = composeResourceUrl(adminRoot, realm, uri);
} else if (isClientSpecified()) {
if (cid == null) {
@@ -260,10 +246,10 @@ public class GetRolesCmd extends GetCmd {
if (rolename == null) {
rolename = RoleOperations.getClientRoleNameFromId(adminRoot, realm, auth, cid, roleid);
}
super.url = composeResourceUrl(adminRoot, realm, "clients/" + cid + "/roles/" + rolename);
super.uri = composeResourceUrl(adminRoot, realm, "clients/" + cid + "/roles/" + rolename);
} else {
// list defined client roles
super.url = composeResourceUrl(adminRoot, realm, "clients/" + cid + "/roles");
super.uri = composeResourceUrl(adminRoot, realm, "clients/" + cid + "/roles");
}
} else {
if (isRoleSpecified()) {
@@ -271,14 +257,14 @@ public class GetRolesCmd extends GetCmd {
if (rolename == null) {
rolename = RoleOperations.getClientRoleNameFromId(adminRoot, realm, auth, cid, roleid);
}
super.url = composeResourceUrl(adminRoot, realm, "roles/" + rolename);
super.uri = composeResourceUrl(adminRoot, realm, "roles/" + rolename);
} else {
// list defined realm roles
super.url = composeResourceUrl(adminRoot, realm, "roles");
super.uri = composeResourceUrl(adminRoot, realm, "roles");
}
}
return super.process(commandInvocation);
super.process();
}
private boolean isRoleSpecified() {
@@ -301,14 +287,12 @@ public class GetRolesCmd extends GetCmd {
return uid != null || uusername != null;
}
protected String suggestHelp() {
return "";
}
@Override
protected boolean nothingToDo() {
return false;
}
@Override
protected String help() {
return usage();
}

View File

@@ -16,92 +16,81 @@
*/
package org.keycloak.client.admin.cli.commands;
import org.jboss.aesh.cl.Arguments;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.console.command.Command;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import java.util.List;
import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters;
import static org.keycloak.client.admin.cli.util.IoUtil.printOut;
@Command(name = "help", description = "This Help")
public class HelpCmd implements Runnable {
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "help", description = "This help")
public class HelpCmd implements Command {
@Arguments
@Parameters
List<String> args;
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
try {
if (args == null || args.size() == 0) {
printOut(KcAdmCmd.usage());
} else {
outer:
switch (args.get(0)) {
case "config": {
if (args.size() > 1) {
switch (args.get(1)) {
case "credentials": {
printOut(ConfigCredentialsCmd.usage());
break outer;
}
case "truststore": {
printOut(ConfigTruststoreCmd.usage());
break outer;
}
}
}
printOut(ConfigCmd.usage());
break;
public void run() {
if (args == null || args.size() == 0) {
printOut(KcAdmCmd.usage());
} else {
outer: switch (args.get(0)) {
case "config": {
if (args.size() > 1) {
switch (args.get(1)) {
case "credentials": {
printOut(ConfigCredentialsCmd.usage());
break outer;
}
case "create": {
printOut(CreateCmd.usage());
break;
case "truststore": {
printOut(ConfigTruststoreCmd.usage());
break outer;
}
case "get": {
printOut(GetCmd.usage());
break;
}
case "update": {
printOut(UpdateCmd.usage());
break;
}
case "delete": {
printOut(DeleteCmd.usage());
break;
}
case "get-roles": {
printOut(GetRolesCmd.usage());
break;
}
case "add-roles": {
printOut(AddRolesCmd.usage());
break;
}
case "remove-roles": {
printOut(RemoveRolesCmd.usage());
break;
}
case "set-password": {
printOut(SetPasswordCmd.usage());
break;
}
default: {
throw new RuntimeException("Unknown command: " + args.get(0));
}
}
printOut(ConfigCmd.usage());
break;
}
case "create": {
printOut(CreateCmd.usage());
break;
}
case "get": {
printOut(GetCmd.usage());
break;
}
case "update": {
printOut(UpdateCmd.usage());
break;
}
case "delete": {
printOut(DeleteCmd.usage());
break;
}
case "get-roles": {
printOut(GetRolesCmd.usage());
break;
}
case "add-roles": {
printOut(AddRolesCmd.usage());
break;
}
case "remove-roles": {
printOut(RemoveRolesCmd.usage());
break;
}
case "set-password": {
printOut(SetPasswordCmd.usage());
break;
}
case "new-object": {
printOut(NewObjectCmd.usage());
break;
}
default: {
throw new IllegalArgumentException("Unknown command: " + args.get(0));
}
}
return CommandResult.SUCCESS;
} finally {
commandInvocation.stop();
}
}
}

View File

@@ -16,47 +16,42 @@
*/
package org.keycloak.client.admin.cli.commands;
import org.jboss.aesh.cl.GroupCommandDefinition;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import java.io.PrintWriter;
import java.io.StringWriter;
import picocli.CommandLine.Command;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
import static org.keycloak.client.admin.cli.util.IoUtil.printErr;
import static org.keycloak.client.admin.cli.util.IoUtil.printOut;
import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@GroupCommandDefinition(name = "kcadm", description = "COMMAND [ARGUMENTS]", groupCommands = {
HelpCmd.class, ConfigCmd.class, NewObjectCmd.class, CreateCmd.class, GetCmd.class, UpdateCmd.class, DeleteCmd.class,
AddRolesCmd.class, RemoveRolesCmd.class, GetRolesCmd.class, SetPasswordCmd.class} )
@Command(name = "kcadm",
header = {
"Keycloak - Open Source Identity and Access Management",
"",
"Find more information at: https://www.keycloak.org/docs/latest"
},
description = {
"%nCOMMAND [ARGUMENTS]"
},
subcommands = {
HelpCmd.class,
ConfigCmd.class,
NewObjectCmd.class,
CreateCmd.class,
GetCmd.class,
UpdateCmd.class,
DeleteCmd.class,
AddRolesCmd.class,
RemoveRolesCmd.class,
GetRolesCmd.class,
SetPasswordCmd.class
})
public class KcAdmCmd extends AbstractGlobalOptionsCmd {
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
try {
// if --help was requested then status is SUCCESS
// if not we print help anyway, but status is FAILURE
if (printHelp()) {
return CommandResult.SUCCESS;
} else if (args != null && args.size() > 0) {
printErr("Unknown command: " + args.get(0));
return CommandResult.FAILURE;
} else {
printOut(usage());
return CommandResult.FAILURE;
}
} finally {
commandInvocation.stop();
}
protected boolean nothingToDo() {
return true;
}
public static String usage() {

View File

@@ -17,11 +17,10 @@
package org.keycloak.client.admin.cli.commands;
import com.fasterxml.jackson.databind.JsonNode;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.cl.Option;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import org.keycloak.client.admin.cli.common.AttributeOperation;
import org.keycloak.client.admin.cli.common.CmdStdinContext;
import org.keycloak.client.admin.cli.util.AccessibleBufferOutputStream;
@@ -32,15 +31,14 @@ import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.SET;
import static org.keycloak.client.admin.cli.util.IoUtil.copyStream;
import static org.keycloak.client.admin.cli.util.IoUtil.printErr;
import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
import static org.keycloak.client.admin.cli.util.OsUtil.OS_ARCH;
import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
import static org.keycloak.client.admin.cli.util.OutputUtil.MAPPER;
@@ -51,59 +49,24 @@ import static org.keycloak.client.admin.cli.util.ParseUtil.parseKeyVal;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "new-object", description = "Command to create new JSON objects locally")
@Command(name = "new-object", description = "Command to create new JSON objects locally")
public class NewObjectCmd extends AbstractGlobalOptionsCmd {
@Option(shortName = 'f', name = "file", description = "Read object from file or standard input if FILENAME is set to '-'", hasValue = true)
@Option(names = {"-f", "--file"}, description = "Read object from file or standard input if FILENAME is set to '-'")
String file;
@Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false)
@Option(names = {"-c", "--compressed"}, description = "Don't pretty print the output")
boolean compressed;
//@OptionGroup(shortName = 's', name = "set", description = "Set attribute to the specified value")
//Map<String, String> attributes = new LinkedHashMap<>();
@Option(names = {"-s", "--set"}, description = "Set a specific attribute NAME to a specified value VALUE")
List<String> values = new ArrayList<>();
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
try {
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
}
processGlobalOptions();
return process(commandInvocation);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
} finally {
commandInvocation.stop();
}
}
public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
List<AttributeOperation> attrs = new LinkedList<>();
Iterator<String> it = args.iterator();
while (it.hasNext()) {
String option = it.next();
switch (option) {
case "-s":
case "--set": {
if (!it.hasNext()) {
throw new IllegalArgumentException("Option " + option + " requires a value");
}
String[] keyVal = parseKeyVal(it.next());
attrs.add(new AttributeOperation(SET, keyVal[0], keyVal[1]));
break;
}
default: {
throw new IllegalArgumentException("Invalid option: " + option);
}
}
}
public void process() {
List<AttributeOperation> attrs = values.stream().map(it -> {
String[] keyVal = parseKeyVal(it);
return new AttributeOperation(SET, keyVal[0], keyVal[1]);
}).collect(Collectors.toList());
InputStream body = null;
@@ -142,20 +105,14 @@ public class NewObjectCmd extends AbstractGlobalOptionsCmd {
if (lastByte != -1 && lastByte != 13 && lastByte != 10) {
printErr("");
}
return CommandResult.SUCCESS;
}
@Override
protected boolean nothingToDo() {
return file == null && (args == null || args.size() == 0);
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help create' for more information";
return file == null && values.isEmpty();
}
@Override
protected String help() {
return usage();
}

View File

@@ -16,250 +16,218 @@
*/
package org.keycloak.client.admin.cli.commands;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.cl.Option;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.keycloak.client.admin.cli.config.ConfigData;
import org.keycloak.client.admin.cli.operations.ClientOperations;
import org.keycloak.client.admin.cli.operations.GroupOperations;
import org.keycloak.client.admin.cli.operations.RoleOperations;
import org.keycloak.client.admin.cli.operations.LocalSearch;
import org.keycloak.client.admin.cli.operations.RoleOperations;
import org.keycloak.client.admin.cli.operations.UserOperations;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import com.fasterxml.jackson.databind.node.ObjectNode;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable;
import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "remove-roles", description = "[ARGUMENTS]")
@Command(name = "remove-roles", description = "[ARGUMENTS]")
public class RemoveRolesCmd extends AbstractAuthOptionsCmd {
@Option(name = "uusername", description = "Target user's 'username'")
@Option(names = "--uusername", description = "Target user's 'username'")
String uusername;
@Option(name = "uid", description = "Target user's 'id'")
@Option(names = "--uid", description = "Target user's 'id'")
String uid;
@Option(name = "gname", description = "Target group's 'name'")
@Option(names = "--gname", description = "Target group's 'name'")
String gname;
@Option(name = "gpath", description = "Target group's 'path'")
@Option(names = "--gpath", description = "Target group's 'path'")
String gpath;
@Option(name = "gid", description = "Target group's 'id'")
@Option(names = "--gid", description = "Target group's 'id'")
String gid;
@Option(name = "rname", description = "Composite role's 'name'")
@Option(names = "--rname", description = "Composite role's 'name'")
String rname;
@Option(name = "rid", description = "Composite role's 'id'")
@Option(names = "--rid", description = "Composite role's 'id'")
String rid;
@Option(name = "cclientid", description = "Target client's 'clientId'")
@Option(names = "--cclientid", description = "Target client's 'clientId'")
String cclientid;
@Option(name = "cid", description = "Target client's 'id'")
@Option(names = "--cid", description = "Target client's 'id'")
String cid;
@Option(names = "--rolename", description = "Role's 'name' attribute")
List<String> roleNames = new ArrayList<>();
@Option(names = "--roleid", description = "Role's 'id' attribute")
List<String> roleIds = new ArrayList<>();
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
protected void process() {
if (uid != null && uusername != null) {
throw new IllegalArgumentException("Incompatible options: --uid and --uusername are mutually exclusive");
}
List<String> roleNames = new LinkedList<>();
List<String> roleIds = new LinkedList<>();
if ((gid != null && gname != null) || (gid != null && gpath != null) || (gname != null && gpath != null)) {
throw new IllegalArgumentException(
"Incompatible options: --gid, --gname and --gpath are mutually exclusive");
}
try {
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
if (roleNames.isEmpty() && roleIds.isEmpty()) {
throw new IllegalArgumentException(
"No role to remove specified. Use --rolename or --roleid to specify roles to remove");
}
if (cid != null && cclientid != null) {
throw new IllegalArgumentException("Incompatible options: --cid and --cclientid are mutually exclusive");
}
if (rid != null && rname != null) {
throw new IllegalArgumentException("Incompatible options: --rid and --rname are mutually exclusive");
}
if (isUserSpecified() && isGroupSpecified()) {
throw new IllegalArgumentException(
"Incompatible options: --uusername / --uid can't be used at the same time as --gname / --gid / --gpath");
}
if (isUserSpecified() && isCompositeRoleSpecified()) {
throw new IllegalArgumentException(
"Incompatible options: --uusername / --uid can't be used at the same time as --rname / --rid");
}
if (isGroupSpecified() && isCompositeRoleSpecified()) {
throw new IllegalArgumentException(
"Incompatible options: --rname / --rid can't be used at the same time as --gname / --gid / --gpath");
}
if (!isUserSpecified() && !isGroupSpecified() && !isCompositeRoleSpecified()) {
throw new IllegalArgumentException(
"No user nor group nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role");
}
ConfigData config = loadConfig();
config = copyWithServerInfo(config);
setupTruststore(config);
String auth = null;
config = ensureAuthInfo(config);
config = copyWithServerInfo(config);
if (credentialsAvailable(config)) {
auth = ensureToken(config);
}
auth = auth != null ? "Bearer " + auth : null;
final String server = config.getServerUrl();
final String realm = getTargetRealm(config);
final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server);
if (isUserSpecified()) {
if (uid == null) {
uid = UserOperations.getIdFromUsername(adminRoot, realm, auth, uusername);
}
processGlobalOptions();
Iterator<String> it = args.iterator();
while (it.hasNext()) {
String option = it.next();
switch (option) {
case "--rolename": {
optionRequiresValueCheck(it, option);
roleNames.add(it.next());
break;
}
case "--roleid": {
optionRequiresValueCheck(it, option);
roleIds.add(it.next());
break;
}
default: {
throw new IllegalArgumentException("Invalid option: " + option);
}
}
}
if (uid != null && uusername != null) {
throw new IllegalArgumentException("Incompatible options: --uid and --uusername are mutually exclusive");
}
if ((gid != null && gname != null) || (gid != null && gpath != null) || (gname != null && gpath != null)) {
throw new IllegalArgumentException("Incompatible options: --gid, --gname and --gpath are mutually exclusive");
}
if (roleNames.isEmpty() && roleIds.isEmpty()) {
throw new IllegalArgumentException("No role to remove specified. Use --rolename or --roleid to specify roles to remove");
}
if (cid != null && cclientid != null) {
throw new IllegalArgumentException("Incompatible options: --cid and --cclientid are mutually exclusive");
}
if (rid != null && rname != null) {
throw new IllegalArgumentException("Incompatible options: --rid and --rname are mutually exclusive");
}
if (isUserSpecified() && isGroupSpecified()) {
throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --gname / --gid / --gpath");
}
if (isUserSpecified() && isCompositeRoleSpecified()) {
throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --rname / --rid");
}
if (isGroupSpecified() && isCompositeRoleSpecified()) {
throw new IllegalArgumentException("Incompatible options: --rname / --rid can't be used at the same time as --gname / --gid / --gpath");
}
if (!isUserSpecified() && !isGroupSpecified() && !isCompositeRoleSpecified()) {
throw new IllegalArgumentException("No user nor group nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role");
}
ConfigData config = loadConfig();
config = copyWithServerInfo(config);
setupTruststore(config, commandInvocation);
String auth = null;
config = ensureAuthInfo(config, commandInvocation);
config = copyWithServerInfo(config);
if (credentialsAvailable(config)) {
auth = ensureToken(config);
}
auth = auth != null ? "Bearer " + auth : null;
final String server = config.getServerUrl();
final String realm = getTargetRealm(config);
final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server);
if (isUserSpecified()) {
if (uid == null) {
uid = UserOperations.getIdFromUsername(adminRoot, realm, auth, uusername);
}
if (isClientSpecified()) {
// remove client roles from a user
if (cid == null) {
cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
}
List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
// now remove the roles
UserOperations.removeClientRoles(adminRoot, realm, auth, uid, cid, new ArrayList<>(rolesToAdd));
} else {
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
// now remove the roles
UserOperations.removeRealmRoles(adminRoot, realm, auth, uid, new ArrayList<>(rolesToAdd));
if (isClientSpecified()) {
// remove client roles from a user
if (cid == null) {
cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
}
} else if (isGroupSpecified()) {
if (gname != null) {
gid = GroupOperations.getIdFromName(adminRoot, realm, auth, gname);
} else if (gpath != null) {
gid = GroupOperations.getIdFromPath(adminRoot, realm, auth, gpath);
}
if (isClientSpecified()) {
// remove client roles from a group
if (cid == null) {
cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
}
List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
// now remove the roles
GroupOperations.removeClientRoles(adminRoot, realm, auth, gid, cid, new ArrayList<>(rolesToAdd));
} else {
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
// now remove the roles
GroupOperations.removeRealmRoles(adminRoot, realm, auth, gid, new ArrayList<>(rolesToAdd));
}
} else if (isCompositeRoleSpecified()) {
if (rid == null) {
rid = RoleOperations.getIdFromRoleName(adminRoot, realm, auth, rname);
}
if (isClientSpecified()) {
// remove client roles from a role
if (cid == null) {
cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
}
List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
// now remove the roles
RoleOperations.removeClientRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd));
} else {
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
// now remove the roles
RoleOperations.removeRealmRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd));
}
// now remove the roles
UserOperations.removeClientRoles(adminRoot, realm, auth, uid, cid, new ArrayList<>(rolesToAdd));
} else {
throw new IllegalArgumentException("No user nor group, nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role");
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
// now remove the roles
UserOperations.removeRealmRoles(adminRoot, realm, auth, uid, new ArrayList<>(rolesToAdd));
}
return CommandResult.SUCCESS;
} else if (isGroupSpecified()) {
if (gname != null) {
gid = GroupOperations.getIdFromName(adminRoot, realm, auth, gname);
} else if (gpath != null) {
gid = GroupOperations.getIdFromPath(adminRoot, realm, auth, gpath);
}
if (isClientSpecified()) {
// remove client roles from a group
if (cid == null) {
cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
}
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
} finally {
commandInvocation.stop();
List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
// now remove the roles
GroupOperations.removeClientRoles(adminRoot, realm, auth, gid, cid, new ArrayList<>(rolesToAdd));
} else {
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
// now remove the roles
GroupOperations.removeRealmRoles(adminRoot, realm, auth, gid, new ArrayList<>(rolesToAdd));
}
} else if (isCompositeRoleSpecified()) {
if (rid == null) {
rid = RoleOperations.getIdFromRoleName(adminRoot, realm, auth, rname);
}
if (isClientSpecified()) {
// remove client roles from a role
if (cid == null) {
cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
}
List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
// now remove the roles
RoleOperations.removeClientRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd));
} else {
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
// now remove the roles
RoleOperations.removeRealmRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd));
}
} else {
throw new IllegalArgumentException(
"No user nor group, nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role");
}
}
private Set<ObjectNode> getRoleRepresentations(List<String> roleNames, List<String> roleIds, LocalSearch roleSearch) {
private Set<ObjectNode> getRoleRepresentations(List<String> roleNames, List<String> roleIds,
LocalSearch roleSearch) {
Set<ObjectNode> rolesToAdd = new HashSet<>();
// now we process roles
@@ -280,12 +248,6 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd {
return rolesToAdd;
}
private void optionRequiresValueCheck(Iterator<String> it, String option) {
if (!it.hasNext()) {
throw new IllegalArgumentException("Option " + option + " requires a value");
}
}
private boolean isClientSpecified() {
return cid != null || cclientid != null;
}
@@ -304,13 +266,11 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd {
@Override
protected boolean nothingToDo() {
return noOptions() && uusername == null && uid == null && cclientid == null && (args == null || args.size() == 0);
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help remove-roles' for more information";
return super.nothingToDo() && uusername == null && uid == null && cclientid == null
&& roleIds.isEmpty() && roleNames.isEmpty();
}
@Override
protected String help() {
return usage();
}

View File

@@ -16,16 +16,14 @@
*/
package org.keycloak.client.admin.cli.commands;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.cl.Option;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.keycloak.client.admin.cli.config.ConfigData;
import java.io.PrintWriter;
import java.io.StringWriter;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import static org.keycloak.client.admin.cli.operations.UserOperations.getIdFromUsername;
import static org.keycloak.client.admin.cli.operations.UserOperations.resetUserPassword;
import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken;
@@ -34,52 +32,28 @@ import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable
import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
import static org.keycloak.client.admin.cli.util.IoUtil.readSecret;
import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "set-password", description = "[ARGUMENTS]")
@Command(name = "set-password", description = "[ARGUMENTS]")
public class SetPasswordCmd extends AbstractAuthOptionsCmd {
@Option(name = "username", description = "Username")
@Option(names = "--username", description = "Username")
String username;
@Option(name = "userid", description = "User ID")
@Option(names = "--userid", description = "User ID")
String userid;
@Option(shortName = 'p', name = "new-password", description = "New password")
@Option(names = {"-p", "--new-password"}, description = "New password")
String pass;
@Option(shortName = 't', name = "temporary", description = "is password temporary", hasValue = false)
@Option(names = {"-t", "--temporary"}, description = "is password temporary")
boolean temporary;
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
try {
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
}
processGlobalOptions();
return process(commandInvocation);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
} finally {
commandInvocation.stop();
}
}
public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
if (args != null && args.size() > 0) {
throw new IllegalArgumentException("Invalid option: " + args.get(0));
}
protected void process() {
if (userid == null && username == null) {
throw new IllegalArgumentException("No user specified. Use --username or --userid to specify user");
}
@@ -89,17 +63,17 @@ public class SetPasswordCmd extends AbstractAuthOptionsCmd {
}
if (pass == null) {
pass = readSecret("Enter password: ", commandInvocation);
pass = readSecret("Enter password: ");
}
ConfigData config = loadConfig();
config = copyWithServerInfo(config);
setupTruststore(config, commandInvocation);
setupTruststore(config);
String auth = null;
config = ensureAuthInfo(config, commandInvocation);
config = ensureAuthInfo(config);
config = copyWithServerInfo(config);
if (credentialsAvailable(config)) {
auth = ensureToken(config);
@@ -117,20 +91,14 @@ public class SetPasswordCmd extends AbstractAuthOptionsCmd {
}
resetUserPassword(adminRoot, realm, auth, userid, pass, temporary);
return CommandResult.SUCCESS;
}
@Override
protected boolean nothingToDo() {
return noOptions() && username == null && userid == null && pass == null;
return super.nothingToDo() && username == null && userid == null && pass == null;
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help set-password' for more information";
}
@Override
protected String help() {
return usage();
}

View File

@@ -17,77 +17,68 @@
package org.keycloak.client.admin.cli.commands;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.cl.Option;
import java.io.PrintWriter;
import java.io.StringWriter;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
import static org.keycloak.client.admin.cli.util.OsUtil.OS_ARCH;
import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "update", description = "CLIENT_ID [ARGUMENTS]")
@Command(name = "update", description = "CLIENT_ID [ARGUMENTS]")
public class UpdateCmd extends AbstractRequestCmd {
@Option(shortName = 'f', name = "file", description = "Read object from file or standard input if FILENAME is set to '-'")
String file;
public UpdateCmd() {
this.httpVerb = "put";
}
@Option(shortName = 'b', name = "body", description = "JSON object to be sent as-is or used as a template")
String body;
@Option(names = {"-f", "--file"}, description = "Read object from file or standard input if FILENAME is set to '-'")
public void setFile(String file) {
this.file = file;
}
@Option(shortName = 'F', name = "fields", description = "A pattern specifying which attributes of JSON response body to actually display as result - causes mismatch with Content-Length header")
String fields;
@Option(names = {"-b", "--body"}, description = "JSON object to be sent as-is or used as a template")
public void setBody(String body) {
this.body = body;
}
@Option(shortName = 'H', name = "print-headers", description = "Print response headers", hasValue = false)
boolean printHeaders;
@Option(names = {"-F", "--fields"}, description = "A pattern specifying which attributes of JSON response body to actually display as result - causes mismatch with Content-Length header")
public void setFields(String fields) {
this.fields = fields;
}
@Option(shortName = 'm', name = "merge", description = "Merge new values with existing configuration on the server - for when the default is not to merge (i.e. if --file is used)", hasValue = false)
boolean mergeMode;
@Option(names = {"-H", "--print-headers"}, description = "Print response headers")
public void setPrintHeaders(boolean printHeaders) {
this.printHeaders = printHeaders;
}
@Option(shortName = 'n', name = "no-merge", description = "Don't merge new values with existing configuration on the server - for when the default is to merge (i.e. is --set is used while --file is not used)", hasValue = false)
boolean noMerge;
@Option(names = {"-m", "--merge"}, description = "Merge new values with existing configuration on the server - for when the default is not to merge (i.e. if --file is used)")
public void setMergeMode(boolean mergeMode) {
this.mergeMode = mergeMode;
}
@Option(shortName = 'o', name = "output", description = "After update output the new client configuration", hasValue = false)
boolean outputResult;
@Option(names = {"-n", "--no-merge"}, description = "Don't merge new values with existing configuration on the server - for when the default is to merge (i.e. is --set is used while --file is not used)")
public void setNoMerge(boolean noMerge) {
this.noMerge = noMerge;
}
@Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false)
boolean compressed;
@Option(names = {"-o", "--output"}, description = "After update output the new client configuration")
public void setOutputResult(boolean outputResult) {
this.outputResult = outputResult;
}
//@GroupOption(shortName = 's', name = "set", description = "Set specific attribute to a specified value", hasValue = true)
//private List<String> attributes = new ArrayList<>();
@Override
void initOptions() {
// set options on parent
super.file = file;
super.body = body;
super.fields = fields;
super.printHeaders = printHeaders;
super.returnId = false;
super.outputResult = true;
super.compressed = compressed;
super.mergeMode = mergeMode;
super.noMerge = noMerge;
super.outputResult = outputResult;
super.httpVerb = "put";
@Option(names = {"-c", "--compressed"}, description = "Don't pretty print the output")
public void setCompressed(boolean compressed) {
this.compressed = compressed;
}
@Override
protected boolean nothingToDo() {
return noOptions() && file == null && body == null && (args == null || args.size() == 0);
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help update' for more information";
}
protected String help() {
return usage();
}

View File

@@ -69,7 +69,7 @@ public class ConfigUtil {
public static void checkServerInfo(ConfigData config) {
if (config.getServerUrl() == null) {
throw new RuntimeException("No server specified. Use --server, or '" + OsUtil.CMD + " config credentials or connection'.");
throw new RuntimeException("No server specified. Use --server, or '" + OsUtil.CMD + " config credentials'.");
}
if (config.getRealm() == null && config.getExternalToken() == null) {
throw new RuntimeException("No realm or token specified. Use --realm, --token, or '" + OsUtil.CMD + " config credentials'.");

View File

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

View File

@@ -38,7 +38,7 @@ public class ParseUtil {
// we expect = as a separator
int pos = keyval.indexOf("=");
if (pos <= 0) {
throw new RuntimeException("Invalid key=value parameter: [" + keyval + "]");
throw new IllegalArgumentException("Invalid key=value parameter: [" + keyval + "]");
}
String [] parsed = new String[2];