KEYCLOAK-912 Admin CLI
This commit is contained in:
parent
a4cbf130b4
commit
c3d9859c6e
95 changed files with 10123 additions and 497 deletions
|
@ -110,7 +110,7 @@ public class Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void checkGrantType(String grantType) {
|
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 +
|
throw new IllegalArgumentException("Unsupported grantType: " + grantType +
|
||||||
" (only " + PASSWORD + " and " + CLIENT_CREDENTIALS + " are supported)");
|
" (only " + PASSWORD + " and " + CLIENT_CREDENTIALS + " are supported)");
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,18 +43,22 @@ import static org.keycloak.OAuth2Constants.PASSWORD;
|
||||||
public class Keycloak {
|
public class Keycloak {
|
||||||
private final Config config;
|
private final Config config;
|
||||||
private final TokenManager tokenManager;
|
private final TokenManager tokenManager;
|
||||||
|
private String authToken;
|
||||||
private final ResteasyWebTarget target;
|
private final ResteasyWebTarget target;
|
||||||
private final ResteasyClient client;
|
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);
|
config = new Config(serverUrl, realm, username, password, clientId, clientSecret, grantType);
|
||||||
client = resteasyClient != null ? resteasyClient : new ResteasyClientBuilder().connectionPoolSize(10).build();
|
client = resteasyClient != null ? resteasyClient : new ResteasyClientBuilder().connectionPoolSize(10).build();
|
||||||
|
authToken = authtoken;
|
||||||
tokenManager = new TokenManager(config, client);
|
tokenManager = authtoken == null ? new TokenManager(config, client) : null;
|
||||||
|
|
||||||
target = client.target(config.getServerUrl());
|
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) {
|
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)
|
.hostnameVerification(ResteasyClientBuilder.HostnameVerificationPolicy.WILDCARD)
|
||||||
.connectionPoolSize(10).build();
|
.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) {
|
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) {
|
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() {
|
public RealmsResource realms() {
|
||||||
|
@ -100,7 +108,7 @@ public class Keycloak {
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public <T> T proxy(Class<T> proxyClass, URI absoluteURI) {
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -60,8 +60,9 @@ public class KeycloakBuilder {
|
||||||
private String password;
|
private String password;
|
||||||
private String clientId;
|
private String clientId;
|
||||||
private String clientSecret;
|
private String clientSecret;
|
||||||
private String grantType = PASSWORD;
|
private String grantType;
|
||||||
private ResteasyClient resteasyClient;
|
private ResteasyClient resteasyClient;
|
||||||
|
private String authorization;
|
||||||
|
|
||||||
public KeycloakBuilder serverUrl(String serverUrl) {
|
public KeycloakBuilder serverUrl(String serverUrl) {
|
||||||
this.serverUrl = serverUrl;
|
this.serverUrl = serverUrl;
|
||||||
|
@ -104,6 +105,11 @@ public class KeycloakBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public KeycloakBuilder authorization(String auth) {
|
||||||
|
this.authorization = auth;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a new Keycloak client from this builder.
|
* Builds a new Keycloak client from this builder.
|
||||||
*/
|
*/
|
||||||
|
@ -116,6 +122,10 @@ public class KeycloakBuilder {
|
||||||
throw new IllegalStateException("realm required");
|
throw new IllegalStateException("realm required");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (authorization == null && grantType == null) {
|
||||||
|
grantType = PASSWORD;
|
||||||
|
}
|
||||||
|
|
||||||
if (PASSWORD.equals(grantType)) {
|
if (PASSWORD.equals(grantType)) {
|
||||||
if (username == null) {
|
if (username == null) {
|
||||||
throw new IllegalStateException("username required");
|
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");
|
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() {
|
private KeycloakBuilder() {
|
||||||
|
|
|
@ -49,8 +49,10 @@ public class BearerAuthFilter implements ClientRequestFilter, ClientResponseFilt
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void filter(ClientRequestContext requestContext) throws IOException {
|
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);
|
requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
177
integration/client-cli/admin-cli/pom.xml
Executable file
177
integration/client-cli/admin-cli/pom.xml
Executable 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>
|
8
integration/client-cli/admin-cli/src/main/bin/kcadm.bat
Normal file
8
integration/client-cli/admin-cli/src/main/bin/kcadm.bat
Normal 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 %*
|
23
integration/client-cli/admin-cli/src/main/bin/kcadm.sh
Executable file
23
integration/client-cli/admin-cli/src/main/bin/kcadm.sh
Executable 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 "$@"
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 + "]" : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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:")) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 + "]";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,11 +36,23 @@
|
||||||
<outputDirectory>keycloak-client-tools/bin</outputDirectory>
|
<outputDirectory>keycloak-client-tools/bin</outputDirectory>
|
||||||
<filtered>true</filtered>
|
<filtered>true</filtered>
|
||||||
</file>
|
</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>
|
</files>
|
||||||
<dependencySets>
|
<dependencySets>
|
||||||
<dependencySet>
|
<dependencySet>
|
||||||
<includes>
|
<includes>
|
||||||
<include>org.keycloak:keycloak-client-registration-cli</include>
|
<include>org.keycloak:keycloak-client-registration-cli</include>
|
||||||
|
<include>org.keycloak:keycloak-admin-cli</include>
|
||||||
</includes>
|
</includes>
|
||||||
<outputDirectory>keycloak-client-tools/bin/client</outputDirectory>
|
<outputDirectory>keycloak-client-tools/bin/client</outputDirectory>
|
||||||
</dependencySet>
|
</dependencySet>
|
||||||
|
|
|
@ -34,6 +34,10 @@
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<artifactId>keycloak-client-registration-cli</artifactId>
|
<artifactId>keycloak-client-registration-cli</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-admin-cli</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
|
@ -33,7 +33,6 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jboss.aesh</groupId>
|
<groupId>org.jboss.aesh</groupId>
|
||||||
<artifactId>aesh</artifactId>
|
<artifactId>aesh</artifactId>
|
||||||
<version>0.66.10</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
|
|
|
@ -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;
|
package org.keycloak.client.registration.cli.commands;
|
||||||
|
|
||||||
import org.jboss.aesh.cl.CommandDefinition;
|
import org.jboss.aesh.cl.CommandDefinition;
|
||||||
|
@ -153,7 +169,7 @@ public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd implements Comma
|
||||||
public static String usage() {
|
public static String usage() {
|
||||||
StringWriter sb = new StringWriter();
|
StringWriter sb = new StringWriter();
|
||||||
PrintWriter out = new PrintWriter(sb);
|
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();
|
||||||
out.println("Command to configure a global truststore to use when using https to connect to Keycloak server.");
|
out.println("Command to configure a global truststore to use when using https to connect to Keycloak server.");
|
||||||
out.println();
|
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("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(" " + PROMPT + " " + CMD + " config truststore " + OS_ARCH.path("~/.keycloak/truststore.jks"));
|
||||||
out.println();
|
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(" " + PROMPT + " " + CMD + " config truststore --storepass " + OS_ARCH.envVar("PASSWORD") + " " + OS_ARCH.path("~/.keycloak/truststore.jks"));
|
||||||
out.println();
|
out.println();
|
||||||
out.println("Remove truststore configuration:");
|
out.println("Remove truststore configuration:");
|
||||||
|
|
|
@ -30,8 +30,19 @@
|
||||||
<artifactId>keycloak-client-cli-parent</artifactId>
|
<artifactId>keycloak-client-cli-parent</artifactId>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.aesh</groupId>
|
||||||
|
<artifactId>aesh</artifactId>
|
||||||
|
<version>0.66.10</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>client-registration-cli</module>
|
<module>client-registration-cli</module>
|
||||||
|
<module>admin-cli</module>
|
||||||
<module>client-cli-dist</module>
|
<module>client-cli-dist</module>
|
||||||
</modules>
|
</modules>
|
||||||
</project>
|
</project>
|
5
pom.xml
5
pom.xml
|
@ -1321,6 +1321,11 @@
|
||||||
<artifactId>keycloak-client-registration-cli</artifactId>
|
<artifactId>keycloak-client-registration-cli</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-admin-cli</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<artifactId>keycloak-client-cli-dist</artifactId>
|
<artifactId>keycloak-client-cli-dist</artifactId>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,8 @@
|
||||||
package org.keycloak.testsuite.cli;
|
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.BufferedReader;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
@ -18,57 +21,27 @@ import java.util.concurrent.TimeUnit;
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
* @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 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";
|
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) {
|
private KcRegExec(String workDir, String argsLine, InputStream stdin) {
|
||||||
this(workDir, argsLine, null, stdin);
|
this(workDir, argsLine, null, stdin);
|
||||||
}
|
}
|
||||||
|
|
||||||
private KcRegExec(String workDir, String argsLine, String env, InputStream stdin) {
|
private KcRegExec(String workDir, String argsLine, String env, InputStream stdin) {
|
||||||
if (workDir != null) {
|
super(workDir, argsLine, env, stdin);
|
||||||
this.workDir = workDir;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.argsLine = argsLine;
|
@Override
|
||||||
this.env = env;
|
public String getCmd() {
|
||||||
|
return "bin/" + CMD;
|
||||||
if (stdin != null) {
|
|
||||||
this.stdin = stdin;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Builder newBuilder() {
|
public static KcRegExec.Builder newBuilder() {
|
||||||
return new Builder();
|
return (KcRegExec.Builder) new KcRegExec.Builder().workDir(WORK_DIR);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static KcRegExec execute(String args) {
|
public static KcRegExec execute(String args) {
|
||||||
|
@ -77,225 +50,9 @@ public class KcRegExec {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void execute() {
|
public static class Builder extends AbstractExecBuilder<KcRegExec> {
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@Override
|
||||||
public KcRegExec execute() {
|
public KcRegExec execute() {
|
||||||
KcRegExec exe = new KcRegExec(workDir, argsLine, env, stdin);
|
KcRegExec exe = new KcRegExec(workDir, argsLine, env, stdin);
|
||||||
exe.dumpStreams = dumpStreams;
|
exe.dumpStreams = dumpStreams;
|
||||||
|
@ -303,6 +60,7 @@ public class KcRegExec {
|
||||||
return exe;
|
return exe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public KcRegExec executeAsync() {
|
public KcRegExec executeAsync() {
|
||||||
KcRegExec exe = new KcRegExec(workDir, argsLine, env, stdin);
|
KcRegExec exe = new KcRegExec(workDir, argsLine, env, stdin);
|
||||||
exe.dumpStreams = dumpStreams;
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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>
|
* @author <a href="mailto:marko.strukelj@gmail.com">Marko Strukelj</a>
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 '"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,7 +19,7 @@ import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy;
|
||||||
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyManager;
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyManager;
|
||||||
import org.keycloak.services.clientregistration.policy.RegistrationAuth;
|
import org.keycloak.services.clientregistration.policy.RegistrationAuth;
|
||||||
import org.keycloak.services.clientregistration.policy.impl.TrustedHostClientRegistrationPolicyFactory;
|
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.cli.KcRegExec;
|
||||||
import org.keycloak.testsuite.util.ClientBuilder;
|
import org.keycloak.testsuite.util.ClientBuilder;
|
||||||
import org.keycloak.testsuite.util.UserBuilder;
|
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>
|
* @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() ?
|
protected String serverUrl = isAuthServerSSL() ?
|
||||||
"https://localhost:" + getAuthServerHttpsPort() + "/auth" :
|
"https://localhost:" + getAuthServerHttpsPort() + "/auth" :
|
||||||
|
@ -527,43 +527,4 @@ public abstract class AbstractCliTest extends AbstractKeycloakTest {
|
||||||
lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
|
lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
|
||||||
Assert.assertEquals("config file not modified", lastModified, lastModified2);
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -15,7 +15,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
*/
|
*/
|
||||||
public class KcRegConfigTest extends AbstractCliTest {
|
public class KcRegConfigTest extends AbstractRegCliTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRegistrationToken() throws IOException {
|
public void testRegistrationToken() throws IOException {
|
||||||
|
|
|
@ -19,7 +19,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
*/
|
*/
|
||||||
public class KcRegCreateTest extends AbstractCliTest {
|
public class KcRegCreateTest extends AbstractRegCliTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCreateWithRealmOverride() throws IOException {
|
public void testCreateWithRealmOverride() throws IOException {
|
||||||
|
|
|
@ -23,7 +23,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
*/
|
*/
|
||||||
public class KcRegTest extends AbstractCliTest {
|
public class KcRegTest extends AbstractRegCliTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testNoArgs() {
|
public void testNoArgs() {
|
||||||
|
@ -68,7 +68,7 @@ public class KcRegTest extends AbstractCliTest {
|
||||||
exe = execute("config truststore");
|
exe = execute("config truststore");
|
||||||
assertExitCodeAndStdErrSize(exe, 1, 0);
|
assertExitCodeAndStdErrSize(exe, 1, 0);
|
||||||
Assert.assertTrue("help message returned", exe.stdoutLines().size() > 10);
|
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");
|
exe = execute("create");
|
||||||
assertExitCodeAndStdErrSize(exe, 1, 0);
|
assertExitCodeAndStdErrSize(exe, 1, 0);
|
||||||
|
@ -172,7 +172,7 @@ public class KcRegTest extends AbstractCliTest {
|
||||||
exe = execute("config truststore --help");
|
exe = execute("config truststore --help");
|
||||||
assertExitCodeAndStdErrSize(exe, 0, 0);
|
assertExitCodeAndStdErrSize(exe, 0, 0);
|
||||||
Assert.assertEquals("stdout first line",
|
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));
|
exe.stdoutLines().get(0));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
*/
|
*/
|
||||||
public class KcRegTruststoreTest extends AbstractCliTest {
|
public class KcRegTruststoreTest extends AbstractRegCliTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTruststore() throws IOException {
|
public void testTruststore() throws IOException {
|
||||||
|
|
|
@ -18,7 +18,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
*/
|
*/
|
||||||
public class KcRegUpdateTest extends AbstractCliTest {
|
public class KcRegUpdateTest extends AbstractRegCliTest {
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -18,7 +18,7 @@ import static org.keycloak.testsuite.cli.KcRegExec.execute;
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
*/
|
*/
|
||||||
public class KcRegUpdateTokenTest extends AbstractCliTest {
|
public class KcRegUpdateTokenTest extends AbstractRegCliTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testUpdateToken() throws IOException {
|
public void testUpdateToken() throws IOException {
|
||||||
|
|
Binary file not shown.
Loading…
Reference in a new issue