Merge pull request #3664 from mstruk/admin-cli

KEYCLOAK-912 Admin CLI
This commit is contained in:
Stian Thorgersen 2016-12-19 15:46:17 +01:00 committed by GitHub
commit faeff029fa
95 changed files with 10123 additions and 497 deletions

View file

@ -110,7 +110,7 @@ public class Config {
}
public static void checkGrantType(String grantType) {
if (!PASSWORD.equals(grantType) && !CLIENT_CREDENTIALS.equals(grantType)) {
if (grantType != null && !PASSWORD.equals(grantType) && !CLIENT_CREDENTIALS.equals(grantType)) {
throw new IllegalArgumentException("Unsupported grantType: " + grantType +
" (only " + PASSWORD + " and " + CLIENT_CREDENTIALS + " are supported)");
}

View file

@ -43,18 +43,22 @@ import static org.keycloak.OAuth2Constants.PASSWORD;
public class Keycloak {
private final Config config;
private final TokenManager tokenManager;
private String authToken;
private final ResteasyWebTarget target;
private final ResteasyClient client;
Keycloak(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, String grantType, ResteasyClient resteasyClient) {
Keycloak(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, String grantType, ResteasyClient resteasyClient, String authtoken) {
config = new Config(serverUrl, realm, username, password, clientId, clientSecret, grantType);
client = resteasyClient != null ? resteasyClient : new ResteasyClientBuilder().connectionPoolSize(10).build();
tokenManager = new TokenManager(config, client);
authToken = authtoken;
tokenManager = authtoken == null ? new TokenManager(config, client) : null;
target = client.target(config.getServerUrl());
target.register(newAuthFilter());
}
target.register(new BearerAuthFilter(tokenManager));
private BearerAuthFilter newAuthFilter() {
return authToken != null ? new BearerAuthFilter(authToken) : new BearerAuthFilter(tokenManager);
}
public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, SSLContext sslContext) {
@ -63,15 +67,19 @@ public class Keycloak {
.hostnameVerification(ResteasyClientBuilder.HostnameVerificationPolicy.WILDCARD)
.connectionPoolSize(10).build();
return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, client);
return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, client, null);
}
public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret) {
return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, null);
return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, null, null);
}
public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId) {
return new Keycloak(serverUrl, realm, username, password, clientId, null, PASSWORD, null);
return new Keycloak(serverUrl, realm, username, password, clientId, null, PASSWORD, null, null);
}
public static Keycloak getInstance(String serverUrl, String realm, String clientId, String authtoken) {
return new Keycloak(serverUrl, realm, null, null, clientId, null, PASSWORD, null, null);
}
public RealmsResource realms() {
@ -100,7 +108,7 @@ public class Keycloak {
* @return
*/
public <T> T proxy(Class<T> proxyClass, URI absoluteURI) {
return client.target(absoluteURI).register(new BearerAuthFilter(tokenManager)).proxy(proxyClass);
return client.target(absoluteURI).register(newAuthFilter()).proxy(proxyClass);
}
/**

View file

@ -60,8 +60,9 @@ public class KeycloakBuilder {
private String password;
private String clientId;
private String clientSecret;
private String grantType = PASSWORD;
private String grantType;
private ResteasyClient resteasyClient;
private String authorization;
public KeycloakBuilder serverUrl(String serverUrl) {
this.serverUrl = serverUrl;
@ -104,6 +105,11 @@ public class KeycloakBuilder {
return this;
}
public KeycloakBuilder authorization(String auth) {
this.authorization = auth;
return this;
}
/**
* Builds a new Keycloak client from this builder.
*/
@ -116,6 +122,10 @@ public class KeycloakBuilder {
throw new IllegalStateException("realm required");
}
if (authorization == null && grantType == null) {
grantType = PASSWORD;
}
if (PASSWORD.equals(grantType)) {
if (username == null) {
throw new IllegalStateException("username required");
@ -130,11 +140,11 @@ public class KeycloakBuilder {
}
}
if (clientId == null) {
if (authorization == null && clientId == null) {
throw new IllegalStateException("clientId required");
}
return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, grantType, resteasyClient);
return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, grantType, resteasyClient, authorization);
}
private KeycloakBuilder() {

View file

@ -49,8 +49,10 @@ public class BearerAuthFilter implements ClientRequestFilter, ClientResponseFilt
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
String authHeader = AUTH_HEADER_PREFIX + (tokenManager != null ? tokenManager.getAccessTokenString() : tokenString);
String authHeader = (tokenManager != null ? tokenManager.getAccessTokenString() : tokenString);
if (!authHeader.startsWith(AUTH_HEADER_PREFIX)) {
authHeader = AUTH_HEADER_PREFIX + authHeader;
}
requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader);
}

View file

@ -0,0 +1,177 @@
<?xml version="1.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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-client-cli-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>2.5.0.Final-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-admin-cli</artifactId>
<name>Keycloak Admin CLI</name>
<description/>
<dependencies>
<dependency>
<groupId>org.jboss.aesh</groupId>
<artifactId>aesh</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<!--version>2.4.3</version-->
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<filters>
<filter>
<artifact>org.keycloak:keycloak-core</artifact>
<includes>
<include>org/keycloak/util/**</include>
<include>org/keycloak/json/**</include>
<include>org/keycloak/jose/jws/**</include>
<include>org/keycloak/jose/jwk/**</include>
<include>org/keycloak/representations/adapters/config/**</include>
<include>org/keycloak/representations/adapters/action/**</include>
<include>org/keycloak/representations/AccessTokenResponse.class</include>
<!--
<include>org/keycloak/representations/idm/ClientRepresentation.class</include>
<include>org/keycloak/representations/idm/RealmRepresentation.class</include>
<include>org/keycloak/representations/idm/UserRepresentation.class</include>
<include>org/keycloak/representations/idm/RoleRepresentation.class</include>
<include>org/keycloak/representations/idm/RoleRepresentation.class</include>
<include>org/keycloak/representations/idm/RolesRepresentation.class</include>
<include>org/keycloak/representations/idm/ScopeMappingRepresentation.class</include>
<include>org/keycloak/representations/idm/UserFederationMapperRepresentation.class</include>
<include>org/keycloak/representations/idm/ProtocolMapperRepresentation.class</include>
<include>org/keycloak/representations/idm/IdentityProviderRepresentation.class</include>
<include>org/keycloak/representations/idm/authorization/**</include>
-->
<include>org/keycloak/representations/idm/**</include>
<include>org/keycloak/representations/JsonWebToken.class</include>
</includes>
</filter>
<filter>
<artifact>org.keycloak:keycloak-common</artifact>
<includes>
<include>org/keycloak/common/util/**</include>
</includes>
</filter>
<filter>
<artifact>org.bouncycastle:bcprov-jdk15on</artifact>
<includes>
<include>**/**</include>
</includes>
</filter>
<filter>
<artifact>org.bouncycastle:bcpkix-jdk15on</artifact>
<excludes>
<exclude>**/**</exclude>
</excludes>
</filter>
<filter>
<artifact>com.fasterxml.jackson.core:jackson-core</artifact>
<includes>
<include>**/**</include>
</includes>
</filter>
<filter>
<artifact>com.fasterxml.jackson.core:jackson-databind</artifact>
<includes>
<include>**/**</include>
</includes>
</filter>
<filter>
<artifact>com.fasterxml.jackson.core:jackson-annotations</artifact>
<includes>
<include>com/fasterxml/jackson/annotation/**</include>
</includes>
</filter>
<filter>
<artifact>org.jboss.resteasy:resteasy-client</artifact>
<includes>
<include>**/**</include>
</includes>
</filter>
<filter>
<artifact>org.jboss.resteasy:resteasy-jaxrs</artifact>
<includes>
<include>**/**</include>
</includes>
</filter>
<filter>
<artifact>org.jboss.resteasy:resteasy-jackson2-provider</artifact>
<includes>
<include>**/**</include>
</includes>
</filter>
<filter>
<artifact>org.jboss.spec.javax.ws.rs:jboss-jaxrs-api_2.0_spec</artifact>
<includes>
<include>**/**</include>
</includes>
</filter>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,8 @@
@echo off
if "%OS%" == "Windows_NT" (
set "DIRNAME=%~dp0%"
) else (
set DIRNAME=.\
)
java %KC_OPTS% -cp %DIRNAME%\client\keycloak-admin-cli-${project.version}.jar org.keycloak.client.admin.cli.KcAdmMain %*

View file

@ -0,0 +1,23 @@
#!/bin/sh
case "`uname`" in
CYGWIN*)
CFILE = `cygpath "$0"`
RESOLVED_NAME=`readlink -f "$CFILE"`
;;
Darwin*)
RESOLVED_NAME=`readlink "$0"`
;;
FreeBSD)
RESOLVED_NAME=`readlink -f "$0"`
;;
Linux)
RESOLVED_NAME=`readlink -f "$0"`
;;
esac
if [ "x$RESOLVED_NAME" = "x" ]; then
RESOLVED_NAME="$0"
fi
DIRNAME=`dirname "$RESOLVED_NAME"`
java $KC_OPTS -cp $DIRNAME/client/keycloak-admin-cli-${project.version}.jar org.keycloak.client.admin.cli.KcAdmMain "$@"

View file

@ -0,0 +1,94 @@
/*
* 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;
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 java.util.ArrayList;
import java.util.Arrays;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class KcAdmMain {
public static void main(String [] args) {
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
boolean needQuote = false;
needQuote = s.indexOf(' ') != -1 || s.indexOf('\"') != -1 || s.indexOf('\'') != -1;
b.append(' ');
if (needQuote) {
b.append('\'');
}
b.append(s);
if (needQuote) {
b.append('\'');
}
}
console.setEcho(false);
console.execute("kcadm" + b.toString());
console.start();
}
}

View file

@ -0,0 +1,117 @@
/*
* 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) {
System.err.println("Unknown command: " + aeshLine.getWords().get(0));
} else {
System.err.println(e.getMessage());
}
if (Globals.dumpTrace) {
e.printStackTrace();
}
System.exit(1);
}
}
// empty line
else if (output != null) {
result = CommandResult.FAILURE;
}
else {
//stop();
result = CommandResult.FAILURE;
}
if (result == CommandResult.SUCCESS) {
return 0;
} else {
return 1;
}
}
private CommandContainer getCommand(ConsoleOperation output, AeshLine aeshLine) throws CommandNotFoundException {
Method m;
try {
m = console.getClass().getDeclaredMethod("getCommand", AeshLine.class, String.class);
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unexpected error: ", e);
}
m.setAccessible(true);
try {
return (CommandContainer) m.invoke(console, aeshLine, output.getBuffer());
} catch (Exception e) {
throw new RuntimeException("Unexpected error: ", e);
}
}
}

View file

@ -0,0 +1,41 @@
/*
* 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

@ -0,0 +1,31 @@
/*
* 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 java.util.List;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class Globals {
public static boolean dumpTrace = false;
public static ValveInputStream stdin;
public static List<String> args;
}

View file

@ -0,0 +1,89 @@
/*
* 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

@ -0,0 +1,267 @@
/*
* 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.commands;
import org.jboss.aesh.cl.Option;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.keycloak.client.admin.cli.config.ConfigData;
import org.keycloak.client.admin.cli.config.ConfigHandler;
import org.keycloak.client.admin.cli.config.FileConfigHandler;
import org.keycloak.client.admin.cli.config.InMemoryConfigHandler;
import org.keycloak.client.admin.cli.config.RealmConfigData;
import org.keycloak.client.admin.cli.util.ConfigUtil;
import org.keycloak.client.admin.cli.util.HttpUtil;
import org.keycloak.client.admin.cli.util.IoUtil;
import java.io.File;
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;
import static org.keycloak.client.admin.cli.util.ConfigUtil.checkServerInfo;
import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
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/auth/admin")
String adminRestRoot;
@Option(name = "config", description = "Path to the config file (~/.keycloak/kcadm.config by default)")
String config;
@Option(name = "no-config", description = "No configuration file should be used, no authentication info should be saved", hasValue = false)
boolean noconfig;
@Option(name = "server", description = "Server endpoint url (e.g. 'http://localhost:8080/auth')")
String server;
@Option(shortName = 'r', name = "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")
String realm;
@Option(name = "client", description = "Realm name to authenticate against")
String clientId;
@Option(name = "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)")
String password;
@Option(name = "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")
String keystore;
@Option(name = "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)")
String keyPass;
@Option(name = "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")
String trustStore;
@Option(name = "trustpass", description = "Truststore password (prompted for if not specified and --truststore is used)")
String trustPass;
protected void initFromParent(AbstractAuthOptionsCmd parent) {
super.initFromParent(parent);
noconfig = parent.noconfig;
config = parent.config;
server = parent.server;
realm = parent.realm;
clientId = parent.clientId;
user = parent.user;
password = parent.password;
secret = parent.secret;
keystore = parent.keystore;
storePass = parent.storePass;
keyPass = parent.keyPass;
alias = parent.alias;
trustStore = parent.trustStore;
trustPass = parent.trustPass;
}
protected void applyDefaultOptionValues() {
if (clientId == null) {
clientId = DEFAULT_CLIENT;
}
}
protected boolean noOptions() {
return 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);
}
protected String getTargetRealm(ConfigData config) {
return targetRealm != null ? targetRealm : config.getRealm();
}
protected void processGlobalOptions() {
super.processGlobalOptions();
if (config != null && noconfig) {
throw new RuntimeException("Options --config and --no-config are mutually exclusive");
}
if (!noconfig) {
setConfigFile(config != null ? config : ConfigUtil.DEFAULT_CONFIG_FILE_PATH);
ConfigUtil.setHandler(new FileConfigHandler());
} else {
InMemoryConfigHandler handler = new InMemoryConfigHandler();
ConfigData data = new ConfigData();
initConfigData(data);
handler.setConfigData(data);
ConfigUtil.setHandler(handler);
}
}
protected void setupTruststore(ConfigData configData, CommandInvocation invocation ) {
if (!configData.getServerUrl().startsWith("https:")) {
return;
}
String truststore = trustStore;
if (truststore == null) {
truststore = configData.getTruststore();
}
if (truststore != null) {
String pass = trustPass;
if (pass == null) {
pass = configData.getTrustpass();
}
if (pass == null) {
pass = IoUtil.readSecret("Enter truststore password: ", invocation);
}
try {
HttpUtil.setTruststore(new File(truststore), pass);
} catch (Exception e) {
throw new RuntimeException("Failed to load truststore: " + truststore, e);
}
}
}
protected ConfigData ensureAuthInfo(ConfigData config, CommandInvocation commandInvocation) {
if (requiresLogin()) {
// make sure current handler is in-memory handler
// restore it at the end
ConfigHandler old = ConfigUtil.getHandler();
try {
// make sure all defaults are initialized after this point
applyDefaultOptionValues();
initConfigData(config);
ConfigUtil.setupInMemoryHandler(config);
ConfigCredentialsCmd login = new ConfigCredentialsCmd();
login.initFromParent(this);
login.init(config);
login.process(commandInvocation);
// this must be executed before finally block which restores config handler
return loadConfig();
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
ConfigUtil.setHandler(old);
}
} else {
checkAuthInfo(config);
// make sure all defaults are initialized after this point
applyDefaultOptionValues();
return loadConfig();
}
}
protected boolean requiresLogin() {
return user != null || password != null || secret != null || keystore != null
|| keyPass != null || storePass != null || alias != null;
}
protected ConfigData copyWithServerInfo(ConfigData config) {
ConfigData result = config.deepcopy();
if (server != null) {
result.setServerUrl(server);
}
if (realm != null) {
result.setRealm(realm);
}
checkServerInfo(result);
return result;
}
private void initConfigData(ConfigData data) {
if (server != null)
data.setServerUrl(server);
if (realm != null)
data.setRealm(realm);
if (trustStore != null)
data.setTruststore(trustStore);
RealmConfigData rdata = data.sessionRealmConfigData();
if (clientId != null)
rdata.setClientId(clientId);
if (secret != null)
rdata.setSecret(secret);
}
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 RuntimeException("Unsupported option: " + name);
}
}
}
}

View file

@ -0,0 +1,113 @@
/*
* 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.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.util.FilterUtil;
import org.keycloak.client.admin.cli.util.ReturnFields;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
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 {
@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;
}
protected void processGlobalOptions() {
Globals.dumpTrace = dumpTrace;
}
protected boolean printHelp() {
if (help || nothingToDo()) {
printOut(help());
return true;
}
return false;
}
protected boolean nothingToDo() {
return false;
}
protected String help() {
return KcAdmCmd.usage();
}
protected String composeAdminRoot(String server) {
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")) {
type = type.substring(0, type.length()-1);
}
return type;
}
protected String extractLastComponentOfUri(String resourceUrl) {
int endPos = resourceUrl.endsWith("/") ? resourceUrl.length()-2 : resourceUrl.length()-1;
int pos = resourceUrl.lastIndexOf("/", endPos);
pos = pos == -1 ? 0 : pos;
return resourceUrl.substring(pos+1, endPos+1);
}
protected JsonNode applyFieldFilter(ObjectMapper mapper, JsonNode rootNode, ReturnFields returnFields) {
// construct new JsonNode that satisfies filtering specified by returnFields
try {
return FilterUtil.copyFilteredObject(rootNode, returnFields);
} catch (IOException e) {
throw new RuntimeException("Failed to apply fields filter", e);
}
}
}

View file

@ -0,0 +1,435 @@
/*
* 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.commands;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
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;
import org.keycloak.client.admin.cli.util.AccessibleBufferOutputStream;
import org.keycloak.client.admin.cli.util.Header;
import org.keycloak.client.admin.cli.util.Headers;
import org.keycloak.client.admin.cli.util.HeadersBody;
import org.keycloak.client.admin.cli.util.HeadersBodyStatus;
import org.keycloak.client.admin.cli.util.HttpUtil;
import org.keycloak.client.admin.cli.util.OutputFormat;
import org.keycloak.client.admin.cli.util.ReflectionUtil;
import org.keycloak.client.admin.cli.util.ReturnFields;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
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 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;
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.HttpUtil.checkSuccess;
import static org.keycloak.client.admin.cli.util.HttpUtil.composeResourceUrl;
import static org.keycloak.client.admin.cli.util.HttpUtil.doGet;
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.IoUtil.printOut;
import static org.keycloak.client.admin.cli.util.OutputUtil.MAPPER;
import static org.keycloak.client.admin.cli.util.OutputUtil.printAsCsv;
import static org.keycloak.client.admin.cli.util.ParseUtil.mergeAttributes;
import static org.keycloak.client.admin.cli.util.ParseUtil.parseFileOrStdin;
import static org.keycloak.client.admin.cli.util.ParseUtil.parseKeyVal;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd {
String file;
String fields;
boolean printHeaders;
boolean returnId;
boolean outputResult;
boolean compressed;
boolean unquoted;
boolean mergeMode;
boolean noMerge;
Integer offset;
Integer limit;
String format = "json";
OutputFormat outputFormat;
String httpVerb;
Headers headers = new Headers();
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();
}
}
abstract void initOptions();
abstract String suggestHelp();
void processOptions(CommandInvocation commandInvocation) {
if (args == null || args.isEmpty()) {
throw new IllegalArgumentException("URI not specified");
}
Iterator<String> it = args.iterator();
while (it.hasNext()) {
String option = it.next();
switch (option) {
case "-s":
case "--set": {
if (!it.hasNext()) {
throw new IllegalArgumentException("Option " + option + " requires a value");
}
String[] keyVal = parseKeyVal(it.next());
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);
}
}
}
}
if (url == null) {
throw new IllegalArgumentException("Resource URI not specified");
}
if (outputResult && returnId) {
throw new IllegalArgumentException("Options -o and -i are mutually exclusive");
}
try {
outputFormat = OutputFormat.valueOf(format.toUpperCase());
} catch (Exception e) {
throw new RuntimeException("Unsupported output format: " + format);
}
if (mergeMode && noMerge) {
throw new IllegalArgumentException("Options --merge and --no-merge are mutually exclusive");
}
if (file == null && attrs.size() > 0 && !noMerge) {
mergeMode = true;
}
}
public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
// see if Content-Type header is explicitly set to non-json value
Header ctype = headers.get("content-type");
InputStream body = null;
CmdStdinContext<JsonNode> ctx = new CmdStdinContext<>();
if (file != null) {
if (ctype != null && !"application/json".equals(ctype.getValue())) {
if ("-".equals(file)) {
body = System.in;
} else {
try {
body = new BufferedInputStream(new FileInputStream(file));
} catch (FileNotFoundException e) {
throw new RuntimeException("File not found: " + file);
}
}
} else {
ctx = parseFileOrStdin(file);
}
}
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;
if (auth != null) {
headers.addIfMissing("Authorization", auth);
}
final String server = config.getServerUrl();
final String realm = getTargetRealm(config);
final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server);
String resourceUrl = composeResourceUrl(adminRoot, realm, url);
String typeName = extractTypeNameFromUri(resourceUrl);
if (filter.size() > 0) {
resourceUrl = HttpUtil.addQueryParamsToUri(resourceUrl, filter);
}
headers.addIfMissing("Accept", "application/json");
if (isUpdate() && mergeMode) {
ObjectNode result;
HeadersBodyStatus response;
try {
response = HttpUtil.doGet(resourceUrl, new HeadersBody(headers));
checkSuccess(resourceUrl, response);
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
copyStream(response.getBody(), buffer);
result = MAPPER.readValue(buffer.toByteArray(), ObjectNode.class);
} catch (IOException e) {
throw new RuntimeException("HTTP request error: " + e.getMessage(), e);
}
CmdStdinContext<JsonNode> ctxremote = new CmdStdinContext<>();
ctxremote.setResult(result);
// merge local representation over remote one
if (ctx.getResult() != null) {
ReflectionUtil.merge(ctx.getResult(), (ObjectNode) ctxremote.getResult());
}
ctx = ctxremote;
}
if (attrs.size() > 0) {
if (body != null) {
throw new RuntimeException("Can't set attributes on content of type other than application/json");
}
ctx = mergeAttributes(ctx, MAPPER.createObjectNode(), attrs);
}
if (body == null && ctx.getContent() != null) {
body = new ByteArrayInputStream(ctx.getContent().getBytes(Charset.forName("utf-8")));
}
ReturnFields returnFields = null;
if (fields != null) {
returnFields = new ReturnFields(fields);
}
// make sure content type is set
if (body != null) {
headers.addIfMissing("Content-Type", "application/json");
}
LinkedHashMap<String, String> queryParams = new LinkedHashMap<>();
if (offset != null) {
queryParams.put("first", String.valueOf(offset));
}
if (limit != null) {
queryParams.put("max", String.valueOf(limit));
}
if (queryParams.size() > 0) {
resourceUrl = HttpUtil.addQueryParamsToUri(resourceUrl, queryParams);
}
HeadersBodyStatus response;
try {
response = HttpUtil.doRequest(httpVerb, resourceUrl, new HeadersBody(headers, body));
} catch (IOException e) {
throw new RuntimeException("HTTP request error: " + e.getMessage(), e);
}
// output response
if (printHeaders) {
printOut(response.getStatus());
for (Header header : response.getHeaders()) {
printOut(header.getName() + ": " + header.getValue());
}
}
checkSuccess(resourceUrl, response);
AccessibleBufferOutputStream abos = new AccessibleBufferOutputStream(System.out);
if (response.getBody() == null) {
throw new RuntimeException("Internal error - response body should never be null");
}
if (printHeaders) {
printOut("");
}
Header location = response.getHeaders().get("Location");
String id = location != null ? extractLastComponentOfUri(location.getValue()) : null;
if (id != null) {
if (returnId) {
printOut(id);
} else if (!outputResult) {
printErr("Created new " + typeName + " with id '" + id + "'");
}
}
if (outputResult) {
if (isCreateOrUpdate() && (response.getStatusCode() == 204 || id != null)) {
// get object for id
headers = new Headers();
if (auth != null) {
headers.add("Authorization", auth);
}
try {
String fetchUrl = id != null ? (resourceUrl + "/" + id) : resourceUrl;
response = doGet(fetchUrl, new HeadersBody(headers));
} catch (IOException e) {
throw new RuntimeException("HTTP request error: " + e.getMessage(), e);
}
}
Header contentType = response.getHeaders().get("content-type");
boolean canPrettyPrint = contentType != null && contentType.getValue().equals("application/json");
boolean pretty = !compressed;
if (canPrettyPrint && (pretty || returnFields != null)) {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
copyStream(response.getBody(), buffer);
try {
JsonNode rootNode = MAPPER.readValue(buffer.toByteArray(), JsonNode.class);
if (returnFields != null) {
rootNode = applyFieldFilter(MAPPER, rootNode, returnFields);
}
if (outputFormat == OutputFormat.JSON) {
// now pretty print it to output
MAPPER.writeValue(abos, rootNode);
} else {
printAsCsv(rootNode, returnFields, unquoted);
}
} catch (Exception ignored) {
copyStream(new ByteArrayInputStream(buffer.toByteArray()), abos);
}
} else {
copyStream(response.getBody(), abos);
}
}
int lastByte = abos.getLastByte();
if (lastByte != -1 && lastByte != 13 && lastByte != 10) {
printErr("");
}
return CommandResult.SUCCESS;
}
private boolean isUpdate() {
return "put".equals(httpVerb);
}
private boolean isCreateOrUpdate() {
return "post".equals(httpVerb) || "put".equals(httpVerb);
}
}

View file

@ -0,0 +1,334 @@
/*
* 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.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.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 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 = "add-roles", description = "[ARGUMENTS]")
public class AddRolesCmd extends AbstractAuthOptionsCmd {
@Option(name = "uusername", description = "Target user's 'username'")
String uusername;
@Option(name = "uid", description = "Target user's 'id'")
String uid;
@Option(name = "gname", description = "Target group's 'name'")
String gname;
@Option(name = "gpath", description = "Target group's 'path'")
String gpath;
@Option(name = "gid", description = "Target group's 'id'")
String gid;
@Option(name = "cclientid", description = "Target client's 'clientId'")
String cclientid;
@Option(name = "cid", description = "Target client's 'id'")
String cid;
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
List<String> roleNames = new LinkedList<>();
List<String> roleIds = new LinkedList<>();
try {
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
}
processGlobalOptions();
Iterator<String> it = args.iterator();
while (it.hasNext()) {
String option = it.next();
switch (option) {
case "--rolename": {
optionRequiresValueCheck(it, option);
roleNames.add(it.next());
break;
}
case "--roleid": {
optionRequiresValueCheck(it, option);
roleIds.add(it.next());
break;
}
default: {
throw new IllegalArgumentException("Invalid option: " + option);
}
}
}
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 specified. Use --rolename or --roleid to specify roles");
}
if (cid != null && cclientid != null) {
throw new IllegalArgumentException("Incompatible options: --cid and --cclientid 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() && !isGroupSpecified()) {
throw new IllegalArgumentException("No user nor group specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group");
}
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));
}
} 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));
// 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 {
throw new IllegalArgumentException("No user nor group specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group");
}
return CommandResult.SUCCESS;
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
} finally {
commandInvocation.stop();
}
}
private Set<ObjectNode> getRoleRepresentations(List<String> roleNames, List<String> roleIds, LocalSearch roleSearch) {
Set<ObjectNode> rolesToAdd = new HashSet<>();
// now we process roles
for (String name : roleNames) {
ObjectNode r = roleSearch.exactMatchOne(name, "name");
if (r == null) {
throw new RuntimeException("Role not found for name: " + name);
}
rolesToAdd.add(r);
}
for (String id : roleIds) {
ObjectNode r = roleSearch.exactMatchOne(id, "id");
if (r == null) {
throw new RuntimeException("Role not found for id: " + id);
}
rolesToAdd.add(r);
}
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;
}
private boolean isGroupSpecified() {
return gid != null || gname != null || gpath != null;
}
private boolean isUserSpecified() {
return uid != null || uusername != null;
}
@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";
}
protected String help() {
return usage();
}
public static String usage() {
StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
out.println("Usage: " + CMD + " add-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]");
out.println("Usage: " + CMD + " add-roles (--gname NAME | --gpath PATH | --gid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]");
out.println();
out.println("Command to add realm or client roles to a user or group.");
out.println();
out.println("Use `" + CMD + " config credentials` to establish an authenticated session, or use CREDENTIALS OPTIONS");
out.println("to perform one time authentication.");
out.println();
out.println("If client is specified using --cclientid or --cid then roles to add are client roles, otherwise they are realm roles.");
out.println("Either a user, or a group needs to be specified. If user is specified using --uusername or --uid then roles are added");
out.println("to a specific user. If group is specified using --gname, --gpath or --gid then roles are added to a specific group.");
out.println("One or more roles have to be specified using --rolename or --roleid so that they are added to a group or a user.");
out.println();
out.println("Arguments:");
out.println();
out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error");
out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --truststore PATH Path to a truststore containing trusted certificates");
out.println(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");
out.println(" CREDENTIALS OPTIONS Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
out.println(" an authenticated sessions. This allows on-the-fly transient authentication that does");
out.println(" not touch a config file.");
out.println();
out.println(" Command specific options:");
out.println(" --uusername User's 'username'. If more than one user exists with the same username");
out.println(" you'll have to use --uid to specify the target user");
out.println(" --uid User's 'id' attribute");
out.println(" --gname Group's 'name'. If more than one group exists with the same name you'll have");
out.println(" to use --gid, or --gpath to specify the target group");
out.println(" --gpath Group's 'path' attribute");
out.println(" --gid Group's 'id' attribute");
out.println(" --cclientid Client's 'clientId' attribute");
out.println(" --cid Client's 'id' attribute");
out.println(" --rolename Role's 'name' attribute");
out.println(" --roleid Role's 'id' attribute");
out.println(" -a, --admin-root URL URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin");
out.println(" -r, --target-realm REALM Target realm to issue requests against if not the one authenticated against");
out.println();
out.println("Examples:");
out.println();
out.println("Add 'offline_access' realm role to a user:");
out.println(" " + PROMPT + " " + CMD + " add-roles -r demorealm --uusername testuser --rolename offline_access");
out.println();
out.println("Add 'realm-management' client roles 'view-users', 'view-clients' and 'view-realm' to a user:");
out.println(" " + PROMPT + " " + CMD + " add-roles -r demorealm --uusername testuser --cclientid realm-management --rolename view-users --rolename view-clients --rolename view-realm");
out.println();
out.println("Add 'uma_authorization' realm role to a group:");
out.println(" " + PROMPT + " " + CMD + " add-roles -r demorealm --gname PowerUsers --rolename uma_authorization");
out.println();
out.println("Add 'realm-management' client roles 'realm-admin' to a group:");
out.println(" " + PROMPT + " " + CMD + " add-roles -r demorealm --gname PowerUsers --cclientid realm-management --rolename realm-admin");
out.println();
out.println();
out.println("Use '" + CMD + " help' for general information and a list of commands");
return sb.toString();
}
}

View file

@ -0,0 +1,95 @@
/*
* 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.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 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} )
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());
}
}
}
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";
}
protected String help() {
return usage();
}
public static String usage() {
StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
out.println("Usage: " + CMD + " config SUB_COMMAND [ARGUMENTS]");
out.println();
out.println("Where SUB_COMMAND is one of: 'credentials', 'truststore'");
out.println();
out.println();
out.println("Use '" + CMD + " help config SUB_COMMAND' for more info.");
out.println("Use '" + CMD + " help' for general information and a list of commands.");
return sb.toString();
}
}

View file

@ -0,0 +1,275 @@
/*
* 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.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.client.admin.cli.config.ConfigData;
import org.keycloak.client.admin.cli.config.RealmConfigData;
import org.keycloak.client.admin.cli.util.AuthUtil;
import org.keycloak.representations.AccessTokenResponse;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
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;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
import static org.keycloak.client.admin.cli.util.ConfigUtil.getHandler;
import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
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;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "credentials", description = "--server SERVER_URL --realm REALM [ARGUMENTS]")
public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd {
private int sigLifetime = 600;
public void init(ConfigData configData) {
if (server == null) {
server = configData.getServerUrl();
}
if (realm == null) {
realm = configData.getRealm();
}
if (trustStore == null) {
trustStore = configData.getTruststore();
}
RealmConfigData rdata = configData.getRealmConfigData(server, realm);
if (rdata == null) {
return;
}
if (clientId == null) {
clientId = rdata.getClientId();
}
}
@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();
}
}
@Override
protected boolean nothingToDo() {
return noOptions();
}
public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
// check server
if (server == null) {
throw new IllegalArgumentException("Required option not specified: --server");
}
try {
new URL(server);
} catch (Exception e) {
throw new RuntimeException("Invalid server endpoint url: " + server, e);
}
if (realm == null)
throw new IllegalArgumentException("Required option not specified: --realm");
String signedRequestToken = null;
boolean clientSet = clientId != null;
applyDefaultOptionValues();
if (user != null) {
printErr("Logging into " + server + " as user " + user + " of realm " + realm);
// if user was set there needs to be a password so we can authenticate
if (password == null) {
password = readSecret("Enter password: ", commandInvocation);
}
// if secret was set to be read from stdin, then ask for it
if ("-".equals(secret) && keystore == null) {
secret = readSecret("Enter client secret: ", commandInvocation);
}
} else if (keystore != null || secret != null || clientSet) {
printErr("Logging into " + server + " as " + "service-account-" + clientId + " of realm " + realm);
if (keystore == null) {
if (secret == null) {
secret = readSecret("Enter client secret: ", commandInvocation);
}
}
}
if (keystore != null) {
if (secret != null) {
throw new IllegalArgumentException("Can't use both --keystore and --secret");
}
if (!new File(keystore).isFile()) {
throw new RuntimeException("No such keystore file: " + keystore);
}
if (storePass == null) {
storePass = readSecret("Enter keystore password: ", commandInvocation);
keyPass = readSecret("Enter key password: ", commandInvocation);
}
if (keyPass == null) {
keyPass = storePass;
}
if (alias == null) {
alias = clientId;
}
String realmInfoUrl = server + "/realms/" + realm;
signedRequestToken = AuthUtil.getSignedRequestToken(keystore, storePass, keyPass,
alias, sigLifetime, clientId, realmInfoUrl);
}
// if only server and realm are set, just save config and be done
if (user == null && secret == null && keystore == null) {
getHandler().saveMergeConfig(config -> {
config.setServerUrl(server);
config.setRealm(realm);
});
return CommandResult.SUCCESS;
}
setupTruststore(copyWithServerInfo(loadConfig()), commandInvocation);
// now use the token endpoint to retrieve access token, and refresh token
AccessTokenResponse tokens = signedRequestToken != null ?
getAuthTokensByJWT(server, realm, user, password, clientId, signedRequestToken) :
secret != null ?
getAuthTokensBySecret(server, realm, user, password, clientId, secret) :
getAuthTokens(server, realm, user, password, clientId);
Long sigExpiresAt = signedRequestToken == null ? null : System.currentTimeMillis() + sigLifetime * 1000;
// save tokens to config file
saveTokens(tokens, server, realm, clientId, signedRequestToken, sigExpiresAt, secret);
return CommandResult.SUCCESS;
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help config credentials' for more information";
}
protected String help() {
return usage();
}
public static String usage() {
StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
out.println("Usage: " + CMD + " config credentials --server SERVER_URL --realm REALM --user USER [--password PASSWORD] [ARGUMENTS]");
out.println(" " + CMD + " config credentials --server SERVER_URL --realm REALM --client CLIENT_ID [--secret SECRET] [ARGUMENTS]");
out.println(" " + CMD + " config credentials --server SERVER_URL --realm REALM --client CLIENT_ID [--keystore KEYSTORE] [ARGUMENTS]");
out.println();
out.println("Command to establish an authenticated client session with the server. There are many authentication");
out.println("options available, and it depends on server side client authentication configuration how client can or should authenticate.");
out.println("The information always required includes --server, and --realm. Then, --user and / or --client need to be used to authenticate.");
out.println("If --client is not provided it defaults to 'admin-cli'. The authentication options / requirements depend on how this client is configured.");
out.println();
out.println("If confidential client authentication is also configured, you may have to specify a client id, and client credentials in addition to");
out.println("user credentials. Client credentials are either a client secret, or a keystore information to use Signed JWT mechanism.");
out.println("If only client credentials are provided, and no user credentials, then the service account is used for login.");
out.println();
out.println("Arguments:");
out.println();
out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error");
out.println(" --config Path to a config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --truststore PATH Path to a truststore containing trusted certificates");
out.println(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");
out.println();
out.println(" Command specific options:");
out.println(" --server SERVER_URL Server endpoint url (e.g. 'http://localhost:8080/auth')");
out.println(" --realm REALM Realm name to use");
out.println(" --user USER Username to login with");
out.println(" --password PASSWORD Password to login with (prompted for if not specified and --user is used)");
out.println(" --client CLIENT_ID ClientId used by this client tool ('admin-cli' by default)");
out.println(" --secret SECRET Secret to authenticate the client (prompted for if --client is specified, and no --keystore is specified)");
out.println(" --keystore PATH Path to a keystore containing private key");
out.println(" --storepass PASSWORD Keystore password (prompted for if not specified and --keystore is used)");
out.println(" --keypass PASSWORD Key password (prompted for if not specified and --keystore is used without --storepass,");
out.println(" otherwise defaults to keystore password)");
out.println(" --alias ALIAS Alias of the key inside a keystore (defaults to the value of ClientId)");
out.println();
out.println();
out.println("Examples:");
out.println();
out.println("Login as 'admin' user of 'master' realm to a local Keycloak server running on default port.");
out.println("You will be prompted for a password:");
out.println(" " + PROMPT + " " + CMD + " config credentials --server http://localhost:8080/auth --realm master --user admin");
out.println();
out.println("Login to Keycloak server at non-default endpoint passing the password via standard input:");
if (OS_ARCH.isWindows()) {
out.println(" " + PROMPT + " echo mypassword | " + CMD + " config credentials --server http://localhost:9080/auth --realm master --user admin");
} else {
out.println(" " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --user admin << EOF");
out.println(" mypassword");
out.println(" EOF");
}
out.println();
out.println("Login specifying a password through command line:");
out.println(" " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --user admin --password " + OS_ARCH.envVar("PASSWORD"));
out.println();
out.println("Login using a client service account of a custom client. You will be prompted for a client secret:");
out.println(" " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --client reg-cli");
out.println();
out.println("Login using a client service account of a custom client, authenticating with signed JWT.");
out.println("You will be prompted for a keystore password, and a key password:");
out.println(" " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --client reg-cli --keystore " + OS_ARCH.path("~/.keycloak/keystore.jks"));
out.println();
out.println("Login as 'user' while also authenticating a custom client with signed JWT.");
out.println("You will be prompted for a user password, a keystore password, and a key password:");
out.println(" " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --user user --client reg-cli --keystore " + OS_ARCH.path("~/.keycloak/keystore.jks"));
out.println();
out.println();
out.println("Use '" + CMD + " help' for general information and a list of commands");
return sb.toString();
}
}

View file

@ -0,0 +1,200 @@
/*
* 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.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 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]")
public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd {
private ConfigCmd parent;
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();
}
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,
"--realm", realm,
"--client", clientId,
"--user", user,
"--password", password,
"--secret", secret,
"--truststore", trustStore,
"--keystore", keystore,
"--keypass", keyPass,
"--alias", alias);
// now update the config
processGlobalOptions();
String store;
String pass;
if (!delete) {
if (truststore == null) {
throw new IllegalArgumentException("No truststore specified");
}
if (!new File(truststore).isFile()) {
throw new RuntimeException("Truststore file not found: " + truststore);
}
if ("-".equals(trustPass)) {
trustPass = readSecret("Enter truststore password: ", commandInvocation);
}
store = truststore;
pass = trustPass;
} else {
if (truststore != 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;
}
saveMergeConfig(config -> {
config.setTruststore(store);
config.setTrustpass(pass);
});
return CommandResult.SUCCESS;
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help config truststore' for more information";
}
protected String help() {
return usage();
}
public static String usage() {
StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
out.println("Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWORD] [ARGUMENTS]");
out.println();
out.println("Command to configure a global truststore to use when using https to connect to Keycloak server.");
out.println();
out.println("Arguments:");
out.println();
out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error");
out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println();
out.println(" Command specific options:");
out.println(" TRUSTSTORE Path to truststore file");
out.println(" --trustpass PASSWORD Truststore password to unlock truststore (prompted for if set to '-')");
out.println(" -d, --delete Remove truststore configuration");
out.println();
out.println();
out.println("Examples:");
out.println();
out.println("Specify a truststore - you will be prompted for truststore password every time it is used:");
out.println(" " + PROMPT + " " + CMD + " config truststore " + OS_ARCH.path("~/.keycloak/truststore.jks"));
out.println();
out.println("Specify a truststore, and password - truststore will automatically be used without prompting for password:");
out.println(" " + PROMPT + " " + CMD + " config truststore --storepass " + OS_ARCH.envVar("PASSWORD") + " " + OS_ARCH.path("~/.keycloak/truststore.jks"));
out.println();
out.println("Remove truststore configuration:");
out.println(" " + PROMPT + " " + CMD + " config truststore --delete");
out.println();
out.println();
out.println("Use '" + CMD + " help' for general information and a list of commands");
return sb.toString();
}
}

View file

@ -0,0 +1,167 @@
/*
* 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.commands;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.cl.Option;
import java.io.PrintWriter;
import java.io.StringWriter;
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")
public class CreateCmd extends AbstractRequestCmd {
@Option(shortName = 'f', name = "file", description = "Read object from file or standard input if FILENAME is set to '-'", hasValue = true)
String 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(shortName = 'H', name = "print-headers", description = "Print response headers", hasValue = false)
boolean printHeaders;
@Option(shortName = 'i', name = "id", description = "After creation only print id of created resource to standard output", hasValue = false)
boolean returnId = false;
@Option(shortName = 'o', name = "output", description = "After creation output the new resource to standard output", hasValue = false)
boolean outputResult = false;
@Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false)
boolean compressed = false;
//@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.fields = fields;
super.printHeaders = printHeaders;
super.returnId = returnId;
super.outputResult = outputResult;
super.compressed = compressed;
super.httpVerb = "post";
}
@Override
protected boolean nothingToDo() {
return noOptions() && file == null && (args == null || args.size() == 0);
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help create' for more information";
}
protected String help() {
return usage();
}
public static String usage() {
StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
out.println("Usage: " + CMD + " create ENDPOINT_URI [ARGUMENTS]");
out.println();
out.println("Command to create new resources on the server.");
out.println();
out.println("Use `" + CMD + " config credentials` to establish an authenticated sessions, or use CREDENTIALS OPTIONS");
out.println("to perform one time authentication.");
out.println();
out.println("Arguments:");
out.println();
out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error");
out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --truststore PATH Path to a truststore containing trusted certificates");
out.println(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");
out.println(" CREDENTIALS OPTIONS Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
out.println(" an authenticated sessions. This allows on-the-fly transient authentication that does");
out.println(" not touch a config file.");
out.println();
out.println(" Command specific options:");
out.println(" ENDPOINT_URI URI used to compose a target resource url. Commonly used values are:");
out.println(" realms, users, roles, groups, clients, keys, serverinfo, components ...");
out.println(" If it starts with 'http://' then it will be used as target resource url");
out.println(" -r, --target-realm REALM Target realm to issue requests against if not the one authenticated against");
out.println(" -s, --set NAME=VALUE Set a specific attribute NAME to a specified value VALUE");
out.println(" -d, --delete NAME Remove a specific attribute NAME from JSON request body");
out.println(" -f, --file FILENAME Read object from file or standard input if FILENAME is set to '-'");
out.println(" -q, --query NAME=VALUE Add to request URI a NAME query parameter with value VALUE");
out.println(" -h, --header NAME=VALUE Set request header NAME to VALUE");
out.println();
out.println(" -H, --print-headers Print response headers");
out.println(" -o, --output After creation output the new resource to standard output");
out.println(" -i, --id After creation only print id of the new resource to standard output");
out.println(" -F, --fields FILTER A filter pattern to specify which fields of a JSON response to output");
out.println(" -c, --compressed Don't pretty print the output");
out.println(" -a, --admin-root URL URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin");
out.println();
out.println();
out.println("Nested attributes are supported by using '.' to separate components of a KEY. Optionaly, the KEY components ");
out.println("can be quoted with double quotes - e.g. my_client.attributes.\"external.user.id\". If VALUE starts with [ and ");
out.println("ends with ] the attribute will be set as a JSON array. If VALUE starts with { and ends with } the attribute ");
out.println("will be set as a JSON object. If KEY ends with an array index - e.g. clients[3]=VALUE - then the specified item");
out.println("of the array is updated. If KEY+=VALUE syntax is used, then KEY is assumed to be an array, and another item is");
out.println("added to it.");
out.println();
out.println("Attributes can also be deleted. If KEY ends with an array index, then the targeted item of an array is removed");
out.println("and the following items are shifted.");
out.println();
out.println();
out.println("Examples:");
out.println();
out.println("Create a new realm:");
out.println(" " + PROMPT + " " + CMD + " create realms -s realm=demorealm -s enabled=true");
out.println();
out.println("Create a new realm role in realm 'demorealm' returning newly created role:");
out.println(" " + PROMPT + " " + CMD + " create roles -r demorealm -s name=manage-all -o");
out.println();
out.println("Create a new user in realm 'demorealm' returning only 'id', and 'username' attributes:");
out.println(" " + PROMPT + " " + CMD + " create users -r demorealm -s username=testuser -s enabled=true -o --fields id,username");
out.println();
out.println("Create a new client using configuration read from standard input:");
if (OS_ARCH.isWindows()) {
out.println(" " + PROMPT + " echo { \"clientId\": \"my_client\" } | " + CMD + " create clients -r demorealm -f -");
} else {
out.println(" " + PROMPT + " " + CMD + " create clients -r demorealm -f - << EOF");
out.println(" {");
out.println(" \"clientId\": \"my_client\"");
out.println(" }");
out.println(" EOF");
}
out.println();
out.println("Create a client using file as a template, and override some attributes - return an 'id' of new client:");
out.println(" " + PROMPT + " " + CMD + " create clients -r demorealm -f my_client.json -s clientId=my_client2 -s 'redirectUris=[\"http://localhost:8980/myapp/*\"]' -i");
out.println();
out.println("Create a new client role for client my_client in realm 'demorealm' (replace ID with output of previous example command):");
out.println(" " + PROMPT + " " + CMD + " create clients/ID/roles -r demorealm -s name=client_role");
out.println();
out.println();
out.println("Use '" + CMD + " help' for general information and a list of commands");
return sb.toString();
}
}

View file

@ -0,0 +1,104 @@
/*
* 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.commands;
import org.jboss.aesh.cl.CommandDefinition;
import java.io.PrintWriter;
import java.io.StringWriter;
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]")
public class DeleteCmd extends CreateCmd {
void initOptions() {
super.initOptions();
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();
}
public static String usage() {
StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
out.println("Usage: " + CMD + " delete ENDPOINT_URI [ARGUMENTS]");
out.println();
out.println("Command to delete resources on the server.");
out.println();
out.println("Use `" + CMD + " config credentials` to establish an authenticated sessions, or use CREDENTIALS OPTIONS");
out.println("to perform one time authentication.");
out.println();
out.println("Arguments:");
out.println();
out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error");
out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --truststore PATH Path to a truststore containing trusted certificates");
out.println(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");
out.println(" CREDENTIALS OPTIONS Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
out.println(" an authenticated sessions. This allows on-the-fly transient authentication that does");
out.println(" not touch a config file.");
out.println();
out.println(" Command specific options:");
out.println(" ENDPOINT_URI URI used to compose a target resource url. Commonly used values start with:");
out.println(" realms/, users/, roles/, groups/, clients/, keys/, components/ ...");
out.println(" If it starts with 'http://' then it will be used as target resource url");
out.println(" -r, --target-realm REALM Target realm to issue requests against if not the one authenticated against");
out.println(" -s, --set NAME=VALUE Send a body with request - set a specific attribute NAME to a specified value VALUE");
out.println(" -d, --delete NAME Remove a specific attribute NAME from JSON request body");
out.println(" -f, --file FILENAME Send a body with request - read object from file or standard input if FILENAME is set to '-'");
out.println(" -q, --query NAME=VALUE Add to request URI a NAME query parameter with value VALUE");
out.println(" -h, --header NAME=VALUE Set request header NAME to VALUE");
out.println();
out.println(" -H, --print-headers Print response headers");
out.println(" -o, --output After delete output any response to standard output");
out.println(" -F, --fields FILTER A filter pattern to specify which fields of a JSON response to output");
out.println(" -c, --compressed Don't pretty print the output");
out.println(" -a, --admin-root URL URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin");
out.println();
out.println("Examples:");
out.println();
out.println("Delete a realm role:");
out.println(" " + PROMPT + " " + CMD + " delete roles/manage-all -r demorealm");
out.println();
out.println("Delete a user (replace USER_ID with the value of user's 'id' attribute):");
out.println(" " + PROMPT + " " + CMD + " delete users/USER_ID -r demorealm");
out.println();
out.println();
out.println("Use '" + CMD + " help' for general information and a list of commands");
return sb.toString();
}
}

View file

@ -0,0 +1,168 @@
/*
* 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.commands;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.cl.Option;
import java.io.PrintWriter;
import java.io.StringWriter;
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 {
@Option(name = "noquotes", description = "", hasValue = false)
boolean unquoted;
@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(shortName = 'H', name = "print-headers", description = "Print response headers", hasValue = false)
boolean printHeaders;
@Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false)
boolean compressed;
@Option(shortName = 'o', name = "offset", description = "Number of results from beginning of resultset to skip")
Integer offset;
@Option(shortName = 'l', name = "limit", description = "Maksimum number of results to return")
Integer limit;
@Option(name = "format", description = "Output format - one of: json, csv", defaultValue = "json")
String format;
@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";
}
@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();
}
public static String usage() {
StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
out.println("Usage: " + CMD + " get ENDPOINT_URI [ARGUMENTS]");
out.println();
out.println("Command to retrieve existing resources from the server.");
out.println();
out.println("Use `" + CMD + " config credentials` to establish an authenticated session, or use CREDENTIALS OPTIONS");
out.println("to perform one time authentication.");
out.println();
out.println("Arguments:");
out.println();
out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error");
out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --truststore PATH Path to a truststore containing trusted certificates");
out.println(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");
out.println(" CREDENTIALS OPTIONS Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
out.println(" an authenticated sessions. This allows on-the-fly transient authentication that does");
out.println(" not touch a config file.");
out.println();
out.println(" Command specific options:");
out.println(" ENDPOINT_URI URI used to compose a target resource url. Commonly used values are:");
out.println(" realms, users, roles, groups, clients, keys, serverinfo, components ...");
out.println(" If it starts with 'http://' then it will be used as target resource url");
out.println(" -r, --target-realm REALM Target realm to issue requests against if not the one authenticated against");
out.println(" -q, --query NAME=VALUE Add to request URI a NAME query parameter with value VALUE");
out.println(" -h, --header NAME=VALUE Set request header NAME to VALUE");
out.println(" -o, --offset OFFSET Set paging offset - adds a query parameter 'first' which some endpoints recognize");
out.println(" -l, --limit LIMIT Set limit to number of items in result - adds a query parameter 'max' ");
out.println(" which some endpoints recognize");
out.println();
out.println(" -H, --print-headers Print response headers");
out.println(" -o, --output After delete output any response to standard output");
out.println(" -F, --fields FILTER A filter pattern to specify which fields of a JSON response to output");
out.println(" -c, --compressed Don't pretty print the output");
out.println(" --format FORMAT Set output format to comma-separated-values by using 'csv'. Default format is 'json'");
out.println(" --noquotes Don't quote strings when output format is 'csv'");
out.println(" -a, --admin-root URL URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin");
out.println();
out.println("Examples:");
out.println();
out.println("Get all realms, displaying only some of the attributes:");
out.println(" " + PROMPT + " " + CMD + " get realms --fields id,realm,enabled");
out.println();
out.println("Get 'demorealm':");
out.println(" " + PROMPT + " " + CMD + " get realms/demorealm");
out.println();
out.println("Get all configured identity providers in demorealm, displaying only some of the attributes:");
out.println(" " + PROMPT + " " + CMD + " get identity-provider/instances -r demorealm --fields alias,providerId,enabled");
out.println();
out.println("Get all clients in demorealm, displaying only some of the attributes:");
out.println(" " + PROMPT + " " + CMD + " get clients -r demorealm --fields 'id,clientId,protocolMappers(id,name,protocol,protocolMapper)'");
out.println();
out.println("Get specific client in demorealm, and remove 'id', and 'protocolMappers' attributes in order to use");
out.println("it as a template (replace ID with client's 'id'):");
out.println(" " + PROMPT + " " + CMD + " get clients/ID -r demorealm --fields '*(*),-id,-protocolMappers' > realm-template.json");
out.println();
out.println("Display first level attributes available on 'serverinfo' resource:");
out.println(" " + PROMPT + " " + CMD + " get serverinfo -r demorealm --fields '*'");
out.println();
out.println("Display system info and memory info:");
out.println(" " + PROMPT + " " + CMD + " get serverinfo -r demorealm --fields 'systemInfo(*),memoryInfo(*)'");
out.println();
out.println("Get adapter configuration for the client (replace ID with client's 'id'):");
out.println(" " + PROMPT + " " + CMD + " get clients/ID/installation/providers/keycloak-oidc-keycloak-json -r demorealm");
out.println();
out.println("Get first 100 users at the most:");
out.println(" " + PROMPT + " " + CMD + " get users -r demorealm --offset 0 --limit 100");
out.println();
out.println("Note: 'users' endpoint knows how to handle --offset and --limit. Most other endpoints don't.");
out.println();
out.println("Get all users whose 'username' matches '*test*' pattern, and 'email' matches '*@google.com*':");
out.println(" " + PROMPT + " " + CMD + " get users -r demorealm -q username=test -q email=@google.com");
out.println();
out.println("Note: it is the 'users' endpoint that interprets query parameters 'username', and 'email' in such a way that");
out.println("it results in the described semantics. Another endpoint may provide a different semantics.");
out.println();
out.println();
out.println("Use '" + CMD + " help' for general information and a list of commands");
return sb.toString();
}
}

View file

@ -0,0 +1,325 @@
/*
* 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.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;
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 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.HttpUtil.composeResourceUrl;
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>
*/
@CommandDefinition(name = "get-roles", description = "[ARGUMENTS]")
public class GetRolesCmd extends GetCmd {
@Option(name = "uusername", description = "Target user's 'username'")
String uusername;
@Option(name = "uid", description = "Target user's 'id'")
String uid;
@Option(name = "cclientid", description = "Target client's 'clientId'")
String cclientid;
@Option(name = "cid", description = "Target client's 'id'")
String cid;
@Option(name = "rolename", description = "Target role's 'name'")
String rname;
@Option(name = "roleid", description = "Target role's 'id'")
String rid;
@Option(name = "gname", description = "Target group's 'name'")
String gname;
@Option(name = "gpath", description = "Target group's 'path'")
String gpath;
@Option(name = "gid", description = "Target group's 'id'")
String gid;
@Option(name = "available", description = "List only available roles", hasValue = false)
boolean available;
@Option(name = "effective", description = "List assigned roles including transitively included roles", hasValue = false)
boolean effective;
@Option(name = "all", description = "List roles for all clients in addition to realm roles", hasValue = false)
boolean all;
void initOptions() {
super.initOptions();
// hack args so that GetCmd option check doesn't fail
// set a placeholder
if (args == null) {
args = new ArrayList();
}
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");
}
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 (rid != null && rname != null) {
throw new IllegalArgumentException("Incompatible options: --roleid and --rolename are mutually exclusive");
}
if (cid != null && cclientid != null) {
throw new IllegalArgumentException("Incompatible options: --cid and --cclientid 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");
}
super.processOptions(commandInvocation);
}
public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
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);
}
if (available) {
super.url = 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");
} else {
super.url = 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");
} else if (effective) {
super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/realm/composite");
} else {
super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/realm");
}
}
} 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);
}
if (available) {
super.url = 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");
} else {
super.url = 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");
} else if (effective) {
super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/realm/composite");
} else {
super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/realm");
}
}
} else if (isClientSpecified()) {
if (cid == null) {
cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
}
if (isRoleSpecified()) {
// get specific client role
if (rname == null) {
rname = RoleOperations.getClientRoleNameFromId(adminRoot, realm, auth, cid, rid);
}
super.url = composeResourceUrl(adminRoot, realm, "clients/" + cid + "/roles/" + rname);
} else {
// list defined client roles
super.url = composeResourceUrl(adminRoot, realm, "clients/" + cid + "/roles");
}
} else {
if (isRoleSpecified()) {
// get specific realm role
if (rname == null) {
rname = RoleOperations.getClientRoleNameFromId(adminRoot, realm, auth, cid, rid);
}
super.url = composeResourceUrl(adminRoot, realm, "roles/" + rname);
} else {
// list defined realm roles
super.url = composeResourceUrl(adminRoot, realm, "roles");
}
}
return super.process(commandInvocation);
}
private boolean isRoleSpecified() {
return rid != null || rname != null;
}
private boolean isClientSpecified() {
return cid != null || cclientid != null;
}
private boolean isGroupSpecified() {
return gid != null || gname != null || gpath != null;
}
private boolean isUserSpecified() {
return uid != null || uusername != null;
}
protected String suggestHelp() {
return "";
}
protected boolean nothingToDo() {
return false;
}
protected String help() {
return usage();
}
public static String usage() {
StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
out.println("Usage: " + CMD + " get-roles [--cclientid CLIENT_ID | --cid ID] [ARGUMENTS]");
out.println("Usage: " + CMD + " get-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] [--available | --effective] (ARGUMENTS)");
out.println("Usage: " + CMD + " get-roles (--gname NAME | --gpath PATH | --gid ID) [--cclientid CLIENT_ID | --cid ID] [--available | --effective] [ARGUMENTS]");
out.println();
out.println("Command to list realm or client roles on a realm, user or group.");
out.println();
out.println("Use `" + CMD + " config credentials` to establish an authenticated session, or use CREDENTIALS OPTIONS");
out.println("to perform one time authentication.");
out.println();
out.println("If client is specified using --cclientid or --cid then client roles are listed, otherwise realm roles are listed.");
out.println("If user is specified using --uusername or --uid then roles are listed for a specific user.");
out.println("If group is specified using --gname, --gpath or --gid then roles are listed for a specific group.");
out.println("If neither user nor group is specified then defined roles are listed for a realm or specific client");
out.println("If role is specified using --rolename or --roleid then only that specific role is returned.");
out.println("If --available is specified, then only roles not yet added to the target user or group are returned.");
out.println("If --effective is specified, then roles added to the target user or group are transitively resolved and a full");
out.println("set of roles in effect for that user or group is returned.");
out.println();
out.println("Arguments:");
out.println();
out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error");
out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --truststore PATH Path to a truststore containing trusted certificates");
out.println(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");
out.println(" CREDENTIALS OPTIONS Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
out.println(" an authenticated sessions. This allows on-the-fly transient authentication that does");
out.println(" not touch a config file.");
out.println();
out.println(" Command specific options:");
out.println(" --uusername User's 'username'. If more than one user exists with the same username");
out.println(" you'll have to use --uid to specify the target user");
out.println(" --uid User's 'id' attribute");
out.println(" --gname Group's 'name'. If more than one group exists with the same name you'll have");
out.println(" to use --gid, or --gpath to specify the target group");
out.println(" --gpath Group's 'path' attribute");
out.println(" --gid Group's 'id' attribute");
out.println(" --cclientid Client's 'clientId' attribute");
out.println(" --cid Client's 'id' attribute");
out.println(" --rolename Role's 'name' attribute");
out.println(" --roleid Role's 'id' attribute");
out.println(" -a, --admin-root URL URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin");
out.println(" -r, --target-realm REALM Target realm to issue requests against if not the one authenticated against");
out.println();
out.println("Examples:");
out.println();
out.println("Get all realm roles defined on a realm:");
out.println(" " + PROMPT + " " + CMD + " get-roles -r demorealm");
out.println();
out.println("Get all client roles defined on a specific client, displaying only 'id' and 'name':");
out.println(" " + PROMPT + " " + CMD + " get-roles -r demorealm --cclientid realm-management --fields id,name");
out.println();
out.println("List all realm roles for a specific user:");
out.println(" " + PROMPT + " " + CMD + " get-roles -r demorealm --uusername testuser");
out.println();
out.println("List effective client roles for 'realm-management' client for a specific user:");
out.println(" " + PROMPT + " " + CMD + " get-roles -r demorealm --uusername testuser --cclientid realm-management --effective");
out.println();
out.println();
out.println("Use '" + CMD + " help' for general information and a list of commands");
return sb.toString();
}
}

View file

@ -0,0 +1,107 @@
/*
* 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.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 static org.keycloak.client.admin.cli.util.IoUtil.printOut;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "help", description = "This help")
public class HelpCmd implements Command {
@Arguments
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;
}
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;
}
default: {
throw new RuntimeException("Unknown command: " + args.get(0));
}
}
}
return CommandResult.SUCCESS;
} finally {
commandInvocation.stop();
}
}
}

View file

@ -0,0 +1,101 @@
/*
* 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.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 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} )
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();
}
}
public static String usage() {
StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
out.println("Keycloak Admin CLI");
out.println();
out.println("Use '" + CMD + " config credentials' command with username and password to start a session against a specific");
out.println("server and realm.");
out.println();
out.println("For example:");
out.println();
out.println(" " + PROMPT + " " + CMD + " config credentials --server http://localhost:8080/auth --realm master --user admin");
out.println(" Enter password: ");
out.println(" Logging into http://localhost:8080/auth as user admin of realm master");
out.println();
out.println("Any configured username can be used for login, but to perform admin operations the user");
out.println("needs proper roles, otherwise operations will fail.");
out.println();
out.println("Usage: " + CMD + " COMMAND [ARGUMENTS]");
out.println();
out.println("Global options:");
out.println(" -x Print full stack trace when exiting with error");
out.println(" --help Print help for specific command");
out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println();
out.println("Commands: ");
out.println(" config Set up credentials, and other configuration settings using the config file");
out.println(" create Create new resource");
out.println(" get Get a resource");
out.println(" update Update a resource");
out.println(" delete Delete a resource");
out.println(" get-roles List roles for a user or a group");
out.println(" add-roles Add role to a user or a group");
out.println(" remove-roles Remove role from a user or a group");
out.println(" set-password Re-set password for a user");
out.println(" help This help");
out.println();
out.println("Use '" + CMD + " help <command>' for more information about a given command.");
return sb.toString();
}
}

View file

@ -0,0 +1,204 @@
/*
* 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.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 org.keycloak.client.admin.cli.common.AttributeOperation;
import org.keycloak.client.admin.cli.common.CmdStdinContext;
import org.keycloak.client.admin.cli.util.AccessibleBufferOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
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;
import static org.keycloak.client.admin.cli.util.ParseUtil.mergeAttributes;
import static org.keycloak.client.admin.cli.util.ParseUtil.parseFileOrStdin;
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")
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)
String file;
@Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false)
boolean compressed;
//@OptionGroup(shortName = 's', name = "set", description = "Set attribute to the specified value")
//Map<String, String> attributes = new LinkedHashMap<>();
@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);
}
}
}
InputStream body = null;
CmdStdinContext<JsonNode> ctx = new CmdStdinContext<>();
if (file != null) {
ctx = parseFileOrStdin(file);
}
if (attrs.size() > 0) {
ctx = mergeAttributes(ctx, MAPPER.createObjectNode(), attrs);
}
if (body == null && ctx.getContent() != null) {
body = new ByteArrayInputStream(ctx.getContent().getBytes(Charset.forName("utf-8")));
}
AccessibleBufferOutputStream abos = new AccessibleBufferOutputStream(System.out);
if (!compressed) {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
copyStream(body, buffer);
try {
JsonNode rootNode = MAPPER.readValue(buffer.toByteArray(), JsonNode.class);
// now pretty print it to output
MAPPER.writeValue(abos, rootNode);
} catch (Exception ignored) {
copyStream(new ByteArrayInputStream(buffer.toByteArray()), abos);
}
} else {
copyStream(body, System.out);
}
int lastByte = abos.getLastByte();
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";
}
protected String help() {
return usage();
}
public static String usage() {
StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
out.println("Usage: " + CMD + " new-object [ARGUMENTS]");
out.println();
out.println("Command to compose JSON objects from attributes, and merge changes into existing JSON documents.");
out.println();
out.println("This is a local command that does not perform any server requests. It's functionality is fully ");
out.println("integrated into 'create', 'update' and 'delete' commands. It's supposed to be a helper tool only.");
out.println();
out.println("Arguments:");
out.println();
out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error");
out.println();
out.println(" Command specific options:");
out.println(" -s, --set NAME=VALUE Set a specific attribute NAME to a specified value VALUE");
out.println(" -f, --file FILENAME Read object from file or standard input if FILENAME is set to '-'");
out.println(" -c, --compressed Don't pretty print the output");
out.println();
out.println("Examples:");
out.println();
out.println("Create a new JSON document with two top level attributes:");
out.println(" " + PROMPT + " " + CMD + " new-object -s realm=demorealm -s enabled=true");
out.println();
out.println("Read a JSON document and apply changes on top of it:");
if (OS_ARCH.isWindows()) {
out.println(" " + PROMPT + " echo { \"clientId\": \"my_client\" } | " + CMD + " new-object -s enabled=true -f -");
} else {
out.println(" " + PROMPT + " " + CMD + " new-object -s enabled=true -f - << EOF");
out.println(" {");
out.println(" \"clientId\": \"my_client\"");
out.println(" }");
out.println(" EOF");
}
out.println();
out.println();
out.println("Use '" + CMD + " help' for general information and a list of commands");
return sb.toString();
}
}

View file

@ -0,0 +1,334 @@
/*
* 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.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.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 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]")
public class RemoveRolesCmd extends AbstractAuthOptionsCmd {
@Option(name = "uusername", description = "Target user's 'username'")
String uusername;
@Option(name = "uid", description = "Target user's 'id'")
String uid;
@Option(name = "gname", description = "Target group's 'name'")
String gname;
@Option(name = "gpath", description = "Target group's 'path'")
String gpath;
@Option(name = "gid", description = "Target group's 'id'")
String gid;
@Option(name = "cclientid", description = "Target client's 'clientId'")
String cclientid;
@Option(name = "cid", description = "Target client's 'id'")
String cid;
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
List<String> roleNames = new LinkedList<>();
List<String> roleIds = new LinkedList<>();
try {
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
}
processGlobalOptions();
Iterator<String> it = args.iterator();
while (it.hasNext()) {
String option = it.next();
switch (option) {
case "--rolename": {
optionRequiresValueCheck(it, option);
roleNames.add(it.next());
break;
}
case "--roleid": {
optionRequiresValueCheck(it, option);
roleIds.add(it.next());
break;
}
default: {
throw new IllegalArgumentException("Invalid option: " + option);
}
}
}
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 specified. Use --rolename or --roleid to specify roles");
}
if (cid != null && cclientid != null) {
throw new IllegalArgumentException("Incompatible options: --cid and --cclientid 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() && !isGroupSpecified()) {
throw new IllegalArgumentException("No user nor group specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group");
}
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));
}
} 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));
// 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 {
throw new IllegalArgumentException("No user nor group specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group");
}
return CommandResult.SUCCESS;
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
} finally {
commandInvocation.stop();
}
}
private Set<ObjectNode> getRoleRepresentations(List<String> roleNames, List<String> roleIds, LocalSearch roleSearch) {
Set<ObjectNode> rolesToAdd = new HashSet<>();
// now we process roles
for (String name : roleNames) {
ObjectNode r = roleSearch.exactMatchOne(name, "name");
if (r == null) {
throw new RuntimeException("Role not found for name: " + name);
}
rolesToAdd.add(r);
}
for (String id : roleIds) {
ObjectNode r = roleSearch.exactMatchOne(id, "id");
if (r == null) {
throw new RuntimeException("Role not found for id: " + id);
}
rolesToAdd.add(r);
}
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;
}
private boolean isGroupSpecified() {
return gid != null || gname != null || gpath != null;
}
private boolean isUserSpecified() {
return uid != null || uusername != null;
}
@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";
}
protected String help() {
return usage();
}
public static String usage() {
StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
out.println("Usage: " + CMD + " remove-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]");
out.println("Usage: " + CMD + " remove-roles (--gname NAME | --gpath PATH | --gid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]");
out.println();
out.println("Command to remove realm or client roles from a user or group.");
out.println();
out.println("Use `" + CMD + " config credentials` to establish an authenticated session, or use CREDENTIALS OPTIONS");
out.println("to perform one time authentication.");
out.println();
out.println("If client is specified using --cclientid or --cid then roles to remove are client roles, otherwise they are realm roles.");
out.println("Either a user, or a group needs to be specified. If user is specified using --uusername or --uid then roles are removed");
out.println("from a specific user. If group is specified using --gname, --gpath or --gid then roles are removed from a specific group.");
out.println("One or more roles have to be specified using --rolename or --roleid to be removed from a group or a user.");
out.println();
out.println("Arguments:");
out.println();
out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error");
out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --truststore PATH Path to a truststore containing trusted certificates");
out.println(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");
out.println(" CREDENTIALS OPTIONS Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
out.println(" an authenticated sessions. This allows on-the-fly transient authentication that does");
out.println(" not touch a config file.");
out.println();
out.println(" Command specific options:");
out.println(" --uusername User's 'username'. If more than one user exists with the same username");
out.println(" you'll have to use --uid to specify the target user");
out.println(" --uid User's 'id' attribute");
out.println(" --gname Group's 'name'. If more than one group exists with the same name you'll have");
out.println(" to use --gid, or --gpath to specify the target group");
out.println(" --gpath Group's 'path' attribute");
out.println(" --gid Group's 'id' attribute");
out.println(" --cclientid Client's 'clientId' attribute");
out.println(" --cid Client's 'id' attribute");
out.println(" --rolename Role's 'name' attribute");
out.println(" --roleid Role's 'id' attribute");
out.println(" -a, --admin-root URL URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin");
out.println(" -r, --target-realm REALM Target realm to issue requests against if not the one authenticated against");
out.println();
out.println("Examples:");
out.println();
out.println("Remove 'offline_access' realm role from a user:");
out.println(" " + PROMPT + " " + CMD + " remove-roles -r demorealm --uusername testuser --rolename offline_access");
out.println();
out.println("Remove 'realm-management' client roles 'view-users', 'view-clients' and 'view-realm' from a user:");
out.println(" " + PROMPT + " " + CMD + " remove-roles -r demorealm --uusername testuser --cclientid realm-management --rolename view-users --rolename view-clients --rolename view-realm");
out.println();
out.println("Remove 'uma_authorization' realm role to a group:");
out.println(" " + PROMPT + " " + CMD + " remove-roles -r demorealm --gname PowerUsers --rolename uma_authorization");
out.println();
out.println("Remove 'realm-management' client roles 'realm-admin' from a group:");
out.println(" " + PROMPT + " " + CMD + " remove-roles -r demorealm --gname PowerUsers --cclientid realm-management --rolename realm-admin");
out.println();
out.println();
out.println("Use '" + CMD + " help' for general information and a list of commands");
return sb.toString();
}
}

View file

@ -0,0 +1,177 @@
/*
* 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.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 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;
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.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]")
public class SetPasswordCmd extends AbstractAuthOptionsCmd {
@Option(name = "username", description = "Username")
String username;
@Option(name = "userid", description = "User ID")
String userid;
@Option(shortName = 'p', name = "new-password", description = "New password")
String pass;
@Option(shortName = 't', name = "temporary", description = "is password temporary", hasValue = false)
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));
}
if (userid == null && username == null) {
throw new IllegalArgumentException("No user specified. Use --username or --userid to specify user");
}
if (userid != null && username != null) {
throw new IllegalArgumentException("Options --userid and --username are mutually exclusive");
}
if (pass == null) {
pass = readSecret("Enter password: ", commandInvocation);
}
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 username is specified resolve id
if (username != null) {
userid = getIdFromUsername(adminRoot, realm, auth, username);
}
resetUserPassword(adminRoot, realm, auth, userid, pass, temporary);
return CommandResult.SUCCESS;
}
@Override
protected boolean nothingToDo() {
return noOptions() && username == null && userid == null && pass == null;
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help set-password' for more information";
}
protected String help() {
return usage();
}
public static String usage() {
StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
out.println("Usage: " + CMD + " set-password (--username USERNAME | --userid ID) [--password PASSWORD] [ARGUMENTS]");
out.println();
out.println("Command to reset user's password.");
out.println();
out.println("Use `" + CMD + " config credentials` to establish an authenticated session, or use CREDENTIALS OPTIONS");
out.println("to perform one time authentication.");
out.println();
out.println("Arguments:");
out.println();
out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error");
out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --truststore PATH Path to a truststore containing trusted certificates");
out.println(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");
out.println(" CREDENTIALS OPTIONS Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
out.println(" an authenticated sessions. This allows on-the-fly transient authentication that does");
out.println(" not touch a config file.");
out.println();
out.println(" Command specific options:");
out.println(" --username USERNAME Identify target user by 'username'");
out.println(" --userid ID Identify target user by 'id'");
out.println(" -p, --new-password New password to set. If not specified you will be prompted for it.");
out.println(" -t, --temporary Make the new password temporary - user has to change it on next logon");
out.println(" -a, --admin-root URL URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin");
out.println(" -r, --target-realm REALM Target realm to issue requests against if not the one authenticated against");
out.println();
out.println("Examples:");
out.println();
out.println("Set new temporary password for the user:");
out.println(" " + PROMPT + " " + CMD + " set-password -r demorealm --username testuser --password NEWPASS -t");
out.println();
out.println();
out.println("Use '" + CMD + " help' for general information and a list of commands");
return sb.toString();
}
}

View file

@ -0,0 +1,165 @@
/*
* 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.commands;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.cl.Option;
import java.io.PrintWriter;
import java.io.StringWriter;
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 = "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;
@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(shortName = 'H', name = "print-headers", description = "Print response headers", hasValue = false)
boolean printHeaders;
@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(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(shortName = 'o', name = "output", description = "After update output the new client configuration", hasValue = false)
boolean outputResult;
@Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false)
boolean compressed;
//@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.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";
}
@Override
protected boolean nothingToDo() {
return noOptions() && file == null && (args == null || args.size() == 0);
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help update' for more information";
}
protected String help() {
return usage();
}
public static String usage() {
StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
out.println("Usage: " + CMD + " update ENDPOINT_URI [ARGUMENTS]");
out.println();
out.println("Command to update existing resources on the server.");
out.println();
out.println("Use `" + CMD + " config credentials` to establish an authenticated sessions, or use CREDENTIALS OPTIONS");
out.println("to perform one time authentication.");
out.println();
out.println("Arguments:");
out.println();
out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error");
out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --truststore PATH Path to a truststore containing trusted certificates");
out.println(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");
out.println(" CREDENTIALS OPTIONS Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
out.println(" an authenticated sessions. This allows on-the-fly transient authentication that does");
out.println(" not touch a config file.");
out.println();
out.println(" Command specific options:");
out.println(" ENDPOINT_URI URI used to compose a target resource url. Commonly used values start with:");
out.println(" realms/, users/, roles/, groups/, clients/, keys/, components/ ...");
out.println(" If it starts with 'http://' then it will be used as target resource url");
out.println(" -r, --target-realm REALM Target realm to issue requests against if not the one authenticated against");
out.println(" -s, --set NAME=VALUE Set a specific attribute NAME to a specified value VALUE");
out.println(" NAME+=VALUE Add item VALUE to list attribute NAME");
out.println(" -d, --delete NAME Remove a specific attribute NAME from JSON request body");
out.println(" -f, --file FILENAME Read object from file or standard input if FILENAME is set to '-'");
out.println(" -q, --query NAME=VALUE Add to request URI a NAME query parameter with value VALUE");
out.println(" -h, --header NAME=VALUE Set request header NAME to VALUE");
out.println(" -m, --merge Merge new values with existing configuration on the server");
out.println(" Merge is automatically enabled unless --file is specified");
out.println(" -n, --no-merge Suppress merge mode");
out.println();
out.println(" -H, --print-headers Print response headers");
out.println(" -o, --output After update output the new resource to standard output");
out.println(" -F, --fields FILTER A filter pattern to specify which fields of a JSON response to output");
out.println(" -c, --compressed Don't pretty print the output");
out.println(" -a, --admin-root URL URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin");
out.println();
out.println();
out.println("Nested attributes are supported by using '.' to separate components of a KEY. Optionaly, the KEY components ");
out.println("can be quoted with double quotes - e.g. my_client.attributes.\"external.user.id\". If VALUE starts with [ and ");
out.println("ends with ] the attribute will be set as a JSON array. If VALUE starts with { and ends with } the attribute ");
out.println("will be set as a JSON object. If KEY ends with an array index - e.g. clients[3]=VALUE - then the specified item");
out.println("of the array is updated. If KEY+=VALUE syntax is used, then KEY is assumed to be an array, and another item is");
out.println("added to it.");
out.println();
out.println("Attributes can also be deleted. If KEY ends with an array index, then the targeted item of an array is removed");
out.println("and the following items are shifted.");
out.println();
out.println("Merged mode fetches target resource item from the server, applies attribute changes to it, and sends it");
out.println("back to the server.");
out.println();
out.println();
out.println("Examples:");
out.println();
out.println("Update a target realm by fetching current configuration from the server, and applying specified changes");
out.println(" " + PROMPT + " " + CMD + " update realms/demorealm -s registrationAllowed=true");
out.println();
out.println("Update a client by overwriting existing configuration using local file as a template (replace ID with client's 'id'):");
out.println(" " + PROMPT + " " + CMD + " update clients/ID -f new_my_client.json -s 'redirectUris=[\"http://localhost:8080/myapp/*\"]'");
out.println();
out.println("Update client by fetching current configuration from server and merging with specified changes (replace ID with client's 'id'):");
out.println(" " + PROMPT + " " + CMD + " update clients/ID -f new_my_client.json -s enabled=true --merge");
out.println();
out.println("Reset user's password (replace ID with user's 'id'):");
out.println(" " + PROMPT + " " + CMD + " update users/ID/reset-password -r demorealm -s type=password -s value=NEWPASSWORD -s temporary=true -n");
out.println();
out.println();
out.println("Use '" + CMD + " help' for general information and a list of commands");
return sb.toString();
}
}

View file

@ -0,0 +1,170 @@
/*
* 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.common;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class AttributeKey {
private static final int START = 0;
private static final int QUOTED = 1;
private static final int UNQUOTED = 2;
private static final int END = 3;
private List<Component> components;
private boolean append;
public AttributeKey() {
components = Collections.emptyList();
}
public AttributeKey(String key) {
if (key.endsWith("+")) {
append = true;
key = key.substring(0, key.length() - 1);
}
components = parse(key);
}
static List<Component> parse(String key) {
if (key == null || "".equals(key)) {
return Collections.emptyList();
}
List<Component> cs = new LinkedList<>();
StringBuilder sb = new StringBuilder();
int state = START;
char[] buf = key.toCharArray();
for (int pos = 0; pos < buf.length; pos++) {
char c = buf[pos];
if (state == START) {
if ('\"' == c) {
state = QUOTED;
} else if ('.' == c) {
throw new RuntimeException("Invalid attribute key: " + key + " (at position " + (pos + 1) + ")");
} else {
state = UNQUOTED;
sb.append(c);
}
} else if (state == QUOTED) {
if ('\"' == c) {
state = END;
} else {
sb.append(c);
}
} else if (state == UNQUOTED || state == END) {
if ('.' == c) {
state = START;
cs.add(new Component(sb.toString()));
sb.setLength(0);
} else if (state == END || '\"' == c) {
throw new RuntimeException("Invalid attribute key: " + key + " (at position " + (pos + 1) + ")");
} else {
sb.append(c);
}
}
}
boolean ok = false;
if (sb.length() > 0) {
if (state == UNQUOTED || state == END) {
cs.add(new Component(sb.toString()));
ok = true;
}
} else if (state == END) {
ok = true;
}
if (!ok) {
throw new RuntimeException("Invalid attribute key: " + key + " (at position " + (buf.length) + ")");
}
return Collections.unmodifiableList(cs);
}
public List<Component> getComponents() {
return components;
}
public boolean isAppend() {
return append;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (Component c: components) {
if (sb.length() > 0) {
sb.append(".");
}
sb.append(c.toString());
}
return sb.toString();
}
public static class Component {
private int index = -1;
private String name;
Component(String name) {
if (name.endsWith("]")) {
int pos = name.lastIndexOf("[", name.length() - 1);
if (pos == -1) {
throw new RuntimeException("Invalid attribute key: " + name + " (']' not allowed here)");
}
String idx = name.substring(pos + 1, name.length() - 1);
try {
index = Integer.parseInt(idx);
} catch (Exception e) {
throw new RuntimeException("Invalid attribute key: " + name + " (Invalid array index: '[" + idx + "]')");
}
this.name = name.substring(0, pos);
} else {
this.name = name;
}
}
public boolean isArray() {
return index >= 0;
}
public int getIndex() {
return index;
}
public String getName() {
return name;
}
@Override
public String toString() {
return name + (index != -1 ? "[" + index + "]" : "");
}
}
}

View file

@ -0,0 +1,58 @@
/*
* 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.common;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class AttributeOperation {
private Type type;
private AttributeKey key;
private String value;
public AttributeOperation(Type type, String key) {
this(type, key, null);
}
public AttributeOperation(Type type, String key, String value) {
if (type == Type.DELETE && value != null) {
throw new IllegalArgumentException("When type is DELETE, value has to be null");
}
this.type = type;
this.key = new AttributeKey(key);
this.value = value;
}
public Type getType() {
return type;
}
public AttributeKey getKey() {
return key;
}
public String getValue() {
return value;
}
public enum Type {
SET,
DELETE
}
}

View file

@ -0,0 +1,44 @@
/*
* 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.common;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class CmdStdinContext<T> {
private T result;
private String content;
public CmdStdinContext() {}
public T getResult() {
return result;
}
public void setResult(T result) {
this.result = result;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}

View file

@ -0,0 +1,176 @@
/*
* 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.config;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class ConfigData {
private String serverUrl;
private String realm;
private String truststore;
private String trustpass;
private Map<String, Map<String, RealmConfigData>> endpoints = new HashMap<>();
public String getServerUrl() {
return serverUrl;
}
public void setServerUrl(String serverUrl) {
this.serverUrl = serverUrl;
}
public String getRealm() {
return realm;
}
public void setRealm(String realm) {
this.realm = realm;
}
public String getTruststore() {
return truststore;
}
public void setTruststore(String truststore) {
this.truststore = truststore;
}
public String getTrustpass() {
return trustpass;
}
public void setTrustpass(String trustpass) {
this.trustpass = trustpass;
}
public Map<String, Map<String, RealmConfigData>> getEndpoints() {
return endpoints;
}
public void setEndpoints(Map<String, Map<String, RealmConfigData>> endpoints) {
for (Map.Entry<String, Map<String, RealmConfigData>> entry: endpoints.entrySet()) {
String endpoint = entry.getKey();
for (Map.Entry<String, RealmConfigData> sub: entry.getValue().entrySet()) {
RealmConfigData rdata = sub.getValue();
rdata.serverUrl(endpoint);
rdata.realm(sub.getKey());
}
}
this.endpoints = endpoints;
}
public RealmConfigData sessionRealmConfigData() {
if (serverUrl == null)
throw new RuntimeException("Illegal state - no current endpoint in config data");
if (realm == null)
throw new RuntimeException("Illegal state - no current realm in config data");
return ensureRealmConfigData(serverUrl, realm);
}
public RealmConfigData getRealmConfigData(String endpoint, String realm) {
Map<String, RealmConfigData> realmData = endpoints.get(endpoint);
if (realmData == null) {
return null;
}
return realmData.get(realm);
}
public RealmConfigData ensureRealmConfigData(String endpoint, String realm) {
RealmConfigData result = getRealmConfigData(endpoint, realm);
if (result == null) {
result = new RealmConfigData();
result.serverUrl(endpoint);
result.realm(realm);
setRealmConfigData(result);
}
return result;
}
public void setRealmConfigData(RealmConfigData data) {
Map<String, RealmConfigData> realm = endpoints.get(data.serverUrl());
if (realm == null) {
realm = new HashMap<>();
endpoints.put(data.serverUrl(), realm);
}
realm.put(data.realm(), data);
}
public void merge(ConfigData source) {
serverUrl = source.serverUrl;
realm = source.realm;
truststore = source.truststore;
trustpass = source.trustpass;
RealmConfigData current = getRealmConfigData(serverUrl, realm);
RealmConfigData sourceRealm = source.getRealmConfigData(serverUrl, realm);
if (current == null) {
setRealmConfigData(sourceRealm);
} else {
current.merge(sourceRealm);
}
}
public ConfigData deepcopy() {
ConfigData data = new ConfigData();
data.serverUrl = serverUrl;
data.realm = realm;
data.truststore = truststore;
data.trustpass = trustpass;
data.endpoints = new HashMap<>();
for (Map.Entry<String, Map<String, RealmConfigData>> item: endpoints.entrySet()) {
Map<String, RealmConfigData> nuitems = new HashMap<>();
Map<String, RealmConfigData> curitems = item.getValue();
if (curitems != null) {
for (Map.Entry<String, RealmConfigData> ditem : curitems.entrySet()) {
RealmConfigData nudata = ditem.getValue();
if (nudata != null) {
nuitems.put(ditem.getKey(), nudata.deepcopy());
}
}
data.endpoints.put(item.getKey(), nuitems);
}
}
return data;
}
@Override
public String toString() {
try {
return JsonSerialization.writeValueAsPrettyString(this);
} catch (IOException e) {
return super.toString() + " - Error: " + e.toString();
}
}
}

View file

@ -0,0 +1,28 @@
/*
* 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.config;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public interface ConfigHandler {
void saveMergeConfig(ConfigUpdateOperation op);
ConfigData loadConfig();
}

View file

@ -0,0 +1,26 @@
/*
* 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.config;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public interface ConfigUpdateOperation {
void update(ConfigData data);
}

View file

@ -0,0 +1,135 @@
/*
* 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.config;
import org.keycloak.client.admin.cli.util.IoUtil;
import org.keycloak.util.JsonSerialization;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.nio.file.Path;
import java.nio.file.Paths;
import static org.keycloak.client.admin.cli.util.IoUtil.printErr;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class FileConfigHandler implements ConfigHandler {
private static final long MAX_SIZE = 10 * 1024 * 1024;
private static String configFile;
public static void setConfigFile(String filename) {
configFile = filename;
}
public static String getConfigFile() {
return configFile;
}
public ConfigData loadConfig() {
// for now just dumb impl ignoring file locks for read
File file = new File(configFile);
if (!file.isFile() || file.length() == 0) {
return new ConfigData();
}
try {
try (FileInputStream is = new FileInputStream(configFile)) {
return JsonSerialization.readValue(is, ConfigData.class);
}
} catch (IOException e) {
throw new RuntimeException("Failed to load " + configFile, e);
}
}
public static void ensureFile() {
Path path = null;
try {
path = Paths.get(new File(configFile).getAbsolutePath());
IoUtil.ensureFile(path);
} catch (Exception e) {
throw new RuntimeException("Failed to create config file: " + path, e);
}
}
public void saveMergeConfig(ConfigUpdateOperation op) {
try {
ensureFile();
try (RandomAccessFile file = new RandomAccessFile(new File(configFile), "rw")) {
FileChannel fileChannel = file.getChannel();
FileLock fileLock = null;
// lock file for write
int tryCount = 0;
do try {
fileLock = fileChannel.tryLock();
break;
} catch (OverlappingFileLockException e) {
// sleep a little, and try again
try {
Thread.sleep(100);
continue;
} catch (InterruptedException e1) {
throw new RuntimeException("Interrupted");
}
} while (tryCount++ < 10);
if (fileLock != null) {
try {
// load config from file
ConfigData config = new ConfigData();
long size = file.length();
if (size > MAX_SIZE) {
printErr("Config file " + configFile + " is too big. It will be overwritten.");
file.setLength(0);
} else if (size > 0){
byte[] buf = new byte[(int) size];
file.readFully(buf);
config = JsonSerialization.readValue(new ByteArrayInputStream(buf), ConfigData.class);
}
// update loaded config
op.update(config);
// save config to file
byte [] content = JsonSerialization.writeValueAsPrettyString(config).getBytes("utf-8");
file.seek(0);
file.write(content);
file.setLength(content.length);
} finally {
fileLock.release();
}
} else {
throw new RuntimeException("Failed to get lock on " + configFile);
}
}
} catch (IOException e) {
throw new RuntimeException("Failed to save " + configFile, e);
}
}
}

View file

@ -0,0 +1,39 @@
/*
* 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.config;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class InMemoryConfigHandler implements ConfigHandler {
private ConfigData cached;
@Override
public void saveMergeConfig(ConfigUpdateOperation config) {
config.update(cached);
}
@Override
public ConfigData loadConfig() {
return cached;
}
public void setConfigData(ConfigData data) {
this.cached = data;
}
}

View file

@ -0,0 +1,172 @@
/*
* 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.config;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class RealmConfigData {
private String serverUrl;
private String realm;
private String clientId;
private String token;
private String refreshToken;
private String signingToken;
private String secret;
private Long expiresAt;
private Long refreshExpiresAt;
private Long sigExpiresAt;
public String serverUrl() {
return serverUrl;
}
public void serverUrl(String serverUrl) {
this.serverUrl = serverUrl;
}
public String realm() {
return realm;
}
public void realm(String realm) {
this.realm = realm;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public String getSigningToken() {
return signingToken;
}
public void setSigningToken(String signingToken) {
this.signingToken = signingToken;
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public Long getExpiresAt() {
return expiresAt;
}
public void setExpiresAt(Long expiresAt) {
this.expiresAt = expiresAt;
}
public Long getRefreshExpiresAt() {
return refreshExpiresAt;
}
public void setRefreshExpiresAt(Long refreshExpiresAt) {
this.refreshExpiresAt = refreshExpiresAt;
}
public Long getSigExpiresAt() {
return sigExpiresAt;
}
public void setSigExpiresAt(Long sigExpiresAt) {
this.sigExpiresAt = sigExpiresAt;
}
public void merge(RealmConfigData source) {
serverUrl = source.serverUrl;
realm = source.realm;
clientId = source.clientId;
token = source.token;
refreshToken = source.refreshToken;
signingToken = source.signingToken;
secret = source.secret;
expiresAt = source.expiresAt;
refreshExpiresAt = source.refreshExpiresAt;
sigExpiresAt = source.sigExpiresAt;
}
public void mergeRefreshTokens(RealmConfigData source) {
token = source.token;
refreshToken = source.refreshToken;
expiresAt = source.expiresAt;
refreshExpiresAt = source.refreshExpiresAt;
}
@Override
public String toString() {
try {
return JsonSerialization.writeValueAsPrettyString(this);
} catch (IOException e) {
return super.toString() + " - Error: " + e.toString();
}
}
public RealmConfigData deepcopy() {
RealmConfigData data = new RealmConfigData();
data.serverUrl = serverUrl;
data.realm = realm;
data.clientId = clientId;
data.token = token;
data.refreshToken = refreshToken;
data.signingToken = signingToken;
data.secret = secret;
data.expiresAt = expiresAt;
data.refreshExpiresAt = refreshExpiresAt;
data.sigExpiresAt = sigExpiresAt;
return data;
}
}

View file

@ -0,0 +1,39 @@
/*
* 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.httpcomponents;
import org.apache.http.annotation.NotThreadSafe;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import java.net.URI;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@NotThreadSafe
public class HttpDelete extends HttpEntityEnclosingRequestBase {
public HttpDelete(final String uri) {
super();
setURI(URI.create(uri));
}
@Override
public String getMethod() {
return "DELETE";
}
}

View file

@ -0,0 +1,29 @@
/*
* 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.operations;
import static org.keycloak.client.admin.cli.util.HttpUtil.getIdForType;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class ClientOperations {
public static String getIdFromClientId(String rootUrl, String realm, String auth, String clientId) {
return getIdForType(rootUrl, realm, auth, "clients", "clientId", clientId);
}
}

View file

@ -0,0 +1,58 @@
/*
* 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.operations;
import java.util.List;
import static org.keycloak.client.admin.cli.util.HttpUtil.composeResourceUrl;
import static org.keycloak.client.admin.cli.util.HttpUtil.doDeleteJSON;
import static org.keycloak.client.admin.cli.util.HttpUtil.doPostJSON;
import static org.keycloak.client.admin.cli.util.HttpUtil.getIdForType;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class GroupOperations {
public static String getIdFromName(String rootUrl, String realm, String auth, String groupname) {
return getIdForType(rootUrl, realm, auth, "groups", "name", groupname);
}
public static String getIdFromPath(String rootUrl, String realm, String auth, String path) {
return getIdForType(rootUrl, realm, auth, "groups", "path", path);
}
public static void addRealmRoles(String rootUrl, String realm, String auth, String groupid, List<?> roles) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/realm");
doPostJSON(resourceUrl, auth, roles);
}
public static void addClientRoles(String rootUrl, String realm, String auth, String groupid, String idOfClient, List<?> roles) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/clients/" + idOfClient);
doPostJSON(resourceUrl, auth, roles);
}
public static void removeRealmRoles(String rootUrl, String realm, String auth, String groupid, List<?> roles) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/realm");
doDeleteJSON(resourceUrl, auth, roles);
}
public static void removeClientRoles(String rootUrl, String realm, String auth, String groupid, String idOfClient, List<?> roles) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/clients/" + idOfClient);
doDeleteJSON(resourceUrl, auth, roles);
}
}

View file

@ -0,0 +1,60 @@
/*
* 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.operations;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.LinkedList;
import java.util.List;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class LocalSearch {
private List<ObjectNode> items;
public LocalSearch(List<ObjectNode> items) {
this.items = items;
}
public ObjectNode exactMatchOne(String value, String ... attrs) {
List<ObjectNode> matched = new LinkedList<>();
for (ObjectNode item: items) {
for (String attr: attrs) {
JsonNode node = item.get(attr);
if (node != null && node.asText().equals(value)) {
matched.add(item);
break;
}
}
}
if (matched.size() == 0) {
return null;
}
if (matched.size() > 1) {
throw new RuntimeException("More than one match");
}
return matched.get(0);
}
}

View file

@ -0,0 +1,129 @@
/*
* 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.operations;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.keycloak.representations.idm.RoleRepresentation;
import java.util.ArrayList;
import java.util.List;
import static org.keycloak.client.admin.cli.util.HttpUtil.composeResourceUrl;
import static org.keycloak.client.admin.cli.util.HttpUtil.doGetJSON;
import static org.keycloak.client.admin.cli.util.HttpUtil.getAttrForType;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class RoleOperations {
public static class LIST_OF_ROLES extends ArrayList<RoleRepresentation>{};
public static class LIST_OF_NODES extends ArrayList<ObjectNode>{};
public static String getRoleNameFromId(String adminRoot, String realm, String auth, String rid) {
return getAttrForType(adminRoot, realm, auth, "roles", "id", rid, "name");
}
public static String getClientRoleNameFromId(String adminRoot, String realm, String auth, String cid, String rid) {
return getAttrForType(adminRoot, realm, auth, "clients/" + cid + "/roles", "id", rid, "name");
}
public static List<RoleRepresentation> getRealmRoles(String rootUrl, String realm, String auth) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "roles");
return doGetJSON(LIST_OF_ROLES.class, resourceUrl, auth);
}
public static ObjectNode getRealmRole(String rootUrl, String realm, String rolename, String auth) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "roles/" + rolename);
return doGetJSON(ObjectNode.class, resourceUrl, auth);
}
public static List<ObjectNode> getClientRoles(String rootUrl, String realm, String idOfClient, String auth) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "clients/" + idOfClient + "/roles");
return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
}
public static ObjectNode getClientRole(String rootUrl, String realm, String idOfClient, String rolename, String auth) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "clients/" + idOfClient + "/roles/" + rolename);
return doGetJSON(ObjectNode.class, resourceUrl, auth);
}
public static List<ObjectNode> getRealmRolesAsNodes(String rootUrl, String realm, String auth) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "roles");
return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
}
public static List<ObjectNode> getRealmRolesForUserAsNodes(String rootUrl, String realm, String userid, String auth) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/realm");
return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
}
public static List<ObjectNode> getCompositeRealmRolesForUserAsNodes(String rootUrl, String realm, String userid, String auth) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/realm/composite");
return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
}
public static List<ObjectNode> getAvailableRealmRolesForUserAsNodes(String rootUrl, String realm, String userid, String auth) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/realm/available");
return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
}
public static List<ObjectNode> getClientRolesForUserAsNodes(String rootUrl, String realm, String userid, String idOfClient, String auth) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/clients/" + idOfClient);
return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
}
public static List<ObjectNode> getCompositeClientRolesForUserAsNodes(String rootUrl, String realm, String userid, String idOfClient, String auth) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/clients/" + idOfClient + "/composite");
return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
}
public static List<ObjectNode> getAvailableClientRolesForUserAsNodes(String rootUrl, String realm, String userid, String idOfClient, String auth) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/clients/" + idOfClient + "/available");
return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
}
public static List<ObjectNode> getRealmRolesForGroupAsNodes(String rootUrl, String realm, String groupid, String auth) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/realm");
return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
}
public static List<ObjectNode> getCompositeRealmRolesForGroupAsNodes(String rootUrl, String realm, String groupid, String auth) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/realm/composite");
return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
}
public static List<ObjectNode> getAvailableRealmRolesForGroupAsNodes(String rootUrl, String realm, String groupid, String auth) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/realm/available");
return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
}
public static List<ObjectNode> getClientRolesForGroupAsNodes(String rootUrl, String realm, String groupid, String idOfClient, String auth) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/clients/" + idOfClient);
return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
}
public static List<ObjectNode> getCompositeClientRolesForGroupAsNodes(String rootUrl, String realm, String groupid, String idOfClient, String auth) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/clients/" + idOfClient + "/composite");
return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
}
public static List<ObjectNode> getAvailableClientRolesForGroupAsNodes(String rootUrl, String realm, String groupid, String idOfClient, String auth) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "groups/" + groupid + "/role-mappings/clients/" + idOfClient + "/available");
return doGetJSON(LIST_OF_NODES.class, resourceUrl, auth);
}
}

View file

@ -0,0 +1,96 @@
/*
* 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.operations;
import org.keycloak.client.admin.cli.util.Headers;
import org.keycloak.client.admin.cli.util.HeadersBody;
import org.keycloak.client.admin.cli.util.HeadersBodyStatus;
import org.keycloak.client.admin.cli.util.HttpUtil;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.util.JsonSerialization;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.List;
import static org.keycloak.client.admin.cli.util.HttpUtil.composeResourceUrl;
import static org.keycloak.client.admin.cli.util.HttpUtil.doDeleteJSON;
import static org.keycloak.client.admin.cli.util.HttpUtil.doPostJSON;
import static org.keycloak.client.admin.cli.util.HttpUtil.getIdForType;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class UserOperations {
public static void addRealmRoles(String rootUrl, String realm, String auth, String userid, List<?> roles) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/realm");
doPostJSON(resourceUrl, auth, roles);
}
public static void addClientRoles(String rootUrl, String realm, String auth, String userid, String idOfClient, List<?> roles) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/clients/" + idOfClient);
doPostJSON(resourceUrl, auth, roles);
}
public static void removeRealmRoles(String rootUrl, String realm, String auth, String userid, List<?> roles) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/realm");
doDeleteJSON(resourceUrl, auth, roles);
}
public static void removeClientRoles(String rootUrl, String realm, String auth, String userid, String idOfClient, List<?> roles) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/role-mappings/clients/" + idOfClient);
doDeleteJSON(resourceUrl, auth, roles);
}
public static void resetUserPassword(String rootUrl, String realm, String auth, String userid, String password, boolean temporary) {
String resourceUrl = composeResourceUrl(rootUrl, realm, "users/" + userid + "/reset-password");
Headers headers = new Headers();
if (auth != null) {
headers.add("Authorization", auth);
}
headers.add("Content-Type", "application/json");
CredentialRepresentation credentials = new CredentialRepresentation();
credentials.setType("password");
credentials.setTemporary(temporary);
credentials.setValue(password);
HeadersBodyStatus response;
byte[] body;
try {
body = JsonSerialization.writeValueAsBytes(credentials);
} catch (IOException e) {
throw new RuntimeException("Failed to serialize JSON", e);
}
try {
response = HttpUtil.doRequest("put", resourceUrl, new HeadersBody(headers, new ByteArrayInputStream(body)));
} catch (IOException e) {
throw new RuntimeException("HTTP request failed: PUT " + resourceUrl + "\n" + new String(body), e);
}
response.checkSuccess();
}
public static String getIdFromUsername(String rootUrl, String realm, String auth, String username) {
return getIdForType(rootUrl, realm, auth, "users", "username", username);
}
}

View file

@ -0,0 +1,66 @@
/*
* 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.util;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class AccessibleBufferOutputStream extends FilterOutputStream{
private byte[] buf;
/**
* Creates an output stream filter built on top of the specified
* underlying output stream.
*
* @param out the underlying output stream to be assigned to
* the field <tt>this.out</tt> for later use, or
* <code>null</code> if this instance is to be
* created without an underlying stream.
*/
public AccessibleBufferOutputStream(OutputStream out) {
super(out);
}
@Override
public void write(int b) throws IOException {
super.write(b);
buf = new byte[] {(byte) b};
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
super.write(b, off, len);
buf = new byte[len];
System.arraycopy(b, off, buf, 0, len);
}
public byte[] getBuffer() {
return buf;
}
public int getLastByte() {
if (buf != null && buf.length > 0) {
return 0xFF & buf[buf.length-1];
}
return -1;
}
}

View file

@ -0,0 +1,39 @@
/*
* 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.util;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class AttributeException extends RuntimeException {
private final String attrName;
public AttributeException(String attrName, String message) {
super(message);
this.attrName = attrName;
}
public AttributeException(String attrName, String message, Throwable th) {
super(message, th);
this.attrName = attrName;
}
public String getAttributeName() {
return attrName;
}
}

View file

@ -0,0 +1,202 @@
/*
* 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.util;
import org.keycloak.client.admin.cli.config.ConfigData;
import org.keycloak.client.admin.cli.config.RealmConfigData;
import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.common.util.Time;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.util.BasicAuthHelper;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.KeyPair;
import java.util.UUID;
import static java.lang.System.currentTimeMillis;
import static org.keycloak.client.admin.cli.util.ConfigUtil.checkAuthInfo;
import static org.keycloak.client.admin.cli.util.ConfigUtil.saveMergeConfig;
import static org.keycloak.client.admin.cli.util.HttpUtil.APPLICATION_FORM_URL_ENCODED;
import static org.keycloak.client.admin.cli.util.HttpUtil.APPLICATION_JSON;
import static org.keycloak.client.admin.cli.util.HttpUtil.doPost;
import static org.keycloak.client.admin.cli.util.HttpUtil.urlencode;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class AuthUtil {
public static String ensureToken(ConfigData config) {
checkAuthInfo(config);
RealmConfigData realmConfig = config.sessionRealmConfigData();
long now = currentTimeMillis();
// check expires of access_token against time
// if it's less than 5s to expiry, renew it
if (realmConfig.getExpiresAt() - now < 5000) {
// check refresh_token against expiry time
// if it's less than 5s to expiry, fail with credentials expired
if (realmConfig.getRefreshExpiresAt() - now < 5000) {
throw new RuntimeException("Session has expired. Login again with '" + OsUtil.CMD + " config credentials'");
}
if (realmConfig.getSigExpiresAt() != null && realmConfig.getSigExpiresAt() - now < 5000) {
throw new RuntimeException("Session has expired. Login again with '" + OsUtil.CMD + " config credentials'");
}
try {
String authorization = null;
StringBuilder body = new StringBuilder("grant_type=refresh_token")
.append("&refresh_token=").append(realmConfig.getRefreshToken())
.append("&client_id=").append(urlencode(realmConfig.getClientId()));
if (realmConfig.getSigningToken() != null) {
body.append("&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
.append("&client_assertion=").append(realmConfig.getSigningToken());
} else if (realmConfig.getSecret() != null) {
authorization = BasicAuthHelper.createHeader(realmConfig.getClientId(), realmConfig.getSecret());
}
InputStream result = doPost(realmConfig.serverUrl() + "/realms/" + realmConfig.realm() + "/protocol/openid-connect/token",
APPLICATION_FORM_URL_ENCODED, APPLICATION_JSON, body.toString(), authorization);
AccessTokenResponse token = JsonSerialization.readValue(result, AccessTokenResponse.class);
saveMergeConfig(cfg -> {
RealmConfigData realmData = cfg.sessionRealmConfigData();
realmData.setToken(token.getToken());
realmData.setRefreshToken(token.getRefreshToken());
realmData.setExpiresAt(currentTimeMillis() + token.getExpiresIn() * 1000);
realmData.setRefreshExpiresAt(currentTimeMillis() + token.getRefreshExpiresIn() * 1000);
});
return token.getToken();
} catch (Exception e) {
throw new RuntimeException("Failed to refresh access token - " + e.getMessage(), e);
}
}
return realmConfig.getToken();
}
public static AccessTokenResponse getAuthTokens(String server, String realm, String user, String password, String clientId) {
StringBuilder body = new StringBuilder();
try {
body.append("grant_type=password")
.append("&username=").append(urlencode(user))
.append("&password=").append(urlencode(password))
.append("&client_id=").append(urlencode(clientId));
InputStream result = doPost(server + "/realms/" + realm + "/protocol/openid-connect/token",
APPLICATION_FORM_URL_ENCODED, APPLICATION_JSON, body.toString(), null);
return JsonSerialization.readValue(result, AccessTokenResponse.class);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Unexpected error: ", e);
} catch (IOException e) {
throw new RuntimeException("Error receiving response: ", e);
}
}
public static AccessTokenResponse getAuthTokensByJWT(String server, String realm, String user, String password, String clientId, String signedRequestToken) {
StringBuilder body = new StringBuilder();
try {
body.append("client_id=").append(urlencode(clientId))
.append("&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
.append("&client_assertion=").append(signedRequestToken);
if (user != null) {
if (password == null) {
throw new RuntimeException("No password specified");
}
body.append("&grant_type=password")
.append("&username=").append(urlencode(user))
.append("&password=").append(urlencode(password));
} else {
body.append("&grant_type=client_credentials");
}
InputStream result = doPost(server + "/realms/" + realm + "/protocol/openid-connect/token",
APPLICATION_FORM_URL_ENCODED, APPLICATION_JSON, body.toString(), null);
return JsonSerialization.readValue(result, AccessTokenResponse.class);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Unexpected error: ", e);
} catch (IOException e) {
throw new RuntimeException("Error receiving response: ", e);
}
}
public static AccessTokenResponse getAuthTokensBySecret(String server, String realm, String user, String password, String clientId, String secret) {
StringBuilder body = new StringBuilder();
try {
if (user != null) {
if (password == null) {
throw new RuntimeException("No password specified");
}
body.append("client_id=").append(urlencode(clientId))
.append("&grant_type=password")
.append("&username=").append(urlencode(user))
.append("&password=").append(urlencode(password));
} else {
body.append("grant_type=client_credentials");
}
InputStream result = doPost(server + "/realms/" + realm + "/protocol/openid-connect/token",
APPLICATION_FORM_URL_ENCODED, APPLICATION_JSON, body.toString(), BasicAuthHelper.createHeader(clientId, secret));
return JsonSerialization.readValue(result, AccessTokenResponse.class);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Unexpected error: ", e);
} catch (IOException e) {
throw new RuntimeException("Error receiving response: ", e);
}
}
public static String getSignedRequestToken(String keystore, String storePass, String keyPass, String alias, int sigLifetime, String clientId, String realmInfoUrl) {
KeyPair keypair = KeystoreUtil.loadKeyPairFromKeystore(keystore, storePass, keyPass, alias, KeystoreUtil.KeystoreFormat.JKS);
JsonWebToken reqToken = new JsonWebToken();
reqToken.id(UUID.randomUUID().toString());
reqToken.issuer(clientId);
reqToken.subject(clientId);
reqToken.audience(realmInfoUrl);
int now = Time.currentTime();
reqToken.issuedAt(now);
reqToken.expiration(now + sigLifetime);
reqToken.notBefore(now);
String signedRequestToken = new JWSBuilder()
.jsonContent(reqToken)
.rsa256(keypair.getPrivate());
return signedRequestToken;
}
}

View file

@ -0,0 +1,116 @@
/*
* 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.util;
import org.keycloak.client.admin.cli.config.ConfigData;
import org.keycloak.client.admin.cli.config.ConfigHandler;
import org.keycloak.client.admin.cli.config.ConfigUpdateOperation;
import org.keycloak.client.admin.cli.config.InMemoryConfigHandler;
import org.keycloak.client.admin.cli.config.RealmConfigData;
import org.keycloak.representations.AccessTokenResponse;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class ConfigUtil {
public static final String DEFAULT_CLIENT = "admin-cli";
public static final String DEFAULT_CONFIG_FILE_STRING = OsUtil.OS_ARCH.isWindows() ? "%HOMEDRIVE%%HOMEPATH%\\.keycloak\\kcadm.config" : "~/.keycloak/kcadm.config";
public static final String DEFAULT_CONFIG_FILE_PATH = System.getProperty("user.home") + "/.keycloak/kcadm.config";
private static ConfigHandler handler;
public static ConfigHandler getHandler() {
return handler;
}
public static void setHandler(ConfigHandler handler) {
ConfigUtil.handler = handler;
}
public static void saveTokens(AccessTokenResponse tokens, String endpoint, String realm, String clientId, String signKey, Long sigExpiresAt, String secret) {
handler.saveMergeConfig(config -> {
config.setServerUrl(endpoint);
config.setRealm(realm);
RealmConfigData realmConfig = config.ensureRealmConfigData(endpoint, realm);
realmConfig.setToken(tokens.getToken());
realmConfig.setRefreshToken(tokens.getRefreshToken());
realmConfig.setSigningToken(signKey);
realmConfig.setSecret(secret);
realmConfig.setExpiresAt(System.currentTimeMillis() + tokens.getExpiresIn() * 1000);
realmConfig.setRefreshExpiresAt(tokens.getRefreshExpiresIn() == 0 ?
Long.MAX_VALUE : System.currentTimeMillis() + tokens.getRefreshExpiresIn() * 1000);
realmConfig.setSigExpiresAt(sigExpiresAt);
realmConfig.setClientId(clientId);
});
}
public static void checkServerInfo(ConfigData config) {
if (config.getServerUrl() == null || config.getRealm() == null) {
throw new RuntimeException("No server or realm specified. Use --server, --realm, or '" + OsUtil.CMD + " config credentials'.");
}
}
public static void checkAuthInfo(ConfigData config) {
checkServerInfo(config);
}
public static boolean credentialsAvailable(ConfigData config) {
return config.getServerUrl() != null && config.getRealm() != null
&& config.sessionRealmConfigData() != null && config.sessionRealmConfigData().getRefreshToken() != null;
}
public static ConfigData loadConfig() {
if (handler == null) {
throw new RuntimeException("No ConfigHandler set");
}
return handler.loadConfig();
}
public static void saveMergeConfig(ConfigUpdateOperation op) {
if (handler == null) {
throw new RuntimeException("No ConfigHandler set");
}
handler.saveMergeConfig(op);
}
public static void setupInMemoryHandler(ConfigData config) {
InMemoryConfigHandler memhandler = null;
if (handler instanceof InMemoryConfigHandler) {
memhandler = (InMemoryConfigHandler) handler;
} else {
memhandler = new InMemoryConfigHandler();
handler = memhandler;
}
memhandler.setConfigData(config);
}
public static String getEffectiveClientId(ConfigData config) {
String clientId = DEFAULT_CLIENT;
RealmConfigData realmData = config.sessionRealmConfigData();
if (realmData != null && realmData.getClientId() != null) {
clientId = realmData.getClientId();
}
return clientId;
}
}

View file

@ -0,0 +1,59 @@
/*
* 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.util;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.IOException;
import java.util.Iterator;
import static org.keycloak.client.admin.cli.util.OutputUtil.MAPPER;
import static org.keycloak.client.admin.cli.util.OutputUtil.convertToJsonNode;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class FilterUtil {
public static JsonNode copyFilteredObject(Object object, ReturnFields returnFields) throws IOException {
JsonNode node = convertToJsonNode(object);
JsonNode r = node;
if (node.isArray()) {
ArrayNode ar = MAPPER.createArrayNode();
for (JsonNode item: node) {
ar.add(copyFilteredObject(item, returnFields));
}
r = ar;
} else if (node.isObject()){
r = MAPPER.createObjectNode();
Iterator<String> fieldNames = node.fieldNames();
while (fieldNames.hasNext()) {
String name = fieldNames.next();
if (returnFields.included(name)) {
JsonNode value = copyFilteredObject(node.get(name), returnFields.child(name));
((ObjectNode) r).set(name, value);
}
}
}
return r;
}
}

View file

@ -0,0 +1,39 @@
/*
* 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.util;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class Header {
private String name;
private String value;
public Header(String key, String value) {
this.name = key;
this.value = value;
}
public String getName() {
return name;
}
public String getValue() {
return value;
}
}

View file

@ -0,0 +1,55 @@
/*
* 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.util;
import java.util.Iterator;
import java.util.LinkedHashMap;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class Headers implements Iterable<Header> {
private LinkedHashMap<String, Header> headers = new LinkedHashMap<>();
public void add(String header, String value) {
headers.put(header.toLowerCase(), new Header(header, value));
}
public boolean addIfMissing(String header, String value) {
String key = header.toLowerCase();
if (!headers.containsKey(key)) {
headers.put(key, new Header(header, value));
return true;
}
return false;
}
public boolean contains(String header) {
String key = header.toLowerCase();
return headers.containsKey(key);
}
public Header get(String header) {
return headers.get(header.toLowerCase());
}
@Override
public Iterator<Header> iterator() {
return headers.values().iterator();
}
}

View file

@ -0,0 +1,72 @@
/*
* 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.util;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.charset.Charset;
import static org.keycloak.client.admin.cli.util.IoUtil.copyStream;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class HeadersBody {
private Headers headers;
private InputStream body;
public HeadersBody(Headers headers) {
this.headers = headers;
}
public HeadersBody(Headers headers, InputStream body) {
this.headers = headers;
this.body = body;
}
public Headers getHeaders() {
return headers;
}
public InputStream getBody() {
return body;
}
public String readBodyString() {
byte [] buffer = readBodyBytes();
return new String(buffer, Charset.forName(getContentCharset()));
}
public byte[] readBodyBytes() {
ByteArrayOutputStream os = new ByteArrayOutputStream();
copyStream(getBody(), os);
return os.toByteArray();
}
public String getContentCharset() {
Header contentType = headers.get("Content-Type");
if (contentType != null) {
int pos = contentType.getValue().lastIndexOf("charset=");
if (pos != -1) {
return contentType.getValue().substring(pos + 8);
}
}
return "iso-8859-1";
}
}

View file

@ -0,0 +1,68 @@
/*
* 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.util;
import org.keycloak.util.JsonSerialization;
import java.io.InputStream;
import java.util.Map;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class HeadersBodyStatus extends HeadersBody {
private final String status;
public HeadersBodyStatus(String status, Headers headers, InputStream body) {
super(headers, body);
this.status = status;
}
public String getStatus() {
return status;
}
private String getStatusCodeAndReason() {
return getStatus().substring(9);
}
public void checkSuccess() {
int code = getStatusCode();
if (code < 200 || code >= 300) {
String content = readBodyString();
Map<String, String> error = null;
try {
error = JsonSerialization.readValue(content, Map.class);
} catch (Exception ignored) {
}
String message = null;
if (error != null) {
String description = error.get("error_description");
String err = error.get("error");
String msg = error.get("errorMessage");
message = msg != null ? msg : err != null ? (description + " ["+ error.get("error") + "]") : null;
}
throw new HttpResponseException(getStatusCodeAndReason(), message, new RuntimeException(content));
}
}
public int getStatusCode() {
return Integer.valueOf(status.split(" ")[1]);
}
}

View file

@ -0,0 +1,34 @@
/*
* 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.util;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class HttpResponseException extends RuntimeException {
private String status;
HttpResponseException(String status, String message, Throwable cause) {
super(message != null ? message : "HTTP error - " + status, cause);
this.status = status;
}
public int getStatusCode() {
return Integer.valueOf(status.split(" ")[0]);
}
}

View file

@ -0,0 +1,450 @@
/*
* 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.util;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.http.HeaderIterator;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.ssl.SSLContexts;
import org.keycloak.client.admin.cli.httpcomponents.HttpDelete;
import org.keycloak.client.admin.cli.operations.LocalSearch;
import org.keycloak.client.admin.cli.operations.RoleOperations;
import org.keycloak.util.JsonSerialization;
import javax.net.ssl.SSLContext;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.keycloak.common.util.ObjectUtil.capitalize;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class HttpUtil {
public static final String APPLICATION_XML = "application/xml";
public static final String APPLICATION_JSON = "application/json";
public static final String APPLICATION_FORM_URL_ENCODED = "application/x-www-form-urlencoded";
public static final String UTF_8 = "utf-8";
private static HttpClient httpClient;
private static SSLConnectionSocketFactory sslsf;
public static InputStream doGet(String url, String acceptType, String authorization) {
try {
HttpGet request = new HttpGet(url);
request.setHeader(HttpHeaders.ACCEPT, acceptType);
return doRequest(authorization, request);
} catch (IOException e) {
throw new RuntimeException("Failed to send request - " + e.getMessage(), e);
}
}
public static InputStream doPost(String url, String contentType, String acceptType, String content, String authorization) {
try {
return doPostOrPut(contentType, acceptType, content, authorization, new HttpPost(url));
} catch (IOException e) {
throw new RuntimeException("Failed to send request - " + e.getMessage(), e);
}
}
public static InputStream doPut(String url, String contentType, String acceptType, String content, String authorization) {
try {
return doPostOrPut(contentType, acceptType, content, authorization, new HttpPut(url));
} catch (IOException e) {
throw new RuntimeException("Failed to send request - " + e.getMessage(), e);
}
}
public static void doDelete(String url, String authorization) {
try {
HttpDelete request = new HttpDelete(url);
doRequest(authorization, request);
} catch (IOException e) {
throw new RuntimeException("Failed to send request - " + e.getMessage(), e);
}
}
public static HeadersBodyStatus doGet(String url, HeadersBody request) throws IOException {
return doRequest("get", url, request);
}
public static HeadersBodyStatus doPost(String url, HeadersBody request) throws IOException {
return doRequest("post", url, request);
}
public static HeadersBodyStatus doPut(String url, HeadersBody request) throws IOException {
return doRequest("put", url, request);
}
public static HeadersBodyStatus doDelete(String url, HeadersBody request) throws IOException {
return doRequest("delete", url, request);
}
public static HeadersBodyStatus doRequest(String type, String url, HeadersBody request) throws IOException {
HttpRequestBase req;
switch (type) {
case "get":
req = new HttpGet(url);
break;
case "post":
req = new HttpPost(url);
break;
case "put":
req = new HttpPut(url);
break;
case "delete":
req = new HttpDelete(url);
break;
case "options":
req = new HttpOptions(url);
break;
case "head":
req = new HttpHead(url);
break;
default:
throw new RuntimeException("Method not supported: " + type);
}
addHeaders(req, request.getHeaders());
if (request.getBody() != null) {
if (req instanceof HttpEntityEnclosingRequestBase == false) {
throw new RuntimeException("Request type does not support body: " + type);
}
((HttpEntityEnclosingRequestBase) req).setEntity(new InputStreamEntity(request.getBody()));
}
HttpResponse res = getHttpClient().execute(req);
InputStream responseStream = null;
if (res.getEntity() != null) {
responseStream = res.getEntity().getContent();
} else {
responseStream = new InputStream() {
@Override
public int read () throws IOException {
return -1;
}
};
}
Headers headers = new Headers();
HeaderIterator it = res.headerIterator();
while (it.hasNext()) {
org.apache.http.Header header = it.nextHeader();
headers.add(header.getName(), header.getValue());
}
return new HeadersBodyStatus(res.getStatusLine().toString(), headers, responseStream);
}
private static void addHeaders(HttpRequestBase request, Headers headers) {
for (Header header: headers) {
request.setHeader(header.getName(), header.getValue());
}
}
private static InputStream doPostOrPut(String contentType, String acceptType, String content, String authorization, HttpEntityEnclosingRequestBase request) throws IOException {
request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
request.setHeader(HttpHeaders.ACCEPT, acceptType);
if (content != null) {
request.setEntity(new StringEntity(content));
}
return doRequest(authorization, request);
}
private static InputStream doRequest(String authorization, HttpRequestBase request) throws IOException {
addAuth(request, authorization);
HttpResponse response = getHttpClient().execute(request);
InputStream responseStream = null;
if (response.getEntity() != null) {
responseStream = response.getEntity().getContent();
}
int code = response.getStatusLine().getStatusCode();
if (code >= 200 && code < 300) {
return responseStream;
} else {
Map<String, String> error = null;
try {
org.apache.http.Header header = response.getEntity().getContentType();
if (header != null && APPLICATION_JSON.equals(header.getValue())) {
error = JsonSerialization.readValue(responseStream, Map.class);
}
} catch (Exception e) {
throw new RuntimeException("Failed to read error response - " + e.getMessage(), e);
} finally {
responseStream.close();
}
String message = null;
if (error != null) {
message = error.get("error_description") + " [" + error.get("error") + "]";
}
throw new RuntimeException(message != null ? message : response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase());
}
}
private static void addAuth(HttpRequestBase request, String authorization) {
if (authorization != null) {
request.setHeader(HttpHeaders.AUTHORIZATION, authorization);
}
}
public static HttpClient getHttpClient() {
if (httpClient == null) {
if (sslsf != null) {
httpClient = HttpClientBuilder.create().useSystemProperties().setSSLSocketFactory(sslsf).build();
} else {
httpClient = HttpClientBuilder.create().useSystemProperties().build();
}
}
return httpClient;
}
public static String urlencode(String value) {
try {
return URLEncoder.encode(value, UTF_8);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Failed to urlencode", e);
}
}
public static void setTruststore(File file, String password) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, KeyManagementException {
if (!file.isFile()) {
throw new RuntimeException("Truststore file not found: " + file.getAbsolutePath());
}
SSLContext theContext = SSLContexts.custom()
.useProtocol("TLS")
.loadTrustMaterial(file, password == null ? null : password.toCharArray())
.build();
sslsf = new SSLConnectionSocketFactory(theContext);
}
public static String extractIdFromLocation(String location) {
int last = location.lastIndexOf("/");
if (last != -1) {
return location.substring(last + 1);
}
return null;
}
public static String addQueryParamsToUri(String uri, String ... queryParams) {
if (queryParams == null) {
return uri;
}
if (queryParams.length % 2 != 0) {
throw new RuntimeException("Value missing for query parameter: " + queryParams[queryParams.length-1]);
}
Map<String, String> params = new LinkedHashMap<>();
for (int i = 0; i < queryParams.length; i += 2) {
params.put(queryParams[i], queryParams[i+1]);
}
return addQueryParamsToUri(uri, params);
}
public static String addQueryParamsToUri(String uri, Map<String, String> queryParams) {
if (queryParams.size() == 0) {
return uri;
}
StringBuilder query = new StringBuilder();
for (Map.Entry<String, String> params: queryParams.entrySet()) {
try {
if (query.length() > 0) {
query.append("&");
}
query.append(params.getKey()).append("=").append(URLEncoder.encode(params.getValue(), "utf-8"));
} catch (Exception e) {
throw new RuntimeException("Failed to encode query params: " + params.getKey() + "=" + params.getValue());
}
}
return uri + (uri.indexOf("?") == -1 ? "?" : "&") + query;
}
public static String composeResourceUrl(String adminRoot, String realm, String uri) {
if (!uri.startsWith("http:") && !uri.startsWith("https:")) {
if ("realms".equals(uri) || uri.startsWith("realms/")) {
uri = normalize(adminRoot) + uri;
} else if ("serverinfo".equals(uri)) {
uri = normalize(adminRoot) + uri;
} else {
uri = normalize(adminRoot) + "realms/" + realm + "/" + uri;
}
}
return uri;
}
public static String normalize(String value) {
return value.endsWith("/") ? value : value + "/";
}
public static void checkSuccess(String url, HeadersBodyStatus response) {
try {
response.checkSuccess();
} catch (HttpResponseException e) {
if (e.getStatusCode() == 404) {
throw new RuntimeException("Resource not found for url: " + url, e);
}
throw e;
}
}
public static <T> T doGetJSON(Class<T> type, String resourceUrl, String auth) {
Headers headers = new Headers();
if (auth != null) {
headers.add("Authorization", auth);
}
headers.add("Accept", "application/json");
HeadersBodyStatus response;
try {
response = HttpUtil.doRequest("get", resourceUrl, new HeadersBody(headers));
} catch (IOException e) {
throw new RuntimeException("HTTP request failed: GET " + resourceUrl, e);
}
checkSuccess(resourceUrl, response);
T result;
try {
result = JsonSerialization.readValue(response.getBody(), type);
} catch (IOException e) {
throw new RuntimeException("Failed to read JSON response", e);
}
return result;
}
public static void doPostJSON(String resourceUrl, String auth, Object content) {
Headers headers = new Headers();
if (auth != null) {
headers.add("Authorization", auth);
}
headers.add("Content-Type", "application/json");
HeadersBodyStatus response;
byte[] body;
try {
body = JsonSerialization.writeValueAsBytes(content);
} catch (IOException e) {
throw new RuntimeException("Failed to serialize JSON", e);
}
try {
response = HttpUtil.doRequest("post", resourceUrl, new HeadersBody(headers, new ByteArrayInputStream(body)));
} catch (IOException e) {
throw new RuntimeException("HTTP request failed: POST " + resourceUrl + "\n" + new String(body), e);
}
checkSuccess(resourceUrl, response);
}
public static void doDeleteJSON(String resourceUrl, String auth, Object content) {
Headers headers = new Headers();
if (auth != null) {
headers.add("Authorization", auth);
}
headers.add("Content-Type", "application/json");
HeadersBodyStatus response;
byte[] body;
try {
body = JsonSerialization.writeValueAsBytes(content);
} catch (IOException e) {
throw new RuntimeException("Failed to serialize JSON", e);
}
try {
response = HttpUtil.doRequest("delete", resourceUrl, new HeadersBody(headers, new ByteArrayInputStream(body)));
} catch (IOException e) {
throw new RuntimeException("HTTP request failed: DELETE " + resourceUrl + "\n" + new String(body), e);
}
checkSuccess(resourceUrl, response);
}
public static String getIdForType(String rootUrl, String realm, String auth, String resourceEndpoint, String attrName, String attrValue) {
return getAttrForType(rootUrl, realm, auth, resourceEndpoint, attrName, attrValue, "id");
}
public static String getAttrForType(String rootUrl, String realm, String auth, String resourceEndpoint, String attrName, String attrValue, String returnAttrName) {
String resourceUrl = composeResourceUrl(rootUrl, realm, resourceEndpoint);
resourceUrl = HttpUtil.addQueryParamsToUri(resourceUrl, attrName, attrValue, "first", "0", "max", "2");
List<ObjectNode> users = doGetJSON(RoleOperations.LIST_OF_NODES.class, resourceUrl, auth);
ObjectNode user;
try {
user = new LocalSearch(users).exactMatchOne(attrValue, attrName);
} catch (Exception e) {
throw new RuntimeException("Multiple " + resourceEndpoint + " found for " + attrName + ": " + attrValue, e);
}
String typeName = singularize(resourceEndpoint);
if (user == null) {
throw new RuntimeException(capitalize(typeName) + " not found for " + attrName + ": " + attrValue);
}
JsonNode attr = user.get(returnAttrName);
if (attr == null) {
throw new RuntimeException("Returned " + typeName + " info has no '" + returnAttrName + "' attribute");
}
return attr.asText();
}
public static String singularize(String value) {
return value.substring(0, value.length()-1);
}
}

View file

@ -0,0 +1,255 @@
/*
* 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.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.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.AclEntry;
import java.nio.file.attribute.AclEntryPermission;
import java.nio.file.attribute.AclEntryType;
import java.nio.file.attribute.AclFileAttributeView;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.UserPrincipal;
import java.util.Formatter;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
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>
*/
public class IoUtil {
public static String readFileOrStdin(String file) {
String content;
if ("-".equals(file)) {
content = readFully(System.in);
} else {
try (InputStream is = new FileInputStream(file)) {
content = readFully(is);
} catch (FileNotFoundException e) {
throw new RuntimeException("File not found: " + file);
} catch (IOException e) {
throw new RuntimeException("Failed to read file: " + file, e);
}
}
return content;
}
public static void waitFor(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted");
}
}
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);
}
/*
if (!Globals.stdin.isStdinAvailable()) {
try {
return readLine(new InputStreamReader(System.in));
} catch (IOException e) {
throw new RuntimeException("Standard input not available");
}
}
*/
// Windows hack - get rid of any \n
result = result.replaceAll("\\n", "");
return result;
}
public static String readFully(InputStream is) {
Charset charset = Charset.forName("utf-8");
StringBuilder out = new StringBuilder();
byte [] buf = new byte[8192];
int rc;
try {
while ((rc = is.read(buf)) != -1) {
out.append(new String(buf, 0, rc, charset));
}
} catch (Exception e) {
throw new RuntimeException("Failed to read stream", e);
}
return out.toString();
}
public static void copyStream(InputStream is, OutputStream os) {
byte [] buf = new byte[8192];
int rc;
try (InputStream input = is) {
while ((rc = input.read(buf)) != -1) {
os.write(buf, 0, rc);
}
} catch (Exception e) {
throw new RuntimeException("Failed to read/write a stream: ", e);
} finally {
try {
os.flush();
} catch (IOException e) {
throw new RuntimeException("Failed to write a stream: ", e);
}
}
}
public static void ensureFile(Path path) throws IOException {
FileSystem fs = FileSystems.getDefault();
Set<String> supportedViews = fs.supportedFileAttributeViews();
Path parent = path.getParent();
if (!isDirectory(parent)) {
createDirectories(parent);
// make sure only owner can read/write it
if (supportedViews.contains("posix")) {
setUnixPermissions(parent);
} else if (supportedViews.contains("acl")) {
setWindowsPermissions(parent);
} else {
warnErr("Failed to restrict access permissions on .keycloak directory: " + parent);
}
}
if (!isRegularFile(path)) {
createFile(path);
// make sure only owner can read/write it
if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
setUnixPermissions(path);
} else if (supportedViews.contains("acl")) {
setWindowsPermissions(path);
} else {
warnErr("Failed to restrict access permissions on config file: " + path);
}
}
}
private static void setUnixPermissions(Path path) throws IOException {
Set<PosixFilePermission> perms = new HashSet<>();
perms.add(PosixFilePermission.OWNER_READ);
perms.add(PosixFilePermission.OWNER_WRITE);
if (isDirectory(path)) {
perms.add(PosixFilePermission.OWNER_EXECUTE);
}
Files.setPosixFilePermissions(path, perms);
}
private static void setWindowsPermissions(Path path) throws IOException {
AclFileAttributeView view = Files.getFileAttributeView(path, AclFileAttributeView.class);
UserPrincipal owner = view.getOwner();
List<AclEntry> acl = view.getAcl();
ListIterator<AclEntry> it = acl.listIterator();
while (it.hasNext()) {
AclEntry entry = it.next();
if ("BUILTIN\\Administrators".equals(entry.principal().getName()) || "NT AUTHORITY\\SYSTEM".equals(entry.principal().getName())) {
continue;
}
it.remove();
}
AclEntry entry = AclEntry.newBuilder()
.setType(AclEntryType.ALLOW)
.setPrincipal(owner)
.setPermissions(AclEntryPermission.READ_DATA, AclEntryPermission.WRITE_DATA,
AclEntryPermission.APPEND_DATA, AclEntryPermission.READ_NAMED_ATTRS,
AclEntryPermission.WRITE_NAMED_ATTRS, AclEntryPermission.EXECUTE,
AclEntryPermission.READ_ATTRIBUTES, AclEntryPermission.WRITE_ATTRIBUTES,
AclEntryPermission.DELETE, AclEntryPermission.READ_ACL, AclEntryPermission.SYNCHRONIZE)
.build();
acl.add(entry);
view.setAcl(acl);
}
public static void printOut(String msg) {
System.out.println(msg);
}
public static void printErr(String msg) {
System.err.println(msg);
}
public static void printfOut(String format, String ... params) {
System.out.println(new Formatter().format("WARN: " + format, params));
}
public static void warnOut(String msg) {
System.out.println("WARN: " + msg);
}
public static void warnErr(String msg) {
System.err.println("WARN: " + msg);
}
public static void warnfOut(String format, String ... params) {
System.out.println(new Formatter().format("WARN: " + format, params));
}
public static void warnfErr(String format, String ... params) {
System.err.println(new Formatter().format("WARN: " + format, params));
}
public static void logOut(String msg) {
System.out.println("LOG: " + msg);
}
}

View file

@ -0,0 +1,71 @@
/*
* 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.util;
/**
* @author <a href="mailto:marko.strukelj@gmail.com">Marko Strukelj</a>
*/
public class OsArch {
private String os;
private String arch;
private boolean legacy;
public OsArch(String os, String arch) {
this(os, arch, false);
}
public OsArch(String os, String arch, boolean legacy) {
this.os = os;
this.arch = arch;
this.legacy = legacy;
}
public String os() {
return os;
}
public String arch() {
return arch;
}
public boolean isLegacy() {
return legacy;
}
public boolean isWindows() {
return "win32".equals(os);
}
public String envVar(String var) {
if (isWindows()) {
return "%" + var + "%";
} else {
return "$" + var;
}
}
public String path(String path) {
if (isWindows()) {
path = path.replaceAll("/", "\\\\");
if (path.startsWith("~")) {
path = "%HOMEPATH%" + path.substring(1);
}
}
return path;
}
}

View file

@ -0,0 +1,64 @@
/*
* 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.util;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class OsUtil {
public static final OsArch OS_ARCH = determineOSAndArch();
// TODO: move CMD out of this class
public static final String CMD = OS_ARCH.isWindows() ? "kcadm.bat" : "kcadm.sh";
public static final String PROMPT = OS_ARCH.isWindows() ? "c:\\>" : "$";
public static final String EOL = OS_ARCH.isWindows() ? "\r\n" : "\n";
public static OsArch determineOSAndArch() {
String os = System.getProperty("os.name").toLowerCase();
String arch = System.getProperty("os.arch");
if (arch.equals("amd64")) {
arch = "x86_64";
}
if (os.startsWith("linux")) {
if (arch.equals("x86") || arch.equals("i386") || arch.equals("i586")) {
arch = "i686";
}
return new OsArch("linux", arch);
} else if (os.startsWith("windows")) {
if (arch.equals("x86")) {
arch = "i386";
}
if (os.indexOf("2008") != -1 || os.indexOf("2003") != -1 || os.indexOf("vista") != -1) {
return new OsArch("win32", arch, true);
} else {
return new OsArch("win32", arch);
}
} else if (os.startsWith("sunos")) {
return new OsArch("sunos5", "x86_64");
} else if (os.startsWith("mac os x")) {
return new OsArch("osx", "x86_64");
}
// unsupported platform
throw new RuntimeException("Could not determine OS and architecture for this operating system: " + os);
}
}

View file

@ -0,0 +1,25 @@
/*
* 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.util;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public enum OutputFormat {
JSON,
CSV
}

View file

@ -0,0 +1,107 @@
/*
* 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.util;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.TextNode;
import org.keycloak.util.JsonSerialization;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
import static org.keycloak.client.admin.cli.util.IoUtil.printOut;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class OutputUtil {
public static ObjectMapper MAPPER = new ObjectMapper();
static {
MAPPER.enable(SerializationFeature.INDENT_OUTPUT);
MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
public static JsonNode convertToJsonNode(Object object) throws IOException {
if (object instanceof JsonNode) {
return (JsonNode) object;
}
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
buffer.write(JsonSerialization.writeValueAsBytes(object));
return MAPPER.readValue(buffer.toByteArray(), JsonNode.class);
}
public static void printAsCsv(Object object, ReturnFields fields, boolean unquoted) throws IOException {
JsonNode node = convertToJsonNode(object);
if (!node.isArray()) {
ArrayNode listNode = MAPPER.createArrayNode();
listNode.add(node);
node = listNode;
}
for (JsonNode item: node) {
StringBuilder buffer = new StringBuilder();
printObjectAsCsv(buffer, item, fields, unquoted);
printOut(buffer.length() > 0 ? buffer.substring(1) : "");
}
}
static void printObjectAsCsv(StringBuilder out, JsonNode node, boolean unquoted) {
printObjectAsCsv(out, node, null, unquoted);
}
static void printObjectAsCsv(StringBuilder out, JsonNode node, ReturnFields fields, boolean unquoted) {
if (node.isObject()) {
if (fields == null) {
Iterator<Map.Entry<String, JsonNode>> it = node.fields();
while (it.hasNext()) {
printObjectAsCsv(out, it.next().getValue(), unquoted);
}
} else {
Iterator<String> it = fields.iterator();
while (it.hasNext()) {
String field = it.next();
JsonNode attr = node.get(field);
printObjectAsCsv(out, attr, fields.child(field), unquoted);
}
}
} else if (node.isArray()) {
for (JsonNode item: node) {
printObjectAsCsv(out, item, fields, unquoted);
}
} else if (node != null) {
out.append(",");
if (unquoted && node instanceof TextNode) {
out.append(node.asText());
} else {
out.append(node.toString());
}
}
}
}

View file

@ -0,0 +1,111 @@
/*
* 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.util;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.keycloak.client.admin.cli.common.AttributeOperation;
import org.keycloak.client.admin.cli.common.CmdStdinContext;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
import java.util.List;
import static org.keycloak.client.admin.cli.util.IoUtil.readFileOrStdin;
import static org.keycloak.client.admin.cli.util.ReflectionUtil.setAttributes;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class ParseUtil {
public static String[] parseKeyVal(String keyval) {
// we expect = as a separator
int pos = keyval.indexOf("=");
if (pos <= 0) {
throw new RuntimeException("Invalid key=value parameter: [" + keyval + "]");
}
String [] parsed = new String[2];
parsed[0] = keyval.substring(0, pos);
parsed[1] = keyval.substring(pos+1);
return parsed;
}
public static CmdStdinContext<JsonNode> parseFileOrStdin(String file) {
String content = readFileOrStdin(file).trim();
JsonNode result = null;
if (content.length() == 0) {
throw new RuntimeException("Document provided by --file option is empty");
}
try {
result = JsonSerialization.readValue(content, JsonNode.class);
} catch (JsonParseException e) {
throw new RuntimeException("Not a valid JSON document - " + e.getMessage(), e);
} catch (IOException e) {
throw new RuntimeException("Failed to read the input document as JSON: " + e.getMessage(), e);
} catch (Exception e) {
throw new RuntimeException("Not a valid JSON document", e);
}
CmdStdinContext<JsonNode> ctx = new CmdStdinContext<>();
ctx.setContent(content);
ctx.setResult(result);
return ctx;
}
public static <T> CmdStdinContext<JsonNode> mergeAttributes(CmdStdinContext<JsonNode> ctx, ObjectNode newObject, List<AttributeOperation> attrs) {
String content = ctx.getContent();
JsonNode node = ctx.getResult();
if (node != null && !node.isObject()) {
throw new RuntimeException("Not a JSON object: " + node);
}
ObjectNode result = (ObjectNode) node;
try {
if (result == null) {
try {
result = newObject;
} catch (Throwable e) {
throw new RuntimeException("Failed to instantiate object: " + e.getMessage(), e);
}
}
if (result != null) {
try {
setAttributes(result, attrs);
} catch (AttributeException e) {
throw new RuntimeException("Failed to set attribute '" + e.getAttributeName() + "' on document type '" + result.getClass().getName() + "'", e);
}
content = JsonSerialization.writeValueAsString(result);
} else {
throw new RuntimeException("Setting attributes is not supported for type: " + result.getClass().getName());
}
} catch (IOException e) {
throw new RuntimeException("Failed to merge set attributes with configuration from file", e);
}
ctx.setContent(content);
ctx.setResult(result);
return ctx;
}
}

View file

@ -0,0 +1,228 @@
/*
* 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.util;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.BooleanNode;
import com.fasterxml.jackson.databind.node.DoubleNode;
import com.fasterxml.jackson.databind.node.LongNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import org.keycloak.client.admin.cli.common.AttributeKey;
import org.keycloak.client.admin.cli.common.AttributeOperation;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.SET;
import static org.keycloak.client.admin.cli.util.OutputUtil.MAPPER;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class ReflectionUtil {
public static void setAttributes(JsonNode client, List<AttributeOperation> attrs) {
for (AttributeOperation item: attrs) {
AttributeKey attr = item.getKey();
JsonNode nested = client;
List<AttributeKey.Component> cs = attr.getComponents();
for (int i = 0; i < cs.size(); i++) {
AttributeKey.Component c = cs.get(i);
// if this is the last component of the name,
// then if SET we need to set value on nested:
// if value already set on nested, then overwrite, maybe remove node + add new node
// if DELETE we need to remove or nullify value (if isArray)
// else get child and
// if exist set nested to child
// else if SET create new empty object or array - depending on c.isArray()
//
// if this is the last component of the name
if (i == cs.size() - 1) {
String val = item.getValue();
ObjectNode obj = (ObjectNode) nested;
if (SET == item.getType()) {
JsonNode valNode = valueToJsonNode(val);
if (c.isArray() || attr.isAppend()) {
JsonNode list = obj.get(c.getName());
// child expected to be an array
if ( ! (list instanceof ArrayNode)) {
// replace with new array
list = MAPPER.createArrayNode();
obj.set(c.getName(), list);
}
setArrayItem((ArrayNode) list, c.getIndex(), valNode);
} else {
((ObjectNode) nested).set(c.getName(), valNode);
}
} else {
// type == DELETE
if (c.isArray()) {
JsonNode list = obj.get(c.getName());
// child expected to be an array
if (list instanceof ArrayNode) {
removeArrayItem((ArrayNode) list, c.getIndex());
}
} else {
obj.remove(c.getName());
}
}
} else {
// get child and
// if exist set nested to child
// else create new empty object or array - depending on c.isArray()
JsonNode node = nested.get(c.getName());
if (node == null) {
if (c.isArray()) {
node = MAPPER.createArrayNode();
} else {
node = MAPPER.createObjectNode();
}
((ObjectNode) nested).set(c.getName(), node);
}
nested = node;
}
}
}
}
private static void setArrayItem(ArrayNode list, int index, JsonNode valNode) {
if (index == -1) {
// append to end of array
list.add(valNode);
return;
}
// make sure items up to index exist
for (int i = list.size(); i < index+1; i++) {
list.add(NullNode.instance);
}
list.set(index, valNode);
}
private static void removeArrayItem(ArrayNode list, int index) {
if (index == -1) {
throw new IllegalArgumentException("Internal error - should never be called with index == -1");
}
list.remove(index);
}
private static JsonNode valueToJsonNode(String val) {
// try get value as JSON object
try {
return MAPPER.readValue(val, ObjectNode.class);
} catch (Exception ignored) {
}
// try get value as JSON array
try {
return MAPPER.readValue(val, ArrayNode.class);
} catch (Exception ignored) {
}
if (isBoolean(val)) {
return BooleanNode.valueOf(Boolean.valueOf(val));
} else if (isInteger(val)) {
return LongNode.valueOf(Long.valueOf(val));
} else if (isNumber(val)) {
return DoubleNode.valueOf(Double.valueOf(val));
} else if (isQuoted(val)) {
return TextNode.valueOf(unquote(val));
}
return TextNode.valueOf(val);
}
private static boolean isInteger(String val) {
try {
Long.valueOf(val);
return true;
} catch (Exception ignored) {
return false;
}
}
private static boolean isNumber(String val) {
try {
Double.valueOf(val);
return true;
} catch (Exception ignored) {
return false;
}
}
private static boolean isBoolean(String val) {
return "false".equals(val) || "true".equals(val);
}
private static boolean isQuoted(String val) {
return val.startsWith("'") || val.startsWith("\"");
}
private static String unquote(String val) {
if (!(val.startsWith("'") || val.startsWith("\"")) || !(val.endsWith("'") || val.endsWith("\""))) {
throw new RuntimeException("Invalid string value: " + val);
}
return val.substring(1, val.length()-1);
}
public static void merge(JsonNode source, ObjectNode dest) {
// Iterate over source
// For each child check if exists on the destination
// if it does go deep
// otherwise copy over
// if it's last component, set it on destination
if (!source.isObject()) {
throw new RuntimeException("Not a JSON object: " + source);
}
Iterator<Map.Entry<String, JsonNode>> it = ((ObjectNode) source).fields();
while (it.hasNext()) {
Map.Entry<String, JsonNode> item = it.next();
String name = item.getKey();
JsonNode node = item.getValue();
JsonNode destNode = dest.get(name);
if (destNode != null) {
if (destNode.isObject()) {
if (node.isObject()) {
merge(node, (ObjectNode) destNode);
} else {
throw new RuntimeException("Attribute is of incompatible type - " + name + ": " + node);
}
} else if (destNode.isArray()) {
if (node.isArray()) {
dest.set(name, node);
} else {
throw new RuntimeException("Attribute is of incompatible type - " + name + ": " + node);
}
} else {
dest.set(name, node);
}
} else {
dest.set(name, node);
}
}
}
}

View file

@ -0,0 +1,333 @@
/*
* 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.util;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
/**
* @author <a href="mailto:marko.strukelj@gmail.com">Marko Strukelj</a>
*/
public class ReturnFields implements Iterable<String> {
public static ReturnFields ALL = new ReturnFields() {
@Override
public ReturnFields child(String field) {
return NONE;
}
@Override
public boolean included(String... pathSegments) {
return true;
}
@Override
public boolean excluded(String field) {
return false;
}
@Override
public Iterator<String> iterator() {
return Collections.singletonList("*").iterator();
}
@Override
public boolean isEmpty() {
return false;
}
public boolean isAll() {
return true;
}
@Override
public String toString() {
return "[ReturnFields ALL]";
}
};
public static ReturnFields NONE = new ReturnFields() {
@Override
public ReturnFields child(String field) {
return this;
}
@Override
public boolean included(String... pathSegments) {
return false;
}
@Override
public boolean excluded(String field) {
return false;
}
@Override
public Iterator<String> iterator() {
List<String> emptyList = Collections.emptyList();
return emptyList.iterator();
}
@Override
public boolean isEmpty() {
return true;
}
@Override
public boolean isAll() {
return false;
}
@Override
public String toString() {
return "[ReturnFields NONE]";
}
};
public static ReturnFields ALL_RECURSIVELY = new ReturnFields() {
@Override
public ReturnFields child(String field) {
return this;
}
@Override
public boolean included(String... pathSegments) {
return true;
}
@Override
public boolean excluded(String field) {
return false;
}
@Override
public Iterator<String> iterator() {
List<String> emptyList = Collections.emptyList();
return emptyList.iterator();
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public boolean isAll() {
return true;
}
};
private enum TargetState {
IdentCommaOpen,
Ident,
Comma,
Anything
}
private enum FieldState {
start,
name,
end
}
private HashMap<String, ReturnFields> fields = new LinkedHashMap<>();
public ReturnFields() {}
public ReturnFields(String spec) {
if (spec == null || spec.trim().length() == 0) {
throw new IllegalArgumentException("Fields spec is null or empty!");
}
// parse the spec, building up the tree for nested children
char[] buf = spec.toCharArray();
StringBuilder token = new StringBuilder(buf.length);
// stack for handling depth
LinkedList<HashMap<String, ReturnFields>> specs = new LinkedList<>();
specs.add(fields);
// parser state
FieldState fldState = FieldState.start;
TargetState state = TargetState.Ident;
int i;
for (i = 0; i < buf.length; i++) {
char c = buf[i];
if (c == ',') {
if (state == TargetState.Ident) {
error(spec, i);
}
if (fldState == FieldState.name) {
specs.getLast().put(token.toString(), null);
token.setLength(0);
}
state = TargetState.Ident;
fldState = FieldState.start;
} else if (c == '(') {
if (state != TargetState.IdentCommaOpen && state != TargetState.Anything) {
error(spec, i);
}
ReturnFields sub = new ReturnFields();
specs.getLast().put(token.toString(), sub);
specs.add(sub.fields);
token.setLength(0);
state = TargetState.Ident;
fldState = FieldState.start;
} else if (c == ')') {
if (state != TargetState.Anything) {
error(spec, i);
}
if (fldState == FieldState.name) {
specs.getLast().put(token.toString(), null);
token.setLength(0);
}
specs.removeLast();
fldState = FieldState.end;
state = specs.size() > 1 ? TargetState.Anything : TargetState.Comma;
} else {
token.append(c);
if (fldState == FieldState.start) {
fldState = FieldState.name;
state = specs.size() > 1 ? TargetState.Anything : TargetState.IdentCommaOpen;
}
}
}
if (specs.size() > 1) {
error(spec, i);
}
if (token.length() > 0) {
specs.getLast().put(token.toString(), null);
} else if (!(state == TargetState.Anything || state == TargetState.Comma)) {
error(spec, i);
}
}
private void error(String spec, int i) {
throw new RuntimeException("Invalid fields specification at position " + i + ": " + spec);
}
/**
* Get ReturnFields for a child field of JSONObject type.
*
* <p>For basic-typed fields this always returns null. Use included() for those.</p>
*
* @param field The child field name for nested returns.
* @return ReturnFields for a child field
*/
public ReturnFields child(String field) {
ReturnFields returnFields = fields.get(field);
if (returnFields == null) {
returnFields = fields.get("*");
if (returnFields == null) {
returnFields = ReturnFields.NONE;
}
}
return returnFields;
}
/**
* Check to see if the field should be included in JSON response.
*
* <p>The check can be performed for any level of depth relative to current nesting level, by specifying multiple path segments.</p>
*
* @param pathSegments Segments to test in the tree of return fields.
* @return true if the specified path should be part of JSON response or not
*/
public boolean included(String... pathSegments) {
if (pathSegments == null || pathSegments.length == 0) {
throw new IllegalArgumentException("No path specified!");
}
ReturnFields current = this;
for (String path : pathSegments) {
if (current == null) {
return false;
}
if (current.fields.containsKey("-" + path)) {
return false;
}
if (current.fields.containsKey("*")) {
return true;
}
if (!current.fields.containsKey(path)) {
return false;
}
current = current.fields.get(path);
}
return true;
}
/**
* Check to see if the field specified is set to be explicitly excluded.
* @param field The field name to check
* @return If the field was explicitly set to be excluded
*/
public boolean excluded(String field) {
if (fields.containsKey("-" + field)) {
return true;
} else {
return false;
}
}
/**
* Iterate over child fields to be included in response.
*
* <p>To get nested field specifier use child(name) passing the field name this iterator returns.</p>
*
* @return iterator over child fields to be included in response.
*/
public Iterator<String> iterator() {
return fields.keySet().iterator();
}
/**
* Determine if zero fields should be returned.
*
* @return <code>true</code> if the list is empty, else, <code>false</code>
*/
public boolean isEmpty() {
return this.fields.isEmpty();
}
public boolean isAll() {
return this.fields.keySet().contains("*");
}
@Override
public String toString() {
return "[ReturnFieldsImpl: fields=" + this.fields + "]";
}
}

View file

@ -0,0 +1,23 @@
org.jboss.resteasy.plugins.providers.jackson.ResteasyJackson2Provider
org.jboss.resteasy.plugins.providers.DataSourceProvider
org.jboss.resteasy.plugins.providers.DocumentProvider
org.jboss.resteasy.plugins.providers.DefaultTextPlain
org.jboss.resteasy.plugins.providers.StringTextStar
org.jboss.resteasy.plugins.providers.SourceProvider
org.jboss.resteasy.plugins.providers.InputStreamProvider
org.jboss.resteasy.plugins.providers.ReaderProvider
org.jboss.resteasy.plugins.providers.ByteArrayProvider
org.jboss.resteasy.plugins.providers.FormUrlEncodedProvider
org.jboss.resteasy.plugins.providers.JaxrsFormProvider
org.jboss.resteasy.plugins.providers.FileProvider
org.jboss.resteasy.plugins.providers.FileRangeWriter
org.jboss.resteasy.plugins.providers.StreamingOutputProvider
org.jboss.resteasy.plugins.providers.IIOImageProvider
org.jboss.resteasy.plugins.providers.SerializableProvider
org.jboss.resteasy.plugins.interceptors.CacheControlFeature
org.jboss.resteasy.plugins.interceptors.encoding.AcceptEncodingGZIPInterceptor
org.jboss.resteasy.plugins.interceptors.encoding.AcceptEncodingGZIPFilter
org.jboss.resteasy.plugins.interceptors.encoding.ClientContentEncodingAnnotationFeature
org.jboss.resteasy.plugins.interceptors.encoding.GZIPDecodingInterceptor
org.jboss.resteasy.plugins.interceptors.encoding.GZIPEncodingInterceptor
org.jboss.resteasy.plugins.interceptors.encoding.ServerContentEncodingAnnotationFeature

View file

@ -0,0 +1,101 @@
/*
* 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.util;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Test;
import org.keycloak.client.admin.cli.common.AttributeOperation;
import org.keycloak.client.admin.cli.common.CmdStdinContext;
import java.nio.charset.Charset;
import java.util.LinkedList;
import java.util.List;
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.OutputUtil.MAPPER;
import static org.keycloak.client.admin.cli.util.ParseUtil.mergeAttributes;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class MergeAttributesTest {
@Test
public void testMergeAttrs() throws Exception {
List<AttributeOperation> attrs = new LinkedList<>();
attrs.add(new AttributeOperation(SET, "realm", "nurealm"));
attrs.add(new AttributeOperation(SET, "enabled", "true"));
attrs.add(new AttributeOperation(SET, "revokeRefreshToken", "true"));
attrs.add(new AttributeOperation(SET, "accessTokenLifespan", "900"));
attrs.add(new AttributeOperation(SET, "smtpServer.host", "localhost"));
attrs.add(new AttributeOperation(SET, "extra.key1", "somevalue"));
attrs.add(new AttributeOperation(SET, "extra.key2", "[\"somevalue\"]"));
attrs.add(new AttributeOperation(SET, "extra.key3[1]", "second item"));
attrs.add(new AttributeOperation(SET, "extra.key4", "\"true\""));
attrs.add(new AttributeOperation(SET, "extra.key5", "\"1000\""));
attrs.add(new AttributeOperation(DELETE, "id"));
attrs.add(new AttributeOperation(DELETE, "attributes.\"_browser_header.xFrameOptions\""));
String localJSON = "{\n" +
" \"id\" : \"24e5d572-756a-435b-8b2b-edbd0a7aa93d\",\n" +
" \"realm\" : \"demorealm\",\n" +
" \"notBefore\" : 0,\n" +
" \"revokeRefreshToken\" : false,\n" +
" \"accessTokenLifespan\" : 300,\n" +
" \"defaultRoles\" : [ \"offline_access\", \"uma_authorization\" ],\n" +
" \"smtpServer\" : { },\n" +
" \"attributes\" : {\n" +
" \"_browser_header.xFrameOptions\" : \"SAMEORIGIN\",\n" +
" \"_browser_header.contentSecurityPolicy\" : \"frame-src 'self'\"\n" +
" }\n" +
"}";
ObjectNode localNode = MAPPER.readValue(localJSON.getBytes(Charset.forName("utf-8")), ObjectNode.class);
CmdStdinContext<JsonNode> ctx = new CmdStdinContext<>();
ctx.setResult(localNode);
ctx = mergeAttributes(ctx, MAPPER.createObjectNode(), attrs);
System.out.println(ctx);
String remoteJSON = "{\n" +
" \"id\" : \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\",\n" +
" \"realm\" : \"demorealm\",\n" +
" \"notBefore\" : 0,\n" +
" \"revokeRefreshToken\" : false,\n" +
" \"accessTokenLifespan\" : 300,\n" +
" \"defaultRoles\" : [ \"uma_authorization\" ],\n" +
" \"remote\" : \"value\",\n" +
" \"attributes\" : {\n" +
" \"_browser_header.xFrameOptions\" : \"SAMEORIGIN\",\n" +
" \"_browser_header.x\" : \"ORIGIN\",\n" +
" \"_browser_header.contentSecurityPolicy\" : \"frame-src 'self'\"\n" +
" }\n" +
"}";
ObjectNode remoteNode = MAPPER.readValue(remoteJSON.getBytes(Charset.forName("utf-8")), ObjectNode.class);
CmdStdinContext<ObjectNode> ctxremote = new CmdStdinContext<>();
ctxremote.setResult(remoteNode);
ReflectionUtil.merge(ctx.getResult(), ctxremote.getResult());
System.out.println(ctx);
//ctx = mergeAttributes(ctx, MAPPER.createObjectNode(), attrs);
}
}

View file

@ -0,0 +1,142 @@
/*
* 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.util;
import org.junit.Assert;
import org.junit.Test;
/**
* @author <a href="mailto:marko.strukelj@gmail.com">Marko Strukelj</a>
*/
public class ReturnFieldsTest {
@Test
public void testBasic() {
String spec = "field1,field2,field3";
ReturnFields fspec = new ReturnFields(spec);
StringBuilder val = new StringBuilder();
for (String field : fspec) {
if (val.length() > 0)
val.append(',');
val.append(field);
}
Assert.assertEquals(spec, val.toString());
// check catching errors
String[] specs = {
"",
null,
",",
"field1,",
",field2"
};
for (String filter : specs) {
try {
fspec = new ReturnFields(filter);
Assert.fail("Parsing of fields spec should have failed! : " + filter);
} catch (Exception e) {
//e.printStackTrace();
}
}
}
@Test
public void testExclude() {
ReturnFields spec = new ReturnFields("*,-name,dog(*,-color)");
Assert.assertTrue(spec.included("foo"));
Assert.assertTrue(spec.included("bar"));
Assert.assertFalse(spec.included("name"));
Assert.assertTrue(spec.included("dog"));
Assert.assertTrue(spec.child("dog").included("breed"));
Assert.assertFalse(spec.child("dog").included("color"));
Assert.assertTrue(spec.excluded("name"));
Assert.assertFalse(spec.excluded("foo"));
Assert.assertFalse(spec.excluded("bar"));
Assert.assertTrue(spec.child("dog").excluded("color"));
Assert.assertFalse(spec.child("dog").excluded("breed"));
}
@Test
public void testNestedWithGlob() {
ReturnFields spec = new ReturnFields("name,dog(*)");
Assert.assertTrue(spec.included("name"));
Assert.assertFalse(spec.included("tacos"));
Assert.assertNotNull(spec.child("dog"));
Assert.assertTrue(spec.child("dog").included("dogname"));
Assert.assertNotNull(spec.child("cat"));
Assert.assertFalse(spec.child("cat").included("name"));
}
@Test
public void testNested() {
String spec = "field1,field2(sub1,sub2(subsub1)),field3";
ReturnFields fspec = new ReturnFields(spec);
String val = traverse(fspec);
Assert.assertEquals(spec, val.toString());
// check catching errors
String[] specs = {
"(",
")",
"field1,(",
"field1,)",
"field1,field2(",
"field1,field2)",
"field1,field2()",
"field1,field2(sub1)(",
"field1,field2(sub1))",
"field1,field2(sub1),"
};
for (String filter : specs) {
try {
fspec = new ReturnFields(filter);
Assert.fail("Parsing of fields spec should have failed! : " + filter);
} catch (Exception e) {
//e.printStackTrace();
}
}
}
private String traverse(ReturnFields fspec) {
StringBuilder buf = new StringBuilder();
for (String field : fspec) {
if (buf.length() > 0)
buf.append(',');
buf.append(field);
ReturnFields cspec = fspec.child(field);
if (cspec != null && cspec != ReturnFields.NONE) {
buf.append('(');
buf.append(traverse(cspec));
buf.append(')');
}
}
return buf.toString();
}
}

View file

@ -36,11 +36,23 @@
<outputDirectory>keycloak-client-tools/bin</outputDirectory>
<filtered>true</filtered>
</file>
<file>
<source>../admin-cli/src/main/bin/kcadm.sh</source>
<outputDirectory>keycloak-client-tools/bin</outputDirectory>
<fileMode>0755</fileMode>
<filtered>true</filtered>
</file>
<file>
<source>../admin-cli/src/main/bin/kcadm.bat</source>
<outputDirectory>keycloak-client-tools/bin</outputDirectory>
<filtered>true</filtered>
</file>
</files>
<dependencySets>
<dependencySet>
<includes>
<include>org.keycloak:keycloak-client-registration-cli</include>
<include>org.keycloak:keycloak-admin-cli</include>
</includes>
<outputDirectory>keycloak-client-tools/bin/client</outputDirectory>
</dependencySet>

View file

@ -34,6 +34,10 @@
<groupId>org.keycloak</groupId>
<artifactId>keycloak-client-registration-cli</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-cli</artifactId>
</dependency>
</dependencies>
<build>

View file

@ -33,7 +33,6 @@
<dependency>
<groupId>org.jboss.aesh</groupId>
<artifactId>aesh</artifactId>
<version>0.66.10</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>

View file

@ -1,3 +1,19 @@
/*
* 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.registration.cli.commands;
import org.jboss.aesh.cl.CommandDefinition;
@ -153,7 +169,7 @@ public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd implements Comma
public static String usage() {
StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
out.println("Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWOD] [ARGUMENTS]");
out.println("Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWORD] [ARGUMENTS]");
out.println();
out.println("Command to configure a global truststore to use when using https to connect to Keycloak server.");
out.println();
@ -174,7 +190,7 @@ public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd implements Comma
out.println("Specify a truststore - you will be prompted for truststore password every time it is used:");
out.println(" " + PROMPT + " " + CMD + " config truststore " + OS_ARCH.path("~/.keycloak/truststore.jks"));
out.println();
out.println("Specify a truststore, and password - truststore will automatically without prompting for password:");
out.println("Specify a truststore, and password - truststore will automatically be used without prompting for password:");
out.println(" " + PROMPT + " " + CMD + " config truststore --storepass " + OS_ARCH.envVar("PASSWORD") + " " + OS_ARCH.path("~/.keycloak/truststore.jks"));
out.println();
out.println("Remove truststore configuration:");

View file

@ -30,8 +30,19 @@
<artifactId>keycloak-client-cli-parent</artifactId>
<packaging>pom</packaging>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jboss.aesh</groupId>
<artifactId>aesh</artifactId>
<version>0.66.10</version>
</dependency>
</dependencies>
</dependencyManagement>
<modules>
<module>client-registration-cli</module>
<module>admin-cli</module>
<module>client-cli-dist</module>
</modules>
</project>

View file

@ -1321,6 +1321,11 @@
<artifactId>keycloak-client-registration-cli</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-cli</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-client-cli-dist</artifactId>

View file

@ -0,0 +1,58 @@
package org.keycloak.testsuite.cli;
import org.keycloak.testsuite.cli.exec.AbstractExec;
import org.keycloak.testsuite.cli.exec.AbstractExecBuilder;
import java.io.InputStream;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class KcAdmExec extends AbstractExec {
public static final String WORK_DIR = System.getProperty("user.dir") + "/target/containers/keycloak-client-tools";
public static final String CMD = OS_ARCH.isWindows() ? "kcadm.bat" : "kcadm.sh";
private KcAdmExec(String workDir, String argsLine, InputStream stdin) {
this(workDir, argsLine, null, stdin);
}
private KcAdmExec(String workDir, String argsLine, String env, InputStream stdin) {
super(workDir, argsLine, env, stdin);
}
@Override
public String getCmd() {
return "bin/" + CMD;
}
public static KcAdmExec.Builder newBuilder() {
return (KcAdmExec.Builder) new KcAdmExec.Builder().workDir(WORK_DIR);
}
public static KcAdmExec execute(String args) {
return newBuilder()
.argsLine(args)
.execute();
}
public static class Builder extends AbstractExecBuilder<KcAdmExec> {
@Override
public KcAdmExec execute() {
KcAdmExec exe = new KcAdmExec(workDir, argsLine, env, stdin);
exe.dumpStreams = dumpStreams;
exe.execute();
return exe;
}
@Override
public KcAdmExec executeAsync() {
KcAdmExec exe = new KcAdmExec(workDir, argsLine, env, stdin);
exe.dumpStreams = dumpStreams;
exe.executeAsync();
return exe;
}
}
}

View file

@ -1,5 +1,8 @@
package org.keycloak.testsuite.cli;
import org.keycloak.testsuite.cli.exec.AbstractExec;
import org.keycloak.testsuite.cli.exec.AbstractExecBuilder;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@ -18,57 +21,27 @@ import java.util.concurrent.TimeUnit;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class KcRegExec {
public class KcRegExec extends AbstractExec {
public static final String WORK_DIR = System.getProperty("user.dir") + "/target/containers/keycloak-client-tools";
public static final OsArch OS_ARCH = OsUtils.determineOSAndArch();
public static final String CMD = OS_ARCH.isWindows() ? "kcreg.bat" : "kcreg.sh";
private long waitTimeout = 30000;
private Process process;
private int exitCode = -1;
private boolean logStreams = Boolean.valueOf(System.getProperty("cli.log.output", "true"));
private boolean dumpStreams;
private String workDir = WORK_DIR;
private String env;
private String argsLine;
private ByteArrayOutputStream stdout = new ByteArrayOutputStream();
private ByteArrayOutputStream stderr = new ByteArrayOutputStream();
private InputStream stdin = new InteractiveInputStream();
private Throwable err;
private KcRegExec(String workDir, String argsLine, InputStream stdin) {
this(workDir, argsLine, null, stdin);
}
private KcRegExec(String workDir, String argsLine, String env, InputStream stdin) {
if (workDir != null) {
this.workDir = workDir;
super(workDir, argsLine, env, stdin);
}
this.argsLine = argsLine;
this.env = env;
if (stdin != null) {
this.stdin = stdin;
}
@Override
public String getCmd() {
return "bin/" + CMD;
}
public static Builder newBuilder() {
return new Builder();
public static KcRegExec.Builder newBuilder() {
return (KcRegExec.Builder) new KcRegExec.Builder().workDir(WORK_DIR);
}
public static KcRegExec execute(String args) {
@ -77,225 +50,9 @@ public class KcRegExec {
.execute();
}
public void execute() {
executeAsync();
if (err == null) {
waitCompletion();
}
}
public void executeAsync() {
try {
if (OS_ARCH.isWindows()) {
String cmd = (env != null ? "set " + env + " & " : "") + "bin\\" + CMD + " " + fixQuotes(argsLine);
System.out.println("Executing: cmd.exe /c " + cmd);
process = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", cmd}, null, new File(workDir));
} else {
String cmd = (env != null ? env + " " : "") + "bin/" + CMD + " " + argsLine;
System.out.println("Executing: sh -c " + cmd);
process = Runtime.getRuntime().exec(new String[]{"sh", "-c", cmd}, null, new File(workDir));
}
new StreamReaderThread(process.getInputStream(), logStreams ? new LoggingOutputStream("STDOUT", stdout) : stdout)
.start();
new StreamReaderThread(process.getErrorStream(), logStreams ? new LoggingOutputStream("STDERR", stderr) : stderr)
.start();
new StreamReaderThread(stdin, process.getOutputStream())
.start();
} catch (Throwable t) {
err = t;
}
}
private String fixQuotes(String argsLine) {
argsLine = argsLine + " ";
argsLine = argsLine.replaceAll("\"", "\\\\\"");
argsLine = argsLine.replaceAll(" '", " \"");
argsLine = argsLine.replaceAll("' ", "\" ");
return argsLine;
}
public void waitCompletion() {
//if (stdin instanceof InteractiveInputStream) {
// ((InteractiveInputStream) stdin).close();
//}
try {
if (process.waitFor(waitTimeout, TimeUnit.MILLISECONDS)) {
exitCode = process.exitValue();
if (exitCode != 0) {
dumpStreams = true;
}
} else {
if (process.isAlive()) {
process.destroyForcibly();
}
throw new RuntimeException("Timeout after " + (waitTimeout / 1000) + " seconds.");
}
} catch (InterruptedException e) {
dumpStreams = true;
throw new RuntimeException("Interrupted ...", e);
} catch (Throwable t) {
dumpStreams = true;
err = t;
} finally {
if (!logStreams && dumpStreams) try {
System.out.println("STDOUT: ");
copyStream(new ByteArrayInputStream(stdout.toByteArray()), System.out);
System.out.println("STDERR: ");
copyStream(new ByteArrayInputStream(stderr.toByteArray()), System.out);
} catch (Exception ignored) {
}
}
}
public int exitCode() {
return exitCode;
}
public Throwable error() {
return err;
}
public InputStream stdout() {
return new ByteArrayInputStream(stdout.toByteArray());
}
public List<String> stdoutLines() {
return parseStreamAsLines(new ByteArrayInputStream(stdout.toByteArray()));
}
public String stdoutString() {
return new String(stdout.toByteArray());
}
public InputStream stderr() {
return new ByteArrayInputStream(stderr.toByteArray());
}
public List<String> stderrLines() {
return parseStreamAsLines(new ByteArrayInputStream(stderr.toByteArray()));
}
public String stderrString() {
return new String(stderr.toByteArray());
}
static List<String> parseStreamAsLines(InputStream stream) {
List<String> lines = new ArrayList<>();
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
String line;
while ((line = reader.readLine()) != null) {
lines.add(line);
}
return lines;
} catch (IOException e) {
throw new RuntimeException("Unexpected I/O error", e);
}
}
public void waitForStdout(String content) {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < waitTimeout) {
if (stdoutString().indexOf(content) != -1) {
return;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted ...", e);
}
}
throw new RuntimeException("Timed while waiting for content to appear in stdout");
}
public void sendToStdin(String s) {
if (stdin instanceof InteractiveInputStream) {
((InteractiveInputStream) stdin).pushBytes(s.getBytes());
} else {
throw new RuntimeException("Can't push to stdin - not interactive");
}
}
static class StreamReaderThread extends Thread {
private InputStream is;
private OutputStream os;
StreamReaderThread(InputStream is, OutputStream os) {
this.is = is;
this.os = os;
}
public void run() {
try {
copyStream(is, os);
} catch (IOException e) {
throw new RuntimeException("Unexpected I/O error", e);
} finally {
try {
os.close();
} catch (IOException ignored) {
System.err.print("IGNORED: error while closing output stream: ");
ignored.printStackTrace();
}
}
}
}
static void copyStream(InputStream is, OutputStream os) throws IOException {
byte [] buf = new byte[8192];
try (InputStream iss = is) {
int c;
while ((c = iss.read(buf)) != -1) {
os.write(buf, 0, c);
os.flush();
}
}
}
public static class Builder {
private String workDir;
private String argsLine;
private InputStream stdin;
private String env;
private boolean dumpStreams;
public Builder workDir(String path) {
this.workDir = path;
return this;
}
public Builder argsLine(String cmd) {
this.argsLine = cmd;
return this;
}
public Builder stdin(InputStream is) {
this.stdin = is;
return this;
}
public Builder env(String env) {
this.env = env;
return this;
}
public Builder fullStreamDump() {
this.dumpStreams = true;
return this;
}
public static class Builder extends AbstractExecBuilder<KcRegExec> {
@Override
public KcRegExec execute() {
KcRegExec exe = new KcRegExec(workDir, argsLine, env, stdin);
exe.dumpStreams = dumpStreams;
@ -303,6 +60,7 @@ public class KcRegExec {
return exe;
}
@Override
public KcRegExec executeAsync() {
KcRegExec exe = new KcRegExec(workDir, argsLine, env, stdin);
exe.dumpStreams = dumpStreams;
@ -311,177 +69,4 @@ public class KcRegExec {
}
}
static class NullInputStream extends InputStream {
@Override
public int read() throws IOException {
return -1;
}
}
static class InteractiveInputStream extends InputStream {
private LinkedList<Byte> queue = new LinkedList<>();
private Thread consumer;
private boolean closed;
@Override
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
@Override
public synchronized int read(byte[] b, int off, int len) throws IOException {
Byte current = null;
int rc = 0;
try {
consumer = Thread.currentThread();
do {
current = queue.poll();
if (current == null) {
if (rc > 0) {
return rc;
} else {
do {
if (closed) {
return -1;
}
wait();
}
while ((current = queue.poll()) == null);
}
}
b[off + rc] = current;
rc++;
} while (rc < len);
} catch (InterruptedException e) {
throw new InterruptedIOException("Signalled to exit");
} finally {
consumer = null;
}
return rc;
}
@Override
public long skip(long n) throws IOException {
return super.skip(n);
}
@Override
public int available() throws IOException {
return super.available();
}
@Override
public synchronized void mark(int readlimit) {
super.mark(readlimit);
}
@Override
public synchronized void reset() throws IOException {
super.reset();
}
@Override
public boolean markSupported() {
return super.markSupported();
}
@Override
public synchronized int read() throws IOException {
if (closed) {
return -1;
}
// when input is available pass it on
Byte current;
try {
consumer = Thread.currentThread();
while ((current = queue.poll()) == null) {
wait();
if (closed) {
return -1;
}
}
} catch (InterruptedException e) {
throw new InterruptedIOException("Signalled to exit");
} finally {
consumer = null;
}
return current;
}
@Override
public synchronized void close() {
closed = true;
new RuntimeException("IIS || close").printStackTrace();
if (consumer != null) {
consumer.interrupt();
}
}
public synchronized void pushBytes(byte [] buff) {
for (byte b : buff) {
queue.add(b);
}
notify();
}
}
static class LoggingOutputStream extends FilterOutputStream {
private ByteArrayOutputStream buffer = new ByteArrayOutputStream();
private String name;
public LoggingOutputStream(String name, OutputStream os) {
super(os);
this.name = name;
}
@Override
public void write(int b) throws IOException {
super.write(b);
if (b == 10) {
log();
} else {
buffer.write(b);
}
}
@Override
public void write(byte[] buf) throws IOException {
write(buf, 0, buf.length);
}
@Override
public void write(byte[] buf, int offs, int len) throws IOException {
for (int i = 0; i < len; i++) {
write(buf[offs+i]);
}
}
@Override
public void close() throws IOException {
super.close();
if (buffer.size() > 0) {
log();
}
}
private void log() {
String log = new String(buffer.toByteArray());
buffer.reset();
System.out.println("[" + name + "] " + log);
}
}
}

View file

@ -0,0 +1,245 @@
package org.keycloak.testsuite.cli.exec;
import org.keycloak.testsuite.cli.OsArch;
import org.keycloak.testsuite.cli.OsUtils;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public abstract class AbstractExec {
public static final String WORK_DIR = System.getProperty("user.dir");
public static final OsArch OS_ARCH = OsUtils.determineOSAndArch();
private long waitTimeout = 30000;
private Process process;
private int exitCode = -1;
private boolean logStreams = Boolean.valueOf(System.getProperty("cli.log.output", "true"));
protected boolean dumpStreams;
protected String workDir = WORK_DIR;
private String env;
private String argsLine;
private ByteArrayOutputStream stdout = new ByteArrayOutputStream();
private ByteArrayOutputStream stderr = new ByteArrayOutputStream();
private InputStream stdin = new InteractiveInputStream();
private Throwable err;
private Thread stdoutRunner;
private Thread stderrRunner;
public AbstractExec(String workDir, String argsLine, InputStream stdin) {
this(workDir, argsLine, null, stdin);
}
public AbstractExec(String workDir, String argsLine, String env, InputStream stdin) {
if (workDir != null) {
this.workDir = workDir;
}
this.argsLine = argsLine;
this.env = env;
if (stdin != null) {
this.stdin = stdin;
}
}
public abstract String getCmd();
public void execute() {
executeAsync();
if (err == null) {
waitCompletion();
}
}
public void executeAsync() {
try {
if (OS_ARCH.isWindows()) {
String cmd = (env != null ? "set " + env + " & " : "") + fixPath(getCmd()) + " " + fixQuotes(argsLine);
System.out.println("Executing: cmd.exe /c " + cmd);
process = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", cmd}, null, new File(workDir));
} else {
String cmd = (env != null ? env + " " : "") + getCmd() + " " + argsLine;
System.out.println("Executing: sh -c " + cmd);
process = Runtime.getRuntime().exec(new String[]{"sh", "-c", cmd}, null, new File(workDir));
}
stdoutRunner = new StreamReaderThread(process.getInputStream(), logStreams ? new LoggingOutputStream("STDOUT", stdout) : stdout);
stdoutRunner.start();
stderrRunner = new StreamReaderThread(process.getErrorStream(), logStreams ? new LoggingOutputStream("STDERR", stderr) : stderr);
stderrRunner.start();
new StreamReaderThread(stdin, process.getOutputStream())
.start();
} catch (Throwable t) {
err = t;
}
}
private String fixPath(String cmd) {
return cmd.replaceAll("/", "\\\\");
}
private String fixQuotes(String argsLine) {
argsLine = argsLine + " ";
argsLine = argsLine.replaceAll("\"", "\\\\\"");
argsLine = argsLine.replaceAll(" '", " \"");
argsLine = argsLine.replaceAll("' ", "\" ");
return argsLine;
}
public void waitCompletion() {
// This is necessary to make sure the process isn't stuck reading from stdin
if (stdin instanceof InteractiveInputStream) {
((InteractiveInputStream) stdin).close();
}
try {
if (process.waitFor(waitTimeout, TimeUnit.MILLISECONDS)) {
exitCode = process.exitValue();
if (exitCode != 0) {
dumpStreams = true;
}
// make sure reading output is really done (just in case)
stdoutRunner.join(5000);
stderrRunner.join(5000);
} else {
if (process.isAlive()) {
process.destroyForcibly();
}
throw new RuntimeException("Timeout after " + (waitTimeout / 1000) + " seconds.");
}
} catch (InterruptedException e) {
dumpStreams = true;
throw new RuntimeException("Interrupted ...", e);
} catch (Throwable t) {
dumpStreams = true;
err = t;
} finally {
if (!logStreams && dumpStreams) try {
System.out.println("STDOUT: ");
copyStream(new ByteArrayInputStream(stdout.toByteArray()), System.out);
System.out.println("STDERR: ");
copyStream(new ByteArrayInputStream(stderr.toByteArray()), System.out);
} catch (Exception ignored) {
}
}
}
public int exitCode() {
return exitCode;
}
public Throwable error() {
return err;
}
public InputStream stdout() {
return new ByteArrayInputStream(stdout.toByteArray());
}
public List<String> stdoutLines() {
return parseStreamAsLines(new ByteArrayInputStream(stdout.toByteArray()));
}
public String stdoutString() {
return new String(stdout.toByteArray());
}
public InputStream stderr() {
return new ByteArrayInputStream(stderr.toByteArray());
}
public List<String> stderrLines() {
return parseStreamAsLines(new ByteArrayInputStream(stderr.toByteArray()));
}
public String stderrString() {
return new String(stderr.toByteArray());
}
static List<String> parseStreamAsLines(InputStream stream) {
List<String> lines = new ArrayList<>();
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
String line;
while ((line = reader.readLine()) != null) {
lines.add(line);
}
return lines;
} catch (IOException e) {
throw new RuntimeException("Unexpected I/O error", e);
}
}
public void waitForStdout(String content) {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < waitTimeout) {
if (stdoutString().indexOf(content) != -1) {
return;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted ...", e);
}
}
throw new RuntimeException("Timed while waiting for content to appear in stdout");
}
public void sendToStdin(String s) {
if (stdin instanceof InteractiveInputStream) {
((InteractiveInputStream) stdin).pushBytes(s.getBytes());
} else {
throw new RuntimeException("Can't push to stdin - not interactive");
}
}
static void copyStream(InputStream is, OutputStream os) throws IOException {
byte [] buf = new byte[8192];
try (InputStream iss = is) {
int c;
while ((c = iss.read(buf)) != -1) {
os.write(buf, 0, c);
os.flush();
}
}
}
}

View file

@ -0,0 +1,44 @@
package org.keycloak.testsuite.cli.exec;
import java.io.InputStream;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public abstract class AbstractExecBuilder<T> {
protected String workDir;
protected String argsLine;
protected InputStream stdin;
protected String env;
protected boolean dumpStreams;
public AbstractExecBuilder<T> workDir(String path) {
this.workDir = path;
return this;
}
public AbstractExecBuilder<T> argsLine(String cmd) {
this.argsLine = cmd;
return this;
}
public AbstractExecBuilder<T> stdin(InputStream is) {
this.stdin = is;
return this;
}
public AbstractExecBuilder<T> env(String env) {
this.env = env;
return this;
}
public AbstractExecBuilder<T> fullStreamDump() {
this.dumpStreams = true;
return this;
}
public abstract T execute();
public abstract T executeAsync();
}

View file

@ -1,4 +1,4 @@
package org.keycloak.testsuite.cli;
package org.keycloak.testsuite.cli.exec;
/**
* @author <a href="mailto:marko.strukelj@gmail.com">Marko Strukelj</a>

View file

@ -0,0 +1,120 @@
package org.keycloak.testsuite.cli.exec;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.util.LinkedList;
class InteractiveInputStream extends InputStream {
private LinkedList<Byte> queue = new LinkedList<>();
private Thread consumer;
private boolean closed;
@Override
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
@Override
public synchronized int read(byte[] b, int off, int len) throws IOException {
Byte current = null;
int rc = 0;
try {
consumer = Thread.currentThread();
do {
current = queue.poll();
if (current == null) {
if (rc > 0) {
return rc;
} else {
do {
if (closed) {
return -1;
}
wait();
}
while ((current = queue.poll()) == null);
}
}
b[off + rc] = current;
rc++;
} while (rc < len);
} catch (InterruptedException e) {
throw new InterruptedIOException("Signalled to exit");
} finally {
consumer = null;
}
return rc;
}
@Override
public long skip(long n) throws IOException {
return super.skip(n);
}
@Override
public int available() throws IOException {
return super.available();
}
@Override
public synchronized void mark(int readlimit) {
super.mark(readlimit);
}
@Override
public synchronized void reset() throws IOException {
super.reset();
}
@Override
public boolean markSupported() {
return super.markSupported();
}
@Override
public synchronized int read() throws IOException {
// when input is available pass it on
Byte current;
try {
consumer = Thread.currentThread();
while ((current = queue.poll()) == null) {
// we don't check for closed before making sure
// that there is nothing more to read
if (closed) {
return -1;
}
wait();
}
} catch (InterruptedException e) {
throw new InterruptedIOException("Signalled to exit");
} finally {
consumer = null;
}
return current;
}
@Override
public synchronized void close() {
closed = true;
if (consumer != null) {
consumer.interrupt();
}
}
public synchronized void pushBytes(byte [] buff) {
for (byte b : buff) {
queue.add(b);
}
notify();
}
}

View file

@ -0,0 +1,53 @@
package org.keycloak.testsuite.cli.exec;
import java.io.ByteArrayOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
class LoggingOutputStream extends FilterOutputStream {
private ByteArrayOutputStream buffer = new ByteArrayOutputStream();
private String name;
public LoggingOutputStream(String name, OutputStream os) {
super(os);
this.name = name;
}
@Override
public void write(int b) throws IOException {
super.write(b);
if (b == 10) {
log();
} else {
buffer.write(b);
}
}
@Override
public void write(byte[] buf) throws IOException {
write(buf, 0, buf.length);
}
@Override
public void write(byte[] buf, int offs, int len) throws IOException {
for (int i = 0; i < len; i++) {
write(buf[offs+i]);
}
}
@Override
public void close() throws IOException {
super.close();
if (buffer.size() > 0) {
log();
}
}
private void log() {
String log = new String(buffer.toByteArray());
buffer.reset();
System.out.println("[" + name + "] " + log);
}
}

View file

@ -0,0 +1,12 @@
package org.keycloak.testsuite.cli.exec;
import java.io.IOException;
import java.io.InputStream;
class NullInputStream extends InputStream {
@Override
public int read() throws IOException {
return -1;
}
}

View file

@ -0,0 +1,33 @@
package org.keycloak.testsuite.cli.exec;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import static org.keycloak.testsuite.cli.exec.AbstractExec.copyStream;
class StreamReaderThread extends Thread {
private InputStream is;
private OutputStream os;
StreamReaderThread(InputStream is, OutputStream os) {
this.is = is;
this.os = os;
}
public void run() {
try {
copyStream(is, os);
} catch (IOException e) {
throw new RuntimeException("Unexpected I/O error", e);
} finally {
try {
os.close();
} catch (IOException ignored) {
System.err.print("IGNORED: error while closing output stream: ");
ignored.printStackTrace();
}
}
}
}

View file

@ -0,0 +1,54 @@
package org.keycloak.testsuite.cli;
import org.junit.Assert;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.cli.exec.AbstractExec;
import java.util.List;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public abstract class AbstractCliTest extends AbstractKeycloakTest {
public void assertExitCodeAndStdOutSize(AbstractExec exe, int exitCode, int stdOutLineCount) {
assertExitCodeAndStreamSizes(exe, exitCode, stdOutLineCount, -1);
}
public void assertExitCodeAndStdErrSize(AbstractExec exe, int exitCode, int stdErrLineCount) {
assertExitCodeAndStreamSizes(exe, exitCode, -1, stdErrLineCount);
}
public void assertExitCodeAndStreamSizes(AbstractExec exe, int exitCode, int stdOutLineCount, int stdErrLineCount) {
Assert.assertEquals("exitCode == " + exitCode, exitCode, exe.exitCode());
if (stdOutLineCount != -1) {
try {
assertLineCount("stdout output", exe.stdoutLines(), stdOutLineCount);
} catch (Throwable e) {
throw new AssertionError("STDOUT: " + exe.stdoutString(), e);
}
}
if (stdErrLineCount != -1) {
try {
assertLineCount("stderr output", exe.stderrLines(), stdErrLineCount);
} catch (Throwable e) {
throw new AssertionError("STDERR: " + exe.stderrString(), e);
}
}
}
private void assertLineCount(String label, List<String> lines, int count) {
if (lines.size() == count) {
return;
}
// there is some kind of race condition in 'kcreg' that results in intermittent extra empty line
if (lines.size() == count + 1) {
if ("".equals(lines.get(lines.size()-1))) {
return;
}
}
Assert.assertTrue(label + " has " + lines.size() + " lines (expected: " + count + ")", lines.size() == count);
}
}

View file

@ -0,0 +1,387 @@
package org.keycloak.testsuite.cli.admin;
import org.junit.Assert;
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.client.admin.cli.config.ConfigData;
import org.keycloak.client.admin.cli.config.FileConfigHandler;
import org.keycloak.client.admin.cli.config.RealmConfigData;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.cli.AbstractCliTest;
import org.keycloak.testsuite.cli.KcAdmExec;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.util.JsonSerialization;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.cli.KcAdmExec.WORK_DIR;
import static org.keycloak.testsuite.cli.KcAdmExec.execute;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public abstract class AbstractAdmCliTest extends AbstractCliTest {
protected String serverUrl = isAuthServerSSL() ?
"https://localhost:" + getAuthServerHttpsPort() + "/auth" :
"http://localhost:" + getAuthServerHttpPort() + "/auth";
static boolean runIntermittentlyFailingTests() {
return "true".equals(System.getProperty("test.intermittent"));
}
static boolean isAuthServerSSL() {
return "true".equals(System.getProperty("auth.server.ssl.required"));
}
static File getDefaultConfigFilePath() {
return new File(System.getProperty("user.home") + "/.keycloak/kcadm.config");
}
static int getAuthServerHttpsPort() {
try {
return Integer.valueOf(System.getProperty("auth.server.https.port"));
} catch (Exception e) {
throw new RuntimeException("System property 'auth.server.https.port' not set or invalid: '"
+ System.getProperty("auth.server.https.port") + "'");
}
}
static int getAuthServerHttpPort() {
try {
return Integer.valueOf(System.getProperty("auth.server.http.port"));
} catch (Exception e) {
throw new RuntimeException("System property 'auth.server.http.port' not set or invalid: '"
+ System.getProperty("auth.server.http.port") + "'");
}
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realmRepresentation = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
testRealms.add(realmRepresentation);
// create admin user account with permissions to manage clients
UserRepresentation admin = UserBuilder.create()
.username("user1")
.password("userpass")
.enabled(true)
.build();
HashMap<String, List<String>> clientRoles = new HashMap<>();
clientRoles.put("realm-management", Arrays.asList("realm-admin"));
admin.setClientRoles(clientRoles);
realmRepresentation.getUsers().add(admin);
// create client with service account to use Signed JWT credentials with
ClientRepresentation regClient = ClientBuilder.create()
.clientId("admin-cli-jwt")
.attribute(JWTClientAuthenticator.CERTIFICATE_ATTR, "MIICnTCCAYUCBgFXUhpRTTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdyZWctY2xpMB4XDTE2MDkyMjEzMzIxOFoXDTI2MDkyMjEzMzM1OFowEjEQMA4GA1UEAwwHcmVnLWNsaTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMHZn/0Bk1M9oKcTHxzn2cGvBWwO1m6OVLQ8LSVwNIf4ixfGkVIkhI5iEGYND+uD8ame54ZPClTVxMra3JldClLIG+L+ymnbT2vKIhEsVvCROs9PnYxbFALt1dXneLIio2uzF+d7/zQWlmeaWfNunSJT1aHNJDkGgDeUuQa25b0IMqsFjsN8Dg4ATkA97r3wKn4Tp3SE7sTM/B2pmra4atNxGeShVrgihqUiQ/PwDiDGwry64AsexkZnQsCR3bJWBAVUiHef3JWzTfWWN5bfCBG6Mnq1xw7YN+YpV1nR3CGmcKJuLe6aTe7Ps8hYejYiQA7Mp7ZQsoImsVFV5HDOlb0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAZl8XvLfKXTPYvq/QyHOg7EDlAdlV3HkmHP9SBAV4BccmHmorMkm5I6I21UA5mfju+0nhbEd0bm0kvJFxIfNU6lJyyVvQx3Gns37KYUOzIV/ocWZuOTBLp5tfIBYbBwfE/s1J4PhpA/3WhBY9JKiLvdJfxECGIgaLs2M0UsylW/7o04+18Od8j/m7crQc7fpe5gJB5m/+hxUDowIjG5CumffX9OHYGDvHBpaUl7QNSGgjP8Bn9ogmIMUBJ7XSYUcohKuk2Cnj6p+GlLuqHbOISUXLVjf0DxhCu6diVxvacKbgAZmyCIO1tGL/UVRxg9GOYdCiC9vHfPuZ8US+ZB0P9g==")
.authenticatorType(JWTClientAuthenticator.PROVIDER_ID)
.serviceAccount()
.build();
realmRepresentation.getClients().add(regClient);
// create service account for client reg-cli with permissions to manage clients
addServiceAccount(realmRepresentation, "admin-cli-jwt");
// create client to use with user account - enable direct grants
regClient = ClientBuilder.create()
.clientId("admin-cli-jwt-direct")
.attribute(JWTClientAuthenticator.CERTIFICATE_ATTR, "MIICnTCCAYUCBgFXUhpRTTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdyZWctY2xpMB4XDTE2MDkyMjEzMzIxOFoXDTI2MDkyMjEzMzM1OFowEjEQMA4GA1UEAwwHcmVnLWNsaTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMHZn/0Bk1M9oKcTHxzn2cGvBWwO1m6OVLQ8LSVwNIf4ixfGkVIkhI5iEGYND+uD8ame54ZPClTVxMra3JldClLIG+L+ymnbT2vKIhEsVvCROs9PnYxbFALt1dXneLIio2uzF+d7/zQWlmeaWfNunSJT1aHNJDkGgDeUuQa25b0IMqsFjsN8Dg4ATkA97r3wKn4Tp3SE7sTM/B2pmra4atNxGeShVrgihqUiQ/PwDiDGwry64AsexkZnQsCR3bJWBAVUiHef3JWzTfWWN5bfCBG6Mnq1xw7YN+YpV1nR3CGmcKJuLe6aTe7Ps8hYejYiQA7Mp7ZQsoImsVFV5HDOlb0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAZl8XvLfKXTPYvq/QyHOg7EDlAdlV3HkmHP9SBAV4BccmHmorMkm5I6I21UA5mfju+0nhbEd0bm0kvJFxIfNU6lJyyVvQx3Gns37KYUOzIV/ocWZuOTBLp5tfIBYbBwfE/s1J4PhpA/3WhBY9JKiLvdJfxECGIgaLs2M0UsylW/7o04+18Od8j/m7crQc7fpe5gJB5m/+hxUDowIjG5CumffX9OHYGDvHBpaUl7QNSGgjP8Bn9ogmIMUBJ7XSYUcohKuk2Cnj6p+GlLuqHbOISUXLVjf0DxhCu6diVxvacKbgAZmyCIO1tGL/UVRxg9GOYdCiC9vHfPuZ8US+ZB0P9g==")
.authenticatorType(JWTClientAuthenticator.PROVIDER_ID)
.directAccessGrants()
.build();
realmRepresentation.getClients().add(regClient);
// create client with service account to use client secret with
regClient = ClientBuilder.create()
.clientId("admin-cli-secret")
.secret("password")
.authenticatorType(ClientIdAndSecretAuthenticator.PROVIDER_ID)
.serviceAccount()
.build();
realmRepresentation.getClients().add(regClient);
// create service account for client reg-cli with permissions to manage clients
addServiceAccount(realmRepresentation, "admin-cli-secret");
// create client to use with user account - enable direct grants
regClient = ClientBuilder.create()
.clientId("admin-cli-secret-direct")
.secret("password")
.authenticatorType(ClientIdAndSecretAuthenticator.PROVIDER_ID)
.directAccessGrants()
.build();
realmRepresentation.getClients().add(regClient);
}
FileConfigHandler initCustomConfigFile() {
String filename = UUID.randomUUID().toString() + ".config";
File cfgFile = new File(WORK_DIR + "/" + filename);
FileConfigHandler handler = new FileConfigHandler();
handler.setConfigFile(cfgFile.getAbsolutePath());
return handler;
}
void assertFieldsEqualWithExclusions(ConfigData config1, ConfigData config2, String ... excluded) {
HashSet<String> exclusions = new HashSet<>(Arrays.asList(excluded));
if (!exclusions.contains("serverUrl")) {
Assert.assertEquals("serverUrl", config1.getServerUrl(), config2.getServerUrl());
}
if (!exclusions.contains("realm")) {
Assert.assertEquals("realm", config1.getRealm(), config2.getRealm());
}
if (!exclusions.contains("truststore")) {
Assert.assertEquals("truststore", config1.getTruststore(), config2.getTruststore());
}
if (!exclusions.contains("endpoints")) {
Map<String, Map<String, RealmConfigData>> endp1 = config1.getEndpoints();
Map<String, Map<String, RealmConfigData>> endp2 = config2.getEndpoints();
Iterator<Map.Entry<String, Map<String, RealmConfigData>>> it1 = endp1.entrySet().iterator();
Iterator<Map.Entry<String, Map<String, RealmConfigData>>> it2 = endp2.entrySet().iterator();
while (it1.hasNext()) {
Map.Entry<String, Map<String, RealmConfigData>> ent1 = it1.next();
Map.Entry<String, Map<String, RealmConfigData>> ent2 = it2.next();
String serverUrl = ent1.getKey();
String endpskey = "endpoints." + serverUrl;
if (!exclusions.contains(endpskey)) {
Assert.assertEquals(endpskey, ent1.getKey(), ent2.getKey());
Map<String, RealmConfigData> realms1 = ent1.getValue();
Map<String, RealmConfigData> realms2 = ent2.getValue();
Iterator<Map.Entry<String, RealmConfigData>> rit1 = realms1.entrySet().iterator();
Iterator<Map.Entry<String, RealmConfigData>> rit2 = realms2.entrySet().iterator();
while (rit1.hasNext()) {
Map.Entry<String, RealmConfigData> rent1 = rit1.next();
Map.Entry<String, RealmConfigData> rent2 = rit2.next();
String realm = rent1.getKey();
String rkey = endpskey + "." + realm;
if (!exclusions.contains(endpskey)) {
Assert.assertEquals(rkey, rent1.getKey(), rent2.getKey());
RealmConfigData rdata1 = rent1.getValue();
RealmConfigData rdata2 = rent2.getValue();
assertFieldsEqualWithExclusions(serverUrl, realm, rdata1, rdata2, excluded);
}
}
}
}
}
}
void assertFieldsEqualWithExclusions(String server, String realm, RealmConfigData data1, RealmConfigData data2, String ... excluded) {
HashSet<String> exclusions = new HashSet<>(Arrays.asList(excluded));
String pfix = "";
if (server != null || realm != null) {
pfix = "endpoints." + server + "." + realm + ".";
}
String ekey = pfix + "serverUrl";
if (!exclusions.contains(ekey)) {
Assert.assertEquals(ekey, data1.serverUrl(), data2.serverUrl());
}
ekey = pfix + "realm";
if (!exclusions.contains(ekey)) {
Assert.assertEquals(ekey, data1.realm(), data2.realm());
}
ekey = pfix + "clientId";
if (!exclusions.contains(ekey)) {
Assert.assertEquals(ekey, data1.getClientId(), data2.getClientId());
}
ekey = pfix + "token";
if (!exclusions.contains(ekey)) {
Assert.assertEquals(ekey, data1.getToken(), data2.getToken());
}
ekey = pfix + "refreshToken";
if (!exclusions.contains(ekey)) {
Assert.assertEquals(ekey, data1.getRefreshToken(), data2.getRefreshToken());
}
ekey = pfix + "expiresAt";
if (!exclusions.contains(ekey)) {
Assert.assertEquals(ekey, data1.getExpiresAt(), data2.getExpiresAt());
}
ekey = pfix + "refreshExpiresAt";
if (!exclusions.contains(ekey)) {
Assert.assertEquals(ekey, data1.getRefreshExpiresAt(), data2.getRefreshExpiresAt());
}
ekey = pfix + "secret";
if (!exclusions.contains(ekey)) {
Assert.assertEquals(ekey, data1.getSecret(), data2.getSecret());
}
ekey = pfix + "signingToken";
if (!exclusions.contains(ekey)) {
Assert.assertEquals(ekey, data1.getSigningToken(), data2.getSigningToken());
}
ekey = pfix + "sigExpiresAt";
if (!exclusions.contains(ekey)) {
Assert.assertEquals(ekey, data1.getSigExpiresAt(), data2.getSigExpiresAt());
}
}
void testCRUDWithOnTheFlyAuth(String serverUrl, String credentials, String extraOptions, String loginMessage) throws IOException {
File configFile = getDefaultConfigFilePath();
long lastModified = configFile.exists() ? configFile.lastModified() : 0;
// This test assumes it is the only user of any instance of on the system
KcAdmExec exe = execute("create clients --no-config --server " + serverUrl +
" --realm test " + credentials + " " + extraOptions + " -s clientId=test-client -o");
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
Assert.assertEquals("login message", loginMessage, exe.stderrLines().get(0));
ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
Assert.assertEquals("clientId", "test-client", client.getClientId());
long lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
Assert.assertEquals("config file not modified", lastModified, lastModified2);
exe = execute("get clients/" + client.getId() + " --no-config --server " + serverUrl + " --realm test " + credentials + " " + extraOptions);
assertExitCodeAndStdErrSize(exe, 0, 1);
ClientRepresentation client2 = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
Assert.assertEquals("clientId", "test-client", client2.getClientId());
lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
Assert.assertEquals("config file not modified", lastModified, lastModified2);
exe = execute("update clients/" + client.getId() + " --no-config --server " + serverUrl + " --realm test " +
credentials + " " + extraOptions + " -s enabled=false -o");
assertExitCodeAndStdErrSize(exe, 0, 1);
ClientRepresentation client4 = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
Assert.assertEquals("clientId", "test-client", client4.getClientId());
Assert.assertFalse("enabled", client4.isEnabled());
lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
Assert.assertEquals("config file not modified", lastModified, lastModified2);
exe = execute("delete clients/" + client.getId() + " --no-config --server " + serverUrl + " --realm test " + credentials + " " + extraOptions);
assertExitCodeAndStreamSizes(exe, 0, 0, 1);
lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
Assert.assertEquals("config file not modified", lastModified, lastModified2);
// subsequent delete should fail
exe = execute("delete clients/" + client.getId() + " --no-config --server " + serverUrl + " --realm test " + credentials + " " + extraOptions);
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
String resourceUri = serverUrl + "/admin/realms/test/clients/" + client.getId();
Assert.assertEquals("error message", "Resource not found for url: " + resourceUri, exe.stderrLines().get(1));
lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
Assert.assertEquals("config file not modified", lastModified, lastModified2);
}
File initTempFile(String extension) throws IOException {
return initTempFile(extension, null);
}
File initTempFile(String extension, String content) throws IOException {
String filename = UUID.randomUUID().toString() + extension;
File file = new File(KcAdmExec.WORK_DIR + "/" + filename);
if (content != null) {
OutputStream os = new FileOutputStream(file);
os.write(content.getBytes(Charset.forName("iso_8859_1")));
os.close();
}
return file;
}
void addServiceAccount(RealmRepresentation realm, String clientId) {
UserRepresentation account = UserBuilder.create()
.username("service-account-" + clientId)
.enabled(true)
.serviceAccountId(clientId)
.build();
HashMap<String, List<String>> clientRoles = new HashMap<>();
clientRoles.put("realm-management", Arrays.asList("realm-admin"));
account.setClientRoles(clientRoles);
realm.getUsers().add(account);
}
void loginAsUser(File configFile, String server, String realm, String user, String password) {
KcAdmExec exe = KcAdmExec.execute("config credentials --server " + server + " --realm " + realm +
" --user " + user + " --password " + password + " --config " + configFile.getAbsolutePath());
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
List<String> lines = exe.stdoutLines();
Assert.assertTrue("stdout output empty", lines.size() == 0);
lines = exe.stderrLines();
Assert.assertTrue("stderr output one line", lines.size() == 1);
Assert.assertEquals("stderr first line", "Logging into " + server + " as user " + user + " of realm " + realm, lines.get(0));
}
}

View file

@ -0,0 +1,131 @@
package org.keycloak.testsuite.cli.admin;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.client.admin.cli.config.FileConfigHandler;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.testsuite.cli.KcAdmExec;
import org.keycloak.testsuite.util.TempFileResource;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
import java.util.Arrays;
import static org.keycloak.testsuite.cli.KcAdmExec.execute;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class KcAdmCreateTest extends AbstractAdmCliTest {
@Test
public void testCreateWithRealmOverride() throws IOException {
FileConfigHandler handler = initCustomConfigFile();
try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
// authenticate as a regular user against one realm
KcAdmExec exe = execute("config credentials -x --config '" + configFile.getName() +
"' --server " + serverUrl + " --realm master --user admin --password admin");
assertExitCodeAndStreamSizes(exe, 0, 0, 1);
exe = execute("create clients --config '" + configFile.getName() + "' --server " + serverUrl + " -r test -s clientId=my_first_client");
assertExitCodeAndStreamSizes(exe, 0, 0, 1);
}
}
@Test
public void testCreateThoroughly() throws IOException {
FileConfigHandler handler = initCustomConfigFile();
try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
final String realm = "test";
// authenticate as a regular user against one realm
KcAdmExec exe = KcAdmExec.execute("config credentials -x --config '" + configFile.getName() +
"' --server " + serverUrl + " --realm master --user admin --password admin");
assertExitCodeAndStreamSizes(exe, 0, 0, 1);
// create configuration from file using stdin redirect ... output an object
String content = "{\n" +
" \"clientId\": \"my_client\",\n" +
" \"enabled\": true,\n" +
" \"redirectUris\": [\"http://localhost:8980/myapp/*\"],\n" +
" \"serviceAccountsEnabled\": true,\n" +
" \"name\": \"My Client App\",\n" +
" \"implicitFlowEnabled\": false,\n" +
" \"publicClient\": true,\n" +
" \"webOrigins\": [\"http://localhost:8980/myapp\"],\n" +
" \"consentRequired\": false,\n" +
" \"baseUrl\": \"http://localhost:8980/myapp\",\n" +
" \"bearerOnly\": true,\n" +
" \"standardFlowEnabled\": true\n" +
"}";
try (TempFileResource tmpFile = new TempFileResource(initTempFile(".json", content))) {
exe = execute("create clients --config '" + configFile.getName() + "' -o -f - < '" + tmpFile.getName() + "'");
assertExitCodeAndStdErrSize(exe, 0, 0);
ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
Assert.assertNotNull("id", client.getId());
Assert.assertEquals("clientId", "my_client", client.getClientId());
Assert.assertEquals("enabled", true, client.isEnabled());
Assert.assertEquals("redirectUris", Arrays.asList("http://localhost:8980/myapp/*"), client.getRedirectUris());
Assert.assertEquals("serviceAccountsEnabled", true, client.isServiceAccountsEnabled());
Assert.assertEquals("name", "My Client App", client.getName());
Assert.assertEquals("implicitFlowEnabled", false, client.isImplicitFlowEnabled());
Assert.assertEquals("publicClient", true, client.isPublicClient());
// note there is no server-side check if protocol is supported
Assert.assertEquals("webOrigins", Arrays.asList("http://localhost:8980/myapp"), client.getWebOrigins());
Assert.assertEquals("consentRequired", false, client.isConsentRequired());
Assert.assertEquals("baseUrl", "http://localhost:8980/myapp", client.getBaseUrl());
Assert.assertEquals("bearerOnly", true, client.isStandardFlowEnabled());
Assert.assertFalse("mappers not empty", client.getProtocolMappers().isEmpty());
// create configuration from file as a template and override clientId and other attributes ... output an object
exe = execute("create clients --config '" + configFile.getName() + "' -o -f '" + tmpFile.getName() +
"' -s clientId=my_client2 -s enabled=false -s 'redirectUris=[\"http://localhost:8980/myapp2/*\"]'" +
" -s 'name=My Client App II' -s 'webOrigins=[\"http://localhost:8980/myapp2\"]'" +
" -s baseUrl=http://localhost:8980/myapp2 -s rootUrl=http://localhost:8980/myapp2");
assertExitCodeAndStdErrSize(exe, 0, 0);
ClientRepresentation client2 = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
Assert.assertNotNull("id", client2.getId());
Assert.assertEquals("clientId", "my_client2", client2.getClientId());
Assert.assertEquals("enabled", false, client2.isEnabled());
Assert.assertEquals("redirectUris", Arrays.asList("http://localhost:8980/myapp2/*"), client2.getRedirectUris());
Assert.assertEquals("serviceAccountsEnabled", true, client2.isServiceAccountsEnabled());
Assert.assertEquals("name", "My Client App II", client2.getName());
Assert.assertEquals("implicitFlowEnabled", false, client2.isImplicitFlowEnabled());
Assert.assertEquals("publicClient", true, client2.isPublicClient());
Assert.assertEquals("webOrigins", Arrays.asList("http://localhost:8980/myapp2"), client2.getWebOrigins());
Assert.assertEquals("consentRequired", false, client2.isConsentRequired());
Assert.assertEquals("baseUrl", "http://localhost:8980/myapp2", client2.getBaseUrl());
Assert.assertEquals("rootUrl", "http://localhost:8980/myapp2", client2.getRootUrl());
Assert.assertEquals("bearerOnly", true, client2.isStandardFlowEnabled());
Assert.assertFalse("mappers not empty", client2.getProtocolMappers().isEmpty());
}
// simple create, output an id
exe = execute("create clients --config '" + configFile.getName() + "' -i -s clientId=my_client3");
assertExitCodeAndStreamSizes(exe, 0, 1, 0);
// simple create, default output
exe = execute("create clients --config '" + configFile.getName() + "' -s clientId=my_client4");
assertExitCodeAndStreamSizes(exe, 0, 0, 1);
Assert.assertTrue("only id returned", exe.stderrLines().get(0).startsWith("Created new client with id '"));
}
}
}

View file

@ -0,0 +1,561 @@
package org.keycloak.testsuite.cli.admin;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.client.admin.cli.config.ConfigData;
import org.keycloak.client.admin.cli.config.FileConfigHandler;
import org.keycloak.client.admin.cli.config.RealmConfigData;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.testsuite.cli.KcAdmExec;
import org.keycloak.testsuite.util.TempFileResource;
import org.keycloak.util.JsonSerialization;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
import static org.keycloak.testsuite.cli.KcAdmExec.execute;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class KcAdmTest extends AbstractAdmCliTest {
@Test
public void testBadCommand() {
/*
* Test most basic execution with non-existent command
*/
KcAdmExec exe = execute("nonexistent");
assertExitCodeAndStreamSizes(exe, 1, 0, 1);
Assert.assertEquals("stderr first line", "Unknown command: nonexistent", exe.stderrLines().get(0));
}
@Test
public void testNoArgs() {
/*
* Test (sub)commands without any arguments
*/
KcAdmExec exe = KcAdmExec.execute("");
assertExitCodeAndStdErrSize(exe, 1, 0);
List<String> lines = exe.stdoutLines();
Assert.assertTrue("stdout output not empty", lines.size() > 0);
Assert.assertEquals("stdout first line", "Keycloak Admin CLI", lines.get(0));
Assert.assertEquals("stdout one but last line", "Use '" + KcAdmExec.CMD + " help <command>' for more information about a given command.", lines.get(lines.size() - 2));
Assert.assertEquals("stdout last line", "", lines.get(lines.size() - 1));
/*
* Test commands without arguments
*/
exe = KcAdmExec.execute("config");
assertExitCodeAndStreamSizes(exe, 1, 0, 1);
Assert.assertEquals("error message",
"Sub-command required by '" + CMD + " config' - one of: 'credentials', 'truststore'",
exe.stderrLines().get(0));
exe = KcAdmExec.execute("config credentials");
assertExitCodeAndStdErrSize(exe, 1, 0);
Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
Assert.assertEquals("help message", "Usage: " + CMD + " config credentials --server SERVER_URL --realm REALM --user USER [--password PASSWORD] [ARGUMENTS]", exe.stdoutLines().get(0));
exe = KcAdmExec.execute("config truststore");
assertExitCodeAndStdErrSize(exe, 1, 0);
Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
Assert.assertEquals("help message", "Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWORD] [ARGUMENTS]", exe.stdoutLines().get(0));
exe = KcAdmExec.execute("create");
assertExitCodeAndStdErrSize(exe, 1, 0);
Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
Assert.assertEquals("help message", "Usage: " + CMD + " create ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
//Assert.assertEquals("error message", "No file nor attribute values specified", exe.stderrLines().get(0));
exe = KcAdmExec.execute("get");
assertExitCodeAndStdErrSize(exe, 1, 0);
Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
Assert.assertEquals("help message", "Usage: " + CMD + " get ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
//Assert.assertEquals("error message", "CLIENT not specified", exe.stderrLines().get(0));
exe = KcAdmExec.execute("update");
assertExitCodeAndStdErrSize(exe, 1, 0);
Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
Assert.assertEquals("help message", "Usage: " + CMD + " update ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
//Assert.assertEquals("error message", "No file nor attribute values specified", exe.stderrLines().get(0));
exe = KcAdmExec.execute("delete");
assertExitCodeAndStdErrSize(exe, 1, 0);
Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
Assert.assertEquals("help message", "Usage: " + CMD + " delete ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
//Assert.assertEquals("error message", "CLIENT not specified", exe.stderrLines().get(0));
//exe = KcAdmExec.execute("get-roles");
//assertExitCodeAndStdErrSize(exe, 0, 0);
//try {
// JsonNode node = JsonSerialization.readValue(exe.stdout(), JsonNode.class);
// Assert.assertTrue("is JSON array", node.isArray());
//} catch (IOException e) {
// throw new AssertionError("Response should be a JSON array", e);
//}
//Assert.assertTrue("JSON message returned", exe.stdoutLines().size() > 10);
//Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
//Assert.assertEquals("help message", "Usage: " + CMD + " get-roles [--cclientid CLIENT_ID | --cid ID] [ARGUMENTS]", exe.stdoutLines().get(0));
exe = KcAdmExec.execute("add-roles");
assertExitCodeAndStdErrSize(exe, 1, 0);
Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
Assert.assertEquals("help message", "Usage: " + CMD + " add-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]", exe.stdoutLines().get(0));
//Assert.assertEquals("error message", "CLIENT not specified", exe.stderrLines().get(0));
exe = KcAdmExec.execute("remove-roles");
assertExitCodeAndStdErrSize(exe, 1, 0);
Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
Assert.assertEquals("help message", "Usage: " + CMD + " remove-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]", exe.stdoutLines().get(0));
exe = KcAdmExec.execute("set-password");
assertExitCodeAndStdErrSize(exe, 1, 0);
Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
Assert.assertEquals("help message", "Usage: " + CMD + " set-password (--username USERNAME | --userid ID) [--password PASSWORD] [ARGUMENTS]", exe.stdoutLines().get(0));
//Assert.assertEquals("error message", "CLIENT not specified", exe.stderrLines().get(0));
exe = KcAdmExec.execute("help");
assertExitCodeAndStdErrSize(exe, 0, 0);
lines = exe.stdoutLines();
Assert.assertTrue("stdout output not empty", lines.size() > 0);
Assert.assertEquals("stdout first line", "Keycloak Admin CLI", lines.get(0));
Assert.assertEquals("stdout one but last line", "Use '" + KcAdmExec.CMD + " help <command>' for more information about a given command.", lines.get(lines.size() - 2));
Assert.assertEquals("stdout last line", "", lines.get(lines.size() - 1));
}
@Test
public void testHelpGlobalOption() {
/*
* Test --help for all commands
*/
KcAdmExec exe = KcAdmExec.execute("--help");
assertExitCodeAndStdErrSize(exe, 0, 0);
Assert.assertEquals("stdout first line", "Keycloak Admin CLI", exe.stdoutLines().get(0));
exe = KcAdmExec.execute("create --help");
assertExitCodeAndStdErrSize(exe, 0, 0);
Assert.assertEquals("stdout first line", "Usage: " + CMD + " create ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
exe = KcAdmExec.execute("get --help");
assertExitCodeAndStdErrSize(exe, 0, 0);
Assert.assertEquals("stdout first line", "Usage: " + CMD + " get ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
exe = KcAdmExec.execute("update --help");
assertExitCodeAndStdErrSize(exe, 0, 0);
Assert.assertEquals("stdout first line", "Usage: " + CMD + " update ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
exe = KcAdmExec.execute("delete --help");
assertExitCodeAndStdErrSize(exe, 0, 0);
Assert.assertEquals("stdout first line", "Usage: " + CMD + " delete ENDPOINT_URI [ARGUMENTS]", exe.stdoutLines().get(0));
exe = KcAdmExec.execute("get-roles --help");
assertExitCodeAndStdErrSize(exe, 0, 0);
Assert.assertEquals("stdout first line", "Usage: " + CMD + " get-roles [--cclientid CLIENT_ID | --cid ID] [ARGUMENTS]", exe.stdoutLines().get(0));
exe = KcAdmExec.execute("add-roles --help");
assertExitCodeAndStdErrSize(exe, 0, 0);
Assert.assertEquals("stdout first line", "Usage: " + CMD + " add-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]", exe.stdoutLines().get(0));
exe = KcAdmExec.execute("remove-roles --help");
assertExitCodeAndStdErrSize(exe, 0, 0);
Assert.assertEquals("stdout first line", "Usage: " + CMD + " remove-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]", exe.stdoutLines().get(0));
exe = KcAdmExec.execute("set-password --help");
assertExitCodeAndStdErrSize(exe, 0, 0);
Assert.assertEquals("stdout first line", "Usage: " + CMD + " set-password (--username USERNAME | --userid ID) [--password PASSWORD] [ARGUMENTS]", exe.stdoutLines().get(0));
exe = KcAdmExec.execute("config --help");
assertExitCodeAndStdErrSize(exe, 0, 0);
Assert.assertEquals("stdout first line", "Usage: " + CMD + " config SUB_COMMAND [ARGUMENTS]", exe.stdoutLines().get(0));
exe = KcAdmExec.execute("config credentials --help");
assertExitCodeAndStdErrSize(exe, 0, 0);
Assert.assertEquals("stdout first line",
"Usage: " + CMD + " config credentials --server SERVER_URL --realm REALM --user USER [--password PASSWORD] [ARGUMENTS]",
exe.stdoutLines().get(0));
exe = KcAdmExec.execute("config truststore --help");
assertExitCodeAndStdErrSize(exe, 0, 0);
Assert.assertEquals("stdout first line",
"Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWORD] [ARGUMENTS]",
exe.stdoutLines().get(0));
}
@Test
public void testBadOptionInPlaceOfCommand() {
/*
* Test most basic execution with non-existent option
*/
KcAdmExec exe = KcAdmExec.execute("--nonexistent");
assertExitCodeAndStreamSizes(exe, 1, 0, 1);
Assert.assertEquals("stderr first line", "Unknown command: --nonexistent", exe.stderrLines().get(0));
}
@Test
public void testBadOption() {
/*
* Test sub-command execution with non-existent option
*/
KcAdmExec exe = KcAdmExec.execute("get users --nonexistent");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
Assert.assertEquals("stderr first line", "Invalid option: --nonexistent", exe.stderrLines().get(0));
Assert.assertEquals("try help", "Try '" + CMD + " help get' for more information", exe.stderrLines().get(1));
// set-password doesn't use @Arguments injection thus unsupported options are handled by Aesh
exe = KcAdmExec.execute("set-password --nonexistent");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
Assert.assertEquals("stderr first line", "Invalid option: --nonexistent", exe.stderrLines().get(0));
Assert.assertEquals("try help", "Try '" + CMD + " help set-password' for more information", exe.stderrLines().get(1));
}
@Test
public void testCredentialsServerAndRealmWithDefaultConfig() {
/*
* Test without --server specified
*/
KcAdmExec exe = KcAdmExec.execute("config credentials --server " + serverUrl + " --realm master");
assertExitCodeAndStreamSizes(exe, 0, 0, 0);
}
@Test
public void testCredentialsNoServerWithDefaultConfig() {
/*
* Test without --server specified
*/
KcAdmExec exe = KcAdmExec.execute("config credentials --realm master --user admin --password admin");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
Assert.assertEquals("stderr first line", "Required option not specified: --server", exe.stderrLines().get(0));
Assert.assertEquals("try help", "Try '" + CMD + " help config credentials' for more information", exe.stderrLines().get(1));
}
@Test
public void testCredentialsNoRealmWithDefaultConfig() {
/*
* Test without --server specified
*/
KcAdmExec exe = KcAdmExec.execute("config credentials --server " + serverUrl + " --user admin --password admin");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
Assert.assertEquals("stderr first line", "Required option not specified: --realm", exe.stderrLines().get(0));
Assert.assertEquals("try help", "Try '" + CMD + " help config credentials' for more information", exe.stderrLines().get(1));
}
@Test
public void testUserLoginWithDefaultConfig() {
/*
* Test most basic user login, using the default admin-cli as a client
*/
KcAdmExec exe = KcAdmExec.execute("config credentials --server " + serverUrl + " --realm master --user admin --password admin");
assertExitCodeAndStreamSizes(exe, 0, 0, 1);
Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as user admin of realm master", exe.stderrLines().get(0));
}
@Test
public void testUserLoginWithDefaultConfigInteractive() throws IOException {
/*
* Test user login with interaction - provide user password after prompted for it
*/
if (!runIntermittentlyFailingTests()) {
System.out.println("TEST SKIPPED - This test currently suffers from intermittent failures. Use -Dtest.intermittent=true to run it.");
return;
}
KcAdmExec exe = KcAdmExec.newBuilder()
.argsLine("config credentials --server " + serverUrl + " --realm master --user admin")
.executeAsync();
exe.waitForStdout("Enter password: ");
exe.sendToStdin("admin" + EOL);
exe.waitCompletion();
assertExitCodeAndStreamSizes(exe, 0, 1, 1);
Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as user admin of realm master", exe.stderrLines().get(0));
/*
* Run the test one more time with stdin redirect
*/
File tmpFile = new File(KcAdmExec.WORK_DIR + "/" + UUID.randomUUID().toString() + ".tmp");
try {
FileOutputStream tmpos = new FileOutputStream(tmpFile);
tmpos.write("admin".getBytes());
tmpos.write(EOL.getBytes());
tmpos.close();
exe = KcAdmExec.execute("config credentials --server " + serverUrl + " --realm master --user admin < '" + tmpFile.getName() + "'");
assertExitCodeAndStreamSizes(exe, 0, 1, 1);
Assert.assertTrue("Enter password prompt", exe.stdoutLines().get(0).startsWith("Enter password: "));
Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as user admin of realm master", exe.stderrLines().get(0));
} finally {
tmpFile.delete();
}
}
@Test
public void testClientLoginWithDefaultConfigInteractive() throws IOException {
/*
* Test client login with interaction - login using service account, and provide a client secret after prompted for it
*/
if (!runIntermittentlyFailingTests()) {
System.out.println("TEST SKIPPED - This test currently suffers from intermittent failures. Use -Dtest.intermittent=true to run it.");
return;
}
// use -Dtest.intermittent=true to run this test
KcAdmExec exe = KcAdmExec.newBuilder()
.argsLine("config credentials --server " + serverUrl + " --realm test --client admin-cli-secret")
.executeAsync();
exe.waitForStdout("Enter client secret: ");
exe.sendToStdin("password" + EOL);
exe.waitCompletion();
assertExitCodeAndStreamSizes(exe, 0, 1, 1);
Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as service-account-admin-cli-secret of realm test", exe.stderrLines().get(0));
/*
* Run the test one more time with stdin redirect
*/
File tmpFile = new File(KcAdmExec.WORK_DIR + "/" + UUID.randomUUID().toString() + ".tmp");
try {
FileOutputStream tmpos = new FileOutputStream(tmpFile);
tmpos.write("password".getBytes());
tmpos.write(EOL.getBytes());
tmpos.close();
exe = KcAdmExec.newBuilder()
.argsLine("config credentials --server " + serverUrl + " --realm test --client admin-cli-secret < '" + tmpFile.getName() + "'")
.execute();
assertExitCodeAndStreamSizes(exe, 0, 1, 1);
Assert.assertTrue("Enter client secret prompt", exe.stdoutLines().get(0).startsWith("Enter client secret: "));
Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as service-account-admin-cli-secret of realm test", exe.stderrLines().get(0));
} finally {
tmpFile.delete();
}
}
@Test
public void testUserLoginWithCustomConfig() {
/*
* Test user login using a custom config file
*/
FileConfigHandler handler = initCustomConfigFile();
File configFile = new File(handler.getConfigFile());
try {
KcAdmExec exe = KcAdmExec.execute("config credentials --server " + serverUrl + " --realm master" +
" --user admin --password admin --config '" + configFile.getName() + "'");
assertExitCodeAndStreamSizes(exe, 0, 0, 1);
Assert.assertEquals("stderr first line", "Logging into " + serverUrl + " as user admin of realm master", exe.stderrLines().get(0));
// make sure the config file exists, and has the right content
ConfigData config = handler.loadConfig();
Assert.assertEquals("serverUrl", serverUrl, config.getServerUrl());
Assert.assertEquals("realm", "master", config.getRealm());
RealmConfigData realmcfg = config.sessionRealmConfigData();
Assert.assertNotNull("realm config data no null", realmcfg);
Assert.assertEquals("realm cfg serverUrl", serverUrl, realmcfg.serverUrl());
Assert.assertEquals("realm cfg realm", "master", realmcfg.realm());
Assert.assertEquals("client id", "admin-cli", realmcfg.getClientId());
Assert.assertNotNull("token not null", realmcfg.getToken());
Assert.assertNotNull("refresh token not null", realmcfg.getRefreshToken());
Assert.assertNotNull("token expires not null", realmcfg.getExpiresAt());
Assert.assertNotNull("token expires in future", realmcfg.getExpiresAt() > System.currentTimeMillis());
Assert.assertNotNull("refresh token expires not null", realmcfg.getRefreshExpiresAt());
Assert.assertNotNull("refresh token expires in future", realmcfg.getRefreshExpiresAt() > System.currentTimeMillis());
} finally {
configFile.delete();
}
}
@Test
public void testCustomConfigLoginCreateDelete() throws IOException {
/*
* Test user login, create, delete session using a custom config file
*/
// prepare for loading a config file
FileConfigHandler handler = initCustomConfigFile();
try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
KcAdmExec exe = KcAdmExec.execute("config credentials --server " + serverUrl +
" --realm master --user admin --password admin --config '" + configFile.getName() + "'");
assertExitCodeAndStreamSizes(exe, 0, 0, 1);
// remember the state of config file
ConfigData config1 = handler.loadConfig();
exe = KcAdmExec.execute("create --config '" + configFile.getName() + "' clients -s clientId=test-client -o");
assertExitCodeAndStdErrSize(exe, 0, 0);
// check changes to config file
ConfigData config2 = handler.loadConfig();
assertFieldsEqualWithExclusions(config1, config2);
ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
Assert.assertEquals("clientId", "test-client", client.getClientId());
exe = KcAdmExec.execute("delete clients/" + client.getId() + " --config '" + configFile.getName() + "'");
assertExitCodeAndStreamSizes(exe, 0, 0, 0);
// check changes to config file
ConfigData config3 = handler.loadConfig();
assertFieldsEqualWithExclusions(config2, config3);
}
}
@Test
public void testCRUDWithOnTheFlyUserAuth() throws IOException {
/*
* Test create, get, update, and delete using on-the-fly authentication - without using any config file.
* Login is performed by each operation again, and again using username, and password.
*/
testCRUDWithOnTheFlyAuth(serverUrl, "--user user1 --password userpass", "",
"Logging into " + serverUrl + " as user user1 of realm test");
}
@Test
public void testCRUDWithOnTheFlyUserAuthWithClientSecret() throws IOException {
/*
* Test create, get, update, and delete using on-the-fly authentication - without using any config file.
* Login is performed by each operation again, and again using username, password, and client secret.
*/
// try client without direct grants enabled
KcAdmExec exe = KcAdmExec.execute("get clients --no-config --server " + serverUrl + " --realm test" +
" --user user1 --password userpass --client admin-cli-secret --secret password");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
Assert.assertEquals("error message", "Client not allowed for direct access grants [invalid_grant]", exe.stderrLines().get(1));
// try wrong user password
exe = KcAdmExec.execute("get clients --no-config --server " + serverUrl + " --realm test" +
" --user user1 --password wrong --client admin-cli-secret-direct --secret password");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
Assert.assertEquals("error message", "Invalid user credentials [invalid_grant]", exe.stderrLines().get(1));
// try wrong client secret
exe = KcAdmExec.execute("get clients --no-config --server " + serverUrl + " --realm test" +
" --user user1 --password userpass --client admin-cli-secret-direct --secret wrong");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
Assert.assertEquals("error message", "Invalid client secret [unauthorized_client]", exe.stderrLines().get(1));
// try whole CRUD
testCRUDWithOnTheFlyAuth(serverUrl, "--user user1 --password userpass --client admin-cli-secret-direct --secret password", "",
"Logging into " + serverUrl + " as user user1 of realm test");
}
@Test
public void testCRUDWithOnTheFlyUserAuthWithSignedJwtClient() throws IOException {
/*
* Test create, get, update, and delete using on-the-fly authentication - without using any config file.
* Login is performed by each operation again, and again using username, password, and client JWT signature.
*/
File keystore = new File(System.getProperty("user.dir") + "/src/test/resources/cli/kcadm/admin-cli-keystore.jks");
Assert.assertTrue("admin-cli-keystore.jks exists", keystore.isFile());
// try client without direct grants enabled
KcAdmExec exe = KcAdmExec.execute("get clients --no-config --server " + serverUrl + " --realm test" +
" --user user1 --password userpass --client admin-cli-jwt --keystore '" + keystore.getAbsolutePath() + "'" +
" --storepass storepass --keypass keypass --alias admin-cli");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
Assert.assertEquals("error message", "Client not allowed for direct access grants [invalid_grant]", exe.stderrLines().get(1));
// try wrong user password
exe = KcAdmExec.execute("get clients --no-config --server " + serverUrl + " --realm test" +
" --user user1 --password wrong --client admin-cli-jwt-direct --keystore '" + keystore.getAbsolutePath() + "'" +
" --storepass storepass --keypass keypass --alias admin-cli");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
Assert.assertEquals("error message", "Invalid user credentials [invalid_grant]", exe.stderrLines().get(1));
// try wrong storepass
exe = KcAdmExec.execute("get clients --no-config --server " + serverUrl + " --realm test" +
" --user user1 --password userpass --client admin-cli-jwt-direct --keystore '" + keystore.getAbsolutePath() + "'" +
" --storepass wrong --keypass keypass --alias admin-cli");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
Assert.assertEquals("login message", "Logging into " + serverUrl + " as user user1 of realm test", exe.stderrLines().get(0));
Assert.assertEquals("error message", "Failed to load private key: Keystore was tampered with, or password was incorrect", exe.stderrLines().get(1));
// try whole CRUD
testCRUDWithOnTheFlyAuth(serverUrl,
"--user user1 --password userpass --client admin-cli-jwt-direct --keystore '" + keystore.getAbsolutePath() + "'" +
" --storepass storepass --keypass keypass --alias admin-cli", "",
"Logging into " + serverUrl + " as user user1 of realm test");
}
@Test
public void testCRUDWithOnTheFlyServiceAccountWithClientSecret() throws IOException {
/*
* Test create, get, update, and delete using on-the-fly authentication - without using any config file.
* Login is performed by each operation again, and again using only client secret - service account is used.
*/
testCRUDWithOnTheFlyAuth(serverUrl, "--client admin-cli-secret --secret password", "",
"Logging into " + serverUrl + " as service-account-admin-cli-secret of realm test");
}
@Test
public void testCRUDWithOnTheFlyServiceAccountWithSignedJwtClient() throws IOException {
/*
* Test create, get, update, and delete using on-the-fly authentication - without using any config file.
* Login is performed by each operation again, and again using only client JWT signature - service account is used.
*/
File keystore = new File(System.getProperty("user.dir") + "/src/test/resources/cli/kcadm/admin-cli-keystore.jks");
Assert.assertTrue("admin-cli-keystore.jks exists", keystore.isFile());
testCRUDWithOnTheFlyAuth(serverUrl,
"--client admin-cli-jwt --keystore '" + keystore.getAbsolutePath() + "' --storepass storepass --keypass keypass --alias admin-cli", "",
"Logging into " + serverUrl + " as service-account-admin-cli-jwt of realm test");
}
}

View file

@ -0,0 +1,115 @@
package org.keycloak.testsuite.cli.admin;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.client.admin.cli.config.ConfigData;
import org.keycloak.client.admin.cli.config.FileConfigHandler;
import org.keycloak.testsuite.cli.KcAdmExec;
import org.keycloak.testsuite.util.TempFileResource;
import java.io.File;
import java.io.IOException;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_PATH;
import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
import static org.keycloak.testsuite.cli.KcAdmExec.CMD;
import static org.keycloak.testsuite.cli.KcAdmExec.execute;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class KcAdmTruststoreTest extends AbstractAdmCliTest {
@Test
public void testTruststore() throws IOException {
// only run this test if ssl protected keycloak server is available
if (!isAuthServerSSL()) {
System.out.println("TEST SKIPPED - This test requires HTTPS. Run with '-Pauth-server-wildfly -Dauth.server.ssl.required=true'");
return;
}
File truststore = new File("src/test/resources/keystore/keycloak.truststore");
FileConfigHandler handler = initCustomConfigFile();
try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
if (runIntermittentlyFailingTests()) {
// configure truststore
KcAdmExec exe = execute("config truststore --config '" + configFile.getName() + "' '" + truststore.getAbsolutePath() + "'");
assertExitCodeAndStreamSizes(exe, 0, 0, 0);
// perform authentication against server - asks for password, then for truststore password
exe = KcAdmExec.newBuilder()
.argsLine("config credentials --server " + serverUrl + " --realm test --user user1" +
" --config '" + configFile.getName() + "'")
.executeAsync();
exe.waitForStdout("Enter password: ");
exe.sendToStdin("userpass" + EOL);
exe.waitForStdout("Enter truststore password: ");
exe.sendToStdin("secret" + EOL);
exe.waitCompletion();
assertExitCodeAndStreamSizes(exe, 0, 2, 1);
// configure truststore with password
exe = execute("config truststore --config '" + configFile.getName() + "' --trustpass secret '" + truststore.getAbsolutePath() + "'");
assertExitCodeAndStreamSizes(exe, 0, 0, 0);
// perform authentication against server - asks for password, then for truststore password
exe = KcAdmExec.newBuilder()
.argsLine("config credentials --server " + serverUrl + " --realm test --user user1" +
" --config '" + configFile.getName() + "'")
.executeAsync();
exe.waitForStdout("Enter password: ");
exe.sendToStdin("userpass" + EOL);
exe.waitCompletion();
assertExitCodeAndStreamSizes(exe, 0, 1, 1);
} else {
System.out.println("TEST SKIPPED PARTIALLY - This test currently suffers from intermittent failures. Use -Dtest.intermittent=true to run it in full.");
}
}
// configure truststore with password
KcAdmExec exe = execute("config truststore --trustpass secret '" + truststore.getAbsolutePath() + "'");
assertExitCodeAndStreamSizes(exe, 0, 0, 0);
// perform authentication against server - asks for password, then for truststore password
exe = execute("config credentials --server " + serverUrl + " --realm test --user user1 --password userpass");
assertExitCodeAndStreamSizes(exe, 0, 0, 1);
exe = execute("config truststore --delete");
assertExitCodeAndStreamSizes(exe, 0, 0, 0);
exe = execute("config truststore --delete '" + truststore.getAbsolutePath() + "'");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
Assert.assertEquals("incompatible", "Option --delete is mutually exclusive with specifying a TRUSTSTORE", exe.stderrLines().get(0));
Assert.assertEquals("try help", "Try '" + CMD + " help config truststore' for more information", exe.stderrLines().get(1));
exe = execute("config truststore --delete --trustpass secret");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
Assert.assertEquals("no truststore error", "Options --trustpass and --delete are mutually exclusive", exe.stderrLines().get(0));
Assert.assertEquals("try help", "Try '" + CMD + " help config truststore' for more information", exe.stderrLines().get(1));
FileConfigHandler cfghandler = new FileConfigHandler();
cfghandler.setConfigFile(DEFAULT_CONFIG_FILE_PATH);
ConfigData config = cfghandler.loadConfig();
Assert.assertNull("truststore null", config.getTruststore());
Assert.assertNull("trustpass null", config.getTrustpass());
// perform no-config CRUD test against ssl protected endpoint
testCRUDWithOnTheFlyAuth(serverUrl,
"--user user1 --password userpass", " --truststore '" + truststore.getAbsolutePath() + "' --trustpass secret",
"Logging into " + serverUrl + " as user user1 of realm test");
}
}

View file

@ -0,0 +1,130 @@
package org.keycloak.testsuite.cli.admin;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.client.admin.cli.config.FileConfigHandler;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.testsuite.cli.KcAdmExec;
import org.keycloak.testsuite.util.TempFileResource;
import org.keycloak.util.JsonSerialization;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Arrays;
import static org.keycloak.testsuite.cli.KcAdmExec.CMD;
import static org.keycloak.testsuite.cli.KcAdmExec.execute;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class KcAdmUpdateTest extends AbstractAdmCliTest {
@Test
public void testUpdateThoroughly() throws IOException {
FileConfigHandler handler = initCustomConfigFile();
try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
final String realm = "test";
loginAsUser(configFile.getFile(), serverUrl, realm, "user1", "userpass");
// create an object so we can update it
KcAdmExec exe = execute("create clients --config '" + configFile.getName() + "' -o -s clientId=my_client");
assertExitCodeAndStdErrSize(exe, 0, 0);
ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
Assert.assertEquals("enabled", true, client.isEnabled());
Assert.assertEquals("publicClient", false, client.isPublicClient());
Assert.assertEquals("bearerOnly", false, client.isBearerOnly());
Assert.assertTrue("redirectUris is empty", client.getRedirectUris().isEmpty());
// Merge update
exe = execute("update clients/" + client.getId() + " --config '" + configFile.getName() + "' -o " +
" -s enabled=false -s 'redirectUris=[\"http://localhost:8980/myapp/*\"]'");
assertExitCodeAndStdErrSize(exe, 0, 0);
client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
Assert.assertEquals("enabled", false, client.isEnabled());
Assert.assertEquals("redirectUris", Arrays.asList("http://localhost:8980/myapp/*"), client.getRedirectUris());
// Another merge update - test deleting an attribute, deleting a list item and adding a list item
exe = execute("update clients/" + client.getId() + " --config '" + configFile.getName() + "' -o -d redirectUris[0] -s webOrigins+=http://localhost:8980/myapp -s webOrigins+=http://localhost:8981/myapp -d webOrigins[0]");
assertExitCodeAndStdErrSize(exe, 0, 0);
client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
Assert.assertTrue("redirectUris is empty", client.getRedirectUris().isEmpty());
Assert.assertEquals("webOrigins", Arrays.asList("http://localhost:8981/myapp"), client.getWebOrigins());
// Another merge update - test nested attributes and setting an attribute using json format
// TODO KEYCLOAK-3705 Updating protocolMapper config via client registration endpoint has no effect
/*
exe = execute("update my_client --config '" + configFile.getName() + "' -o -s 'protocolMappers[0].config.\"id.token.claim\"=false' " +
"-s 'protocolMappers[4].config={\"single\": \"true\", \"attribute.nameformat\": \"Basic\", \"attribute.name\": \"Role\"}'");
assertExitCodeAndStdErrSize(exe, 0, 0);
client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
Assert.assertEquals("protocolMapper[0].config.\"id.token.claim\"", "false", client.getProtocolMappers().get(0).getConfig().get("id.token.claim"));
Assert.assertEquals("protocolMappers[4].config.single", "true", client.getProtocolMappers().get(4).getConfig().get("single"));
Assert.assertEquals("protocolMappers[4].config.\"attribute.nameformat\"", "Basic", client.getProtocolMappers().get(4).getConfig().get("attribute.nameformat"));
Assert.assertEquals("protocolMappers[4].config.\"attribute.name\"", "Role", client.getProtocolMappers().get(4).getConfig().get("attribute.name"));
*/
// update using oidc format
// check that using an invalid attribute key is not ignored
exe = execute("update clients/" + client.getId() + " --nonexisting --config '" + configFile.getName() + "'");
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
Assert.assertEquals("error message", "Invalid option: --nonexisting", exe.stderrLines().get(0));
Assert.assertEquals("try help", "Try '" + CMD + " help update' for more information", exe.stderrLines().get(1));
// test overwrite from file
exe = KcAdmExec.newBuilder()
.argsLine("update clients/" + client.getId() + " --config '" + configFile.getName() +
"' -o -s clientId=my_client -s 'redirectUris=[\"http://localhost:8980/myapp/*\"]' -f -")
.stdin(new ByteArrayInputStream("{ \"enabled\": false }".getBytes()))
.execute();
assertExitCodeAndStdErrSize(exe, 0, 0);
client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
// web origin is not sent to the server, thus it retains the current value
Assert.assertEquals("webOrigins", Arrays.asList("http://localhost:8981/myapp"), client.getWebOrigins());
Assert.assertFalse("enabled is false", client.isEnabled());
Assert.assertEquals("redirectUris", Arrays.asList("http://localhost:8980/myapp/*"), client.getRedirectUris());
// test using merge with file
exe = KcAdmExec.newBuilder()
.argsLine("update clients/" + client.getId() + " --config '" + configFile.getName() +
"' -o -s enabled=true -m -f -")
.stdin(new ByteArrayInputStream("{ \"webOrigins\": [\"http://localhost:8980/myapp\"] }".getBytes()))
.execute();
assertExitCodeAndStdErrSize(exe, 0, 0);
client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
Assert.assertEquals("webOrigins", Arrays.asList("http://localhost:8980/myapp"), client.getWebOrigins());
Assert.assertTrue("enabled is true", client.isEnabled());
Assert.assertEquals("redirectUris", Arrays.asList("http://localhost:8980/myapp/*"), client.getRedirectUris());
}
}
}

View file

@ -19,7 +19,7 @@ import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy;
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyManager;
import org.keycloak.services.clientregistration.policy.RegistrationAuth;
import org.keycloak.services.clientregistration.policy.impl.TrustedHostClientRegistrationPolicyFactory;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.cli.AbstractCliTest;
import org.keycloak.testsuite.cli.KcRegExec;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.UserBuilder;
@ -45,7 +45,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public abstract class AbstractCliTest extends AbstractKeycloakTest {
public abstract class AbstractRegCliTest extends AbstractCliTest {
protected String serverUrl = isAuthServerSSL() ?
"https://localhost:" + getAuthServerHttpsPort() + "/auth" :
@ -527,43 +527,4 @@ public abstract class AbstractCliTest extends AbstractKeycloakTest {
lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
Assert.assertEquals("config file not modified", lastModified, lastModified2);
}
void assertExitCodeAndStdOutSize(KcRegExec exe, int exitCode, int stdOutLineCount) {
assertExitCodeAndStreamSizes(exe, exitCode, stdOutLineCount, -1);
}
void assertExitCodeAndStdErrSize(KcRegExec exe, int exitCode, int stdErrLineCount) {
assertExitCodeAndStreamSizes(exe, exitCode, -1, stdErrLineCount);
}
void assertExitCodeAndStreamSizes(KcRegExec exe, int exitCode, int stdOutLineCount, int stdErrLineCount) {
Assert.assertEquals("exitCode == " + exitCode, exitCode, exe.exitCode());
if (stdOutLineCount != -1) {
try {
assertLineCount("stdout output", exe.stdoutLines(), stdOutLineCount);
} catch (Throwable e) {
throw new AssertionError("STDOUT: " + exe.stdoutString(), e);
}
}
if (stdErrLineCount != -1) {
try {
assertLineCount("stderr output", exe.stderrLines(), stdErrLineCount);
} catch (Throwable e) {
throw new AssertionError("STDERR: " + exe.stderrString(), e);
}
}
}
void assertLineCount(String label, List<String> lines, int count) {
if (lines.size() == count) {
return;
}
// there is some kind of race condition in 'kcreg' that results in intermittent extra empty line
if (lines.size() == count + 1) {
if ("".equals(lines.get(lines.size()-1))) {
return;
}
}
Assert.assertTrue(label + " has " + lines.size() + " lines (expected: " + count + ")", lines.size() == count);
}
}

View file

@ -15,7 +15,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class KcRegConfigTest extends AbstractCliTest {
public class KcRegConfigTest extends AbstractRegCliTest {
@Test
public void testRegistrationToken() throws IOException {

View file

@ -19,7 +19,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class KcRegCreateTest extends AbstractCliTest {
public class KcRegCreateTest extends AbstractRegCliTest {
@Test
public void testCreateWithRealmOverride() throws IOException {

View file

@ -23,7 +23,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class KcRegTest extends AbstractCliTest {
public class KcRegTest extends AbstractRegCliTest {
@Test
public void testNoArgs() {
@ -68,7 +68,7 @@ public class KcRegTest extends AbstractCliTest {
exe = execute("config truststore");
assertExitCodeAndStdErrSize(exe, 1, 0);
Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
Assert.assertEquals("help message", "Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWOD] [ARGUMENTS]", exe.stdoutLines().get(0));
Assert.assertEquals("help message", "Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWORD] [ARGUMENTS]", exe.stdoutLines().get(0));
exe = execute("create");
assertExitCodeAndStdErrSize(exe, 1, 0);
@ -172,7 +172,7 @@ public class KcRegTest extends AbstractCliTest {
exe = execute("config truststore --help");
assertExitCodeAndStdErrSize(exe, 0, 0);
Assert.assertEquals("stdout first line",
"Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWOD] [ARGUMENTS]",
"Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWORD] [ARGUMENTS]",
exe.stdoutLines().get(0));
}

View file

@ -18,7 +18,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class KcRegTruststoreTest extends AbstractCliTest {
public class KcRegTruststoreTest extends AbstractRegCliTest {
@Test
public void testTruststore() throws IOException {

View file

@ -18,7 +18,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class KcRegUpdateTest extends AbstractCliTest {
public class KcRegUpdateTest extends AbstractRegCliTest {
@Test

View file

@ -18,7 +18,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class KcRegUpdateTokenTest extends AbstractCliTest {
public class KcRegUpdateTokenTest extends AbstractRegCliTest {
@Test
public void testUpdateToken() throws IOException {