Merge pull request #3325 from mstruk/cli-reg
KEYCLOAK-2084 Client Registration CLI
This commit is contained in:
commit
14a51e589d
76 changed files with 8728 additions and 286 deletions
|
@ -79,5 +79,13 @@
|
||||||
<include>layers.conf</include>
|
<include>layers.conf</include>
|
||||||
</includes>
|
</includes>
|
||||||
</fileSet>
|
</fileSet>
|
||||||
|
<fileSet>
|
||||||
|
<directory>target/unpacked/keycloak-client-tools</directory>
|
||||||
|
<outputDirectory/>
|
||||||
|
<filtered>false</filtered>
|
||||||
|
<includes>
|
||||||
|
<include>**/*</include>
|
||||||
|
</includes>
|
||||||
|
</fileSet>
|
||||||
</fileSets>
|
</fileSets>
|
||||||
</assembly>
|
</assembly>
|
||||||
|
|
|
@ -81,6 +81,29 @@
|
||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-dependency-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>unpack-client-cli-dist</id>
|
||||||
|
<phase>prepare-package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>unpack</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<artifactItems>
|
||||||
|
<artifactItem>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-client-cli-dist</artifactId>
|
||||||
|
<type>zip</type>
|
||||||
|
<outputDirectory>${project.build.directory}/unpacked</outputDirectory>
|
||||||
|
</artifactItem>
|
||||||
|
</artifactItems>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,9 @@ import org.keycloak.admin.client.resource.RealmsResource;
|
||||||
import org.keycloak.admin.client.resource.ServerInfoResource;
|
import org.keycloak.admin.client.resource.ServerInfoResource;
|
||||||
import org.keycloak.admin.client.token.TokenManager;
|
import org.keycloak.admin.client.token.TokenManager;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
|
||||||
import static org.keycloak.OAuth2Constants.PASSWORD;
|
import static org.keycloak.OAuth2Constants.PASSWORD;
|
||||||
|
|
||||||
|
@ -55,6 +57,15 @@ public class Keycloak {
|
||||||
target.register(new BearerAuthFilter(tokenManager));
|
target.register(new BearerAuthFilter(tokenManager));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, SSLContext sslContext) {
|
||||||
|
ResteasyClient client = new ResteasyClientBuilder()
|
||||||
|
.sslContext(sslContext)
|
||||||
|
.hostnameVerification(ResteasyClientBuilder.HostnameVerificationPolicy.WILDCARD)
|
||||||
|
.connectionPoolSize(10).build();
|
||||||
|
|
||||||
|
return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, client);
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
49
integration/client-cli/client-cli-dist/assembly.xml
Executable file
49
integration/client-cli/client-cli-dist/assembly.xml
Executable file
|
@ -0,0 +1,49 @@
|
||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<assembly>
|
||||||
|
<id>keycloak-client-cli-dist</id>
|
||||||
|
|
||||||
|
<formats>
|
||||||
|
<format>zip</format>
|
||||||
|
</formats>
|
||||||
|
|
||||||
|
<includeBaseDirectory>false</includeBaseDirectory>
|
||||||
|
|
||||||
|
<files>
|
||||||
|
<file>
|
||||||
|
<source>../client-registration-cli/src/main/bin/kcreg.sh</source>
|
||||||
|
<outputDirectory>keycloak-client-tools/bin</outputDirectory>
|
||||||
|
<fileMode>0755</fileMode>
|
||||||
|
<filtered>true</filtered>
|
||||||
|
</file>
|
||||||
|
<file>
|
||||||
|
<source>../client-registration-cli/src/main/bin/kcreg.bat</source>
|
||||||
|
<outputDirectory>keycloak-client-tools/bin</outputDirectory>
|
||||||
|
<filtered>true</filtered>
|
||||||
|
</file>
|
||||||
|
</files>
|
||||||
|
<dependencySets>
|
||||||
|
<dependencySet>
|
||||||
|
<includes>
|
||||||
|
<include>org.keycloak:keycloak-client-registration-cli</include>
|
||||||
|
</includes>
|
||||||
|
<outputDirectory>keycloak-client-tools/bin/client</outputDirectory>
|
||||||
|
</dependencySet>
|
||||||
|
</dependencySets>
|
||||||
|
|
||||||
|
</assembly>
|
69
integration/client-cli/client-cli-dist/pom.xml
Executable file
69
integration/client-cli/client-cli-dist/pom.xml
Executable file
|
@ -0,0 +1,69 @@
|
||||||
|
<!--
|
||||||
|
~ 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">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<artifactId>keycloak-client-cli-parent</artifactId>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<version>2.3.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>keycloak-client-cli-dist</artifactId>
|
||||||
|
<packaging>pom</packaging>
|
||||||
|
<name>Keycloak Client CLI Distribution</name>
|
||||||
|
<description/>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-client-registration-cli</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<finalName>keycloak-client-cli-${project.version}</finalName>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-assembly-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>assemble</id>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>single</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<descriptors>
|
||||||
|
<descriptor>assembly.xml</descriptor>
|
||||||
|
</descriptors>
|
||||||
|
<outputDirectory>
|
||||||
|
target
|
||||||
|
</outputDirectory>
|
||||||
|
<workDirectory>
|
||||||
|
target/assembly/work
|
||||||
|
</workDirectory>
|
||||||
|
<appendAssemblyId>false</appendAssemblyId>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
143
integration/client-cli/client-registration-cli/pom.xml
Executable file
143
integration/client-cli/client-registration-cli/pom.xml
Executable file
|
@ -0,0 +1,143 @@
|
||||||
|
<?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.3.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>keycloak-client-registration-cli</artifactId>
|
||||||
|
<name>Keycloak Client Registration CLI</name>
|
||||||
|
<description/>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.aesh</groupId>
|
||||||
|
<artifactId>aesh</artifactId>
|
||||||
|
<version>0.66.10</version>
|
||||||
|
</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/AccessTokenResponse.class</include>
|
||||||
|
<include>org/keycloak/representations/idm/ClientRepresentation.class</include>
|
||||||
|
<include>org/keycloak/representations/idm/ProtocolMapperRepresentation.class</include>
|
||||||
|
<include>org/keycloak/representations/oidc/OIDCClientRepresentation.class</include>
|
||||||
|
<include>org/keycloak/representations/idm/authorization/**</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>*:*</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>
|
|
@ -0,0 +1,8 @@
|
||||||
|
@echo off
|
||||||
|
|
||||||
|
if "%OS%" == "Windows_NT" (
|
||||||
|
set "DIRNAME=%~dp0%"
|
||||||
|
) else (
|
||||||
|
set DIRNAME=.\
|
||||||
|
)
|
||||||
|
java %KC_OPTS% -cp %DIRNAME%\client\keycloak-client-registration-cli-${project.version}.jar org.keycloak.client.registration.cli.KcRegMain %*
|
3
integration/client-cli/client-registration-cli/src/main/bin/kcreg.sh
Executable file
3
integration/client-cli/client-registration-cli/src/main/bin/kcreg.sh
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
DIRNAME=`dirname "$0"`
|
||||||
|
java $KC_OPTS -cp $DIRNAME/client/keycloak-client-registration-cli-${project.version}.jar org.keycloak.client.registration.cli.KcRegMain "$@"
|
|
@ -0,0 +1,75 @@
|
||||||
|
package org.keycloak.client.registration.cli;
|
||||||
|
|
||||||
|
import org.jboss.aesh.console.AeshConsoleBuilder;
|
||||||
|
import org.jboss.aesh.console.AeshConsoleImpl;
|
||||||
|
import org.jboss.aesh.console.Prompt;
|
||||||
|
import org.jboss.aesh.console.command.registry.AeshCommandRegistryBuilder;
|
||||||
|
import org.jboss.aesh.console.command.registry.CommandRegistry;
|
||||||
|
import org.jboss.aesh.console.settings.Settings;
|
||||||
|
import org.jboss.aesh.console.settings.SettingsBuilder;
|
||||||
|
import org.keycloak.client.registration.cli.aesh.AeshEnhancer;
|
||||||
|
import org.keycloak.client.registration.cli.aesh.ValveInputStream;
|
||||||
|
import org.keycloak.client.registration.cli.aesh.Globals;
|
||||||
|
import org.keycloak.client.registration.cli.commands.KcRegCmd;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public class KcRegMain {
|
||||||
|
|
||||||
|
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(KcRegCmd.class)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
AeshConsoleImpl console = (AeshConsoleImpl) new AeshConsoleBuilder()
|
||||||
|
.settings(settings)
|
||||||
|
.commandRegistry(registry)
|
||||||
|
.prompt(new Prompt(""))
|
||||||
|
.create();
|
||||||
|
|
||||||
|
AeshEnhancer.enhance(console);
|
||||||
|
|
||||||
|
// work around parser issues with quotes and brackets
|
||||||
|
ArrayList<String> arguments = new ArrayList<>();
|
||||||
|
arguments.add("kcreg");
|
||||||
|
arguments.addAll(Arrays.asList(args));
|
||||||
|
Globals.args = arguments;
|
||||||
|
|
||||||
|
StringBuilder b = new StringBuilder();
|
||||||
|
for (String s : args) {
|
||||||
|
// quote if necessary
|
||||||
|
boolean needQuote = false;
|
||||||
|
needQuote = s.indexOf(' ') != -1 || s.indexOf('\"') != -1 || s.indexOf('\'') != -1;
|
||||||
|
b.append(' ');
|
||||||
|
if (needQuote) {
|
||||||
|
b.append('\'');
|
||||||
|
}
|
||||||
|
b.append(s);
|
||||||
|
if (needQuote) {
|
||||||
|
b.append('\'');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.setEcho(false);
|
||||||
|
|
||||||
|
console.execute("kcreg" + b.toString());
|
||||||
|
|
||||||
|
console.start();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
package org.keycloak.client.registration.cli.aesh;
|
||||||
|
|
||||||
|
import org.jboss.aesh.cl.parser.OptionParserException;
|
||||||
|
import org.jboss.aesh.cl.result.ResultHandler;
|
||||||
|
import org.jboss.aesh.console.AeshConsoleCallback;
|
||||||
|
import org.jboss.aesh.console.AeshConsoleImpl;
|
||||||
|
import org.jboss.aesh.console.ConsoleOperation;
|
||||||
|
import org.jboss.aesh.console.command.CommandNotFoundException;
|
||||||
|
import org.jboss.aesh.console.command.CommandResult;
|
||||||
|
import org.jboss.aesh.console.command.container.CommandContainer;
|
||||||
|
import org.jboss.aesh.console.command.container.CommandContainerResult;
|
||||||
|
import org.jboss.aesh.console.command.invocation.AeshCommandInvocation;
|
||||||
|
import org.jboss.aesh.console.command.invocation.AeshCommandInvocationProvider;
|
||||||
|
import org.jboss.aesh.parser.AeshLine;
|
||||||
|
import org.jboss.aesh.parser.ParserStatus;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
class AeshConsoleCallbackImpl extends AeshConsoleCallback {
|
||||||
|
|
||||||
|
private final AeshConsoleImpl console;
|
||||||
|
private CommandResult result;
|
||||||
|
|
||||||
|
AeshConsoleCallbackImpl(AeshConsoleImpl aeshConsole) {
|
||||||
|
this.console = aeshConsole;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public int execute(ConsoleOperation output) throws InterruptedException {
|
||||||
|
if (output != null && output.getBuffer().trim().length() > 0) {
|
||||||
|
ResultHandler resultHandler = null;
|
||||||
|
//AeshLine aeshLine = Parser.findAllWords(output.getBuffer());
|
||||||
|
AeshLine aeshLine = new AeshLine(output.getBuffer(), Globals.args, ParserStatus.OK, "");
|
||||||
|
try (CommandContainer commandContainer = getCommand(output, aeshLine)) {
|
||||||
|
resultHandler = commandContainer.getParser().getProcessedCommand().getResultHandler();
|
||||||
|
CommandContainerResult ccResult =
|
||||||
|
commandContainer.executeCommand(aeshLine, console.getInvocationProviders(), console.getAeshContext(),
|
||||||
|
new AeshCommandInvocationProvider().enhanceCommandInvocation(
|
||||||
|
new AeshCommandInvocation(console,
|
||||||
|
output.getControlOperator(), output.getPid(), this)));
|
||||||
|
|
||||||
|
result = ccResult.getCommandResult();
|
||||||
|
|
||||||
|
if(result == CommandResult.SUCCESS && resultHandler != null)
|
||||||
|
resultHandler.onSuccess();
|
||||||
|
else if(resultHandler != null)
|
||||||
|
resultHandler.onFailure(result);
|
||||||
|
|
||||||
|
} 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,25 @@
|
||||||
|
package org.keycloak.client.registration.cli.aesh;
|
||||||
|
|
||||||
|
import org.jboss.aesh.console.AeshConsoleImpl;
|
||||||
|
import org.jboss.aesh.console.Console;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public class AeshEnhancer {
|
||||||
|
|
||||||
|
public static void enhance(AeshConsoleImpl console) {
|
||||||
|
try {
|
||||||
|
Globals.stdin.setConsole(console);
|
||||||
|
|
||||||
|
Field field = AeshConsoleImpl.class.getDeclaredField("console");
|
||||||
|
field.setAccessible(true);
|
||||||
|
Console internalConsole = (Console) field.get(console);
|
||||||
|
internalConsole.setConsoleCallback(new AeshConsoleCallbackImpl(console));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to install Aesh enhancement", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package org.keycloak.client.registration.cli.aesh;
|
||||||
|
|
||||||
|
import org.jboss.aesh.cl.converter.Converter;
|
||||||
|
import org.jboss.aesh.cl.validator.OptionValidatorException;
|
||||||
|
import org.jboss.aesh.console.command.converter.ConverterInvocation;
|
||||||
|
import org.keycloak.client.registration.cli.common.EndpointType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public class EndpointTypeConverter implements Converter<EndpointType, ConverterInvocation> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EndpointType convert(ConverterInvocation converterInvocation) throws OptionValidatorException {
|
||||||
|
return EndpointType.of(converterInvocation.getInput());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package org.keycloak.client.registration.cli.aesh;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public class Globals {
|
||||||
|
|
||||||
|
public static boolean dumpTrace = false;
|
||||||
|
|
||||||
|
public static ValveInputStream stdin;
|
||||||
|
|
||||||
|
public static List<String> args;
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package org.keycloak.client.registration.cli.aesh;
|
||||||
|
|
||||||
|
import org.jboss.aesh.console.AeshConsoleImpl;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InterruptedIOException;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This stream blocks and waits, until there is some stream in the queue.
|
||||||
|
* It reads all streams from the queue, and then blocks until it receives more.
|
||||||
|
*/
|
||||||
|
public class ValveInputStream extends InputStream {
|
||||||
|
|
||||||
|
private BlockingQueue<InputStream> queue = new LinkedBlockingQueue<>(10);
|
||||||
|
|
||||||
|
private InputStream current;
|
||||||
|
|
||||||
|
private AeshConsoleImpl console;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException {
|
||||||
|
if (current == null) {
|
||||||
|
try {
|
||||||
|
current = queue.take();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new InterruptedIOException("Signalled to exit");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int c = current.read();
|
||||||
|
if (c == -1) {
|
||||||
|
//current = null;
|
||||||
|
if (console != null) {
|
||||||
|
console.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For some reason AeshInputStream wants to do blocking read of whole buffers, which for stdin
|
||||||
|
* results in blocked input.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int read(byte b[], int off, int len) throws IOException {
|
||||||
|
int c = read();
|
||||||
|
if (c == -1) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
b[off] = (byte) c;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInputStream(InputStream is) {
|
||||||
|
if (queue.contains(is)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queue.add(is);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConsole(AeshConsoleImpl console) {
|
||||||
|
this.console = console;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isStdinAvailable() {
|
||||||
|
return console.isRunning();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,235 @@
|
||||||
|
package org.keycloak.client.registration.cli.commands;
|
||||||
|
|
||||||
|
import org.jboss.aesh.cl.Option;
|
||||||
|
import org.jboss.aesh.console.command.invocation.CommandInvocation;
|
||||||
|
import org.keycloak.client.registration.cli.config.ConfigData;
|
||||||
|
import org.keycloak.client.registration.cli.config.ConfigHandler;
|
||||||
|
import org.keycloak.client.registration.cli.config.FileConfigHandler;
|
||||||
|
import org.keycloak.client.registration.cli.config.InMemoryConfigHandler;
|
||||||
|
import org.keycloak.client.registration.cli.config.RealmConfigData;
|
||||||
|
import org.keycloak.client.registration.cli.util.ConfigUtil;
|
||||||
|
import org.keycloak.client.registration.cli.util.HttpUtil;
|
||||||
|
import org.keycloak.client.registration.cli.util.IoUtil;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import static org.keycloak.client.registration.cli.config.FileConfigHandler.setConfigFile;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.checkAuthInfo;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.checkServerInfo;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
|
||||||
|
|
||||||
|
static final String DEFAULT_CLIENT = "admin-cli";
|
||||||
|
|
||||||
|
|
||||||
|
@Option(name = "config", description = "Path to the config file (~/.keycloak/kcreg.config by default)", hasValue = true)
|
||||||
|
protected String config;
|
||||||
|
|
||||||
|
@Option(name = "no-config", description = "No configuration file should be used, no authentication info should be saved", hasValue = false)
|
||||||
|
protected boolean noconfig;
|
||||||
|
|
||||||
|
@Option(name = "server", description = "Server endpoint url (e.g. 'http://localhost:8080/auth')", hasValue = true)
|
||||||
|
protected String server;
|
||||||
|
|
||||||
|
@Option(name = "realm", description = "Realm name to authenticate against", hasValue = true)
|
||||||
|
protected String realm;
|
||||||
|
|
||||||
|
@Option(name = "client", description = "Realm name to authenticate against", hasValue = true)
|
||||||
|
protected String clientId;
|
||||||
|
|
||||||
|
@Option(name = "user", description = "Username to login with", hasValue = true)
|
||||||
|
protected String user;
|
||||||
|
|
||||||
|
@Option(name = "password", description = "Password to login with (prompted for if not specified and --user is used)", hasValue = true)
|
||||||
|
protected String password;
|
||||||
|
|
||||||
|
@Option(name = "secret", description = "Secret to authenticate the client (prompted for if no --user or --keystore is specified)", hasValue = true)
|
||||||
|
protected String secret;
|
||||||
|
|
||||||
|
@Option(name = "keystore", description = "Path to a keystore containing private key", hasValue = true)
|
||||||
|
protected String keystore;
|
||||||
|
|
||||||
|
@Option(name = "storepass", description = "Keystore password (prompted for if not specified and --keystore is used)", hasValue = true)
|
||||||
|
protected String storePass;
|
||||||
|
|
||||||
|
@Option(name = "keypass", description = "Key password (prompted for if not specified and --keystore is used without --storepass, \n otherwise defaults to keystore password)", hasValue = true)
|
||||||
|
protected String keyPass;
|
||||||
|
|
||||||
|
@Option(name = "alias", description = "Alias of the key inside a keystore (defaults to the value of ClientId)", hasValue = true)
|
||||||
|
protected String alias;
|
||||||
|
|
||||||
|
@Option(name = "truststore", description = "Path to a truststore", hasValue = true)
|
||||||
|
protected String trustStore;
|
||||||
|
|
||||||
|
@Option(name = "trustpass", description = "Truststore password (prompted for if not specified and --truststore is used)", hasValue = true)
|
||||||
|
protected String trustPass;
|
||||||
|
|
||||||
|
@Option(shortName = 't', name = "token", description = "Initial / Registration access token to use)", hasValue = true)
|
||||||
|
protected String token;
|
||||||
|
|
||||||
|
protected void init(AbstractAuthOptionsCmd parent) {
|
||||||
|
|
||||||
|
super.init(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;
|
||||||
|
token = parent.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void applyDefaultOptionValues() {
|
||||||
|
if (clientId == null) {
|
||||||
|
clientId = DEFAULT_CLIENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(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,22 @@
|
||||||
|
package org.keycloak.client.registration.cli.commands;
|
||||||
|
|
||||||
|
import org.jboss.aesh.cl.Option;
|
||||||
|
import org.jboss.aesh.console.command.Command;
|
||||||
|
import org.keycloak.client.registration.cli.aesh.Globals;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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)
|
||||||
|
protected boolean dumpTrace;
|
||||||
|
|
||||||
|
protected void init(AbstractGlobalOptionsCmd parent) {
|
||||||
|
dumpTrace = parent.dumpTrace;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void processGlobalOptions() {
|
||||||
|
Globals.dumpTrace = dumpTrace;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,152 @@
|
||||||
|
package org.keycloak.client.registration.cli.commands;
|
||||||
|
|
||||||
|
import org.jboss.aesh.cl.Arguments;
|
||||||
|
import org.jboss.aesh.cl.CommandDefinition;
|
||||||
|
import org.jboss.aesh.cl.Option;
|
||||||
|
import org.jboss.aesh.console.command.CommandException;
|
||||||
|
import org.jboss.aesh.console.command.CommandResult;
|
||||||
|
import org.jboss.aesh.console.command.invocation.CommandInvocation;
|
||||||
|
import org.keycloak.client.registration.cli.common.AttributeKey;
|
||||||
|
import org.keycloak.client.registration.cli.common.EndpointType;
|
||||||
|
import org.keycloak.client.registration.cli.util.ReflectionUtil;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||||
|
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.ParameterizedType;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ReflectionUtil.getAttributeListWithJSonTypes;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ReflectionUtil.isBasicType;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ReflectionUtil.isListType;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ReflectionUtil.isMapType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
@CommandDefinition(name = "attrs", description = "[ATTRIBUTE] [--endpoint TYPE]")
|
||||||
|
public class AttrsCmd extends AbstractGlobalOptionsCmd {
|
||||||
|
|
||||||
|
@Option(shortName = 'e', name = "endpoint", description = "Endpoint type to use", hasValue = true)
|
||||||
|
protected String endpoint;
|
||||||
|
|
||||||
|
@Arguments
|
||||||
|
protected List<String> args;
|
||||||
|
|
||||||
|
protected String attr;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
|
||||||
|
try {
|
||||||
|
processGlobalOptions();
|
||||||
|
|
||||||
|
|
||||||
|
EndpointType regType = EndpointType.DEFAULT;
|
||||||
|
PrintStream out = commandInvocation.getShell().out();
|
||||||
|
|
||||||
|
if (endpoint != null) {
|
||||||
|
regType = EndpointType.of(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args != null) {
|
||||||
|
if (args.size() > 1) {
|
||||||
|
throw new RuntimeException("Invalid option: " + args.get(1));
|
||||||
|
}
|
||||||
|
attr = args.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Class type = regType == EndpointType.DEFAULT ? ClientRepresentation.class : (regType == EndpointType.OIDC ? OIDCClientRepresentation.class : null);
|
||||||
|
if (type == null) {
|
||||||
|
throw new RuntimeException("Endpoint not supported: " + regType);
|
||||||
|
}
|
||||||
|
AttributeKey key = attr == null ? new AttributeKey() : new AttributeKey(attr);
|
||||||
|
|
||||||
|
Field f = ReflectionUtil.resolveField(type, key);
|
||||||
|
String ts = f != null ? ReflectionUtil.getTypeString(null, f) : null;
|
||||||
|
|
||||||
|
if (f == null) {
|
||||||
|
out.printf("Attributes for %s format:\n", regType.getEndpoint());
|
||||||
|
|
||||||
|
LinkedHashMap<String, String> items = getAttributeListWithJSonTypes(type, key);
|
||||||
|
for (Map.Entry<String, String> item : items.entrySet()) {
|
||||||
|
out.printf(" %-40s %s\n", item.getKey(), item.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
out.printf("%-40s %s", attr, ts);
|
||||||
|
boolean eol = false;
|
||||||
|
|
||||||
|
Type t = f.getGenericType();
|
||||||
|
if (isListType(f.getType()) && t instanceof ParameterizedType) {
|
||||||
|
t = ((ParameterizedType) t).getActualTypeArguments()[0];
|
||||||
|
if (!isBasicType(t) && t instanceof Class) {
|
||||||
|
eol = true;
|
||||||
|
System.out.printf(", where value is:\n", ts);
|
||||||
|
LinkedHashMap<String, String> items = ReflectionUtil.getAttributeListWithJSonTypes((Class) t, null);
|
||||||
|
for (Map.Entry<String, String> item : items.entrySet()) {
|
||||||
|
out.printf(" %-36s %s\n", item.getKey(), item.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (isMapType(f.getType()) && t instanceof ParameterizedType) {
|
||||||
|
t = ((ParameterizedType) t).getActualTypeArguments()[1];
|
||||||
|
if (!isBasicType(t) && t instanceof Class) {
|
||||||
|
eol = true;
|
||||||
|
out.printf(", where value is:\n", ts);
|
||||||
|
LinkedHashMap<String, String> items = ReflectionUtil.getAttributeListWithJSonTypes((Class) t, null);
|
||||||
|
for (Map.Entry<String, String> item : items.entrySet()) {
|
||||||
|
out.printf(" %-36s %s\n", item.getKey(), item.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!eol) {
|
||||||
|
// add end of line
|
||||||
|
out.println();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CommandResult.SUCCESS;
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
commandInvocation.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String usage() {
|
||||||
|
StringWriter sb = new StringWriter();
|
||||||
|
PrintWriter out = new PrintWriter(sb);
|
||||||
|
out.println("Usage: " + CMD + " attrs [ATTRIBUTE] [ARGUMENTS]");
|
||||||
|
out.println();
|
||||||
|
out.println("List available configuration attributes.");
|
||||||
|
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(" ATTRIBUTE Attribute key (if omitted all attributes for the endpoint type are listed)");
|
||||||
|
out.println(" Dot notation can be used to target sub-attributes.");
|
||||||
|
out.println(" -e, --endpoint TYPE Endpoint type to use - one of: 'default', 'oidc' (if omitted 'default' is used)");
|
||||||
|
out.println();
|
||||||
|
out.println("Examples:");
|
||||||
|
out.println();
|
||||||
|
out.println("List all attributes for default endpoint:");
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " attrs");
|
||||||
|
out.println();
|
||||||
|
out.println("List (sub)attributes of 'protocolMappers' attribute for default endpoint:");
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " attrs protocolMappers");
|
||||||
|
out.println();
|
||||||
|
out.println();
|
||||||
|
out.println("Use '" + CMD + " help' for general information and a list of commands");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.client.registration.cli.commands;
|
||||||
|
|
||||||
|
import org.jboss.aesh.cl.Arguments;
|
||||||
|
import org.jboss.aesh.cl.GroupCommandDefinition;
|
||||||
|
import org.jboss.aesh.console.command.CommandException;
|
||||||
|
import org.jboss.aesh.console.command.Command;
|
||||||
|
import org.jboss.aesh.console.command.CommandResult;
|
||||||
|
import org.jboss.aesh.console.command.invocation.CommandInvocation;
|
||||||
|
import org.keycloak.client.registration.cli.util.OsUtil;
|
||||||
|
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
|
||||||
|
@GroupCommandDefinition(name = "config", description = "COMMAND [ARGUMENTS]", groupCommands = {ConfigCredentialsCmd.class} )
|
||||||
|
public class ConfigCmd extends AbstractAuthOptionsCmd implements Command {
|
||||||
|
|
||||||
|
@Arguments
|
||||||
|
protected List<String> args;
|
||||||
|
|
||||||
|
|
||||||
|
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
|
||||||
|
try {
|
||||||
|
if (args.size() == 0) {
|
||||||
|
throw new RuntimeException("Sub-command required by '" + OsUtil.CMD + " config' - one of: 'credentials', 'truststore', 'initial-token', 'registration-token'");
|
||||||
|
}
|
||||||
|
|
||||||
|
String cmd = args.get(0);
|
||||||
|
switch (cmd) {
|
||||||
|
case "credentials": {
|
||||||
|
return new ConfigCredentialsCmd(this).execute(commandInvocation);
|
||||||
|
}
|
||||||
|
case "truststore": {
|
||||||
|
return new ConfigTruststoreCmd(this).execute(commandInvocation);
|
||||||
|
}
|
||||||
|
case "initial-token": {
|
||||||
|
return new ConfigInitialTokenCmd(this).execute(commandInvocation);
|
||||||
|
}
|
||||||
|
case "registration-token": {
|
||||||
|
return new ConfigRegistrationTokenCmd(this).execute(commandInvocation);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new RuntimeException("Unknown sub-command: " + cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
commandInvocation.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String usage() {
|
||||||
|
StringWriter sb = new StringWriter();
|
||||||
|
PrintWriter out = new PrintWriter(sb);
|
||||||
|
out.println("Usage: " + OsUtil.CMD + " config SUB_COMMAND [ARGUMENTS]");
|
||||||
|
out.println();
|
||||||
|
out.println("Where SUB_COMMAND is one of: 'credentials', 'truststore', 'initial-token', 'registration-token'");
|
||||||
|
out.println();
|
||||||
|
out.println();
|
||||||
|
out.println("Use '" + OsUtil.CMD + " help config SUB_COMMAND' for more info.");
|
||||||
|
out.println("Use '" + OsUtil.CMD + " help' for general information and a list of commands.");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,246 @@
|
||||||
|
package org.keycloak.client.registration.cli.commands;
|
||||||
|
|
||||||
|
import org.jboss.aesh.cl.CommandDefinition;
|
||||||
|
import org.jboss.aesh.console.command.Command;
|
||||||
|
import org.jboss.aesh.console.command.CommandException;
|
||||||
|
import org.jboss.aesh.console.command.CommandResult;
|
||||||
|
import org.jboss.aesh.console.command.invocation.CommandInvocation;
|
||||||
|
import org.keycloak.client.registration.cli.config.ConfigData;
|
||||||
|
import org.keycloak.client.registration.cli.config.RealmConfigData;
|
||||||
|
import org.keycloak.client.registration.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.registration.cli.util.AuthUtil.getAuthTokens;
|
||||||
|
import static org.keycloak.client.registration.cli.util.AuthUtil.getAuthTokensByJWT;
|
||||||
|
import static org.keycloak.client.registration.cli.util.AuthUtil.getAuthTokensBySecret;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.getHandler;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveTokens;
|
||||||
|
import static org.keycloak.client.registration.cli.util.IoUtil.printErr;
|
||||||
|
import static org.keycloak.client.registration.cli.util.IoUtil.readSecret;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
@CommandDefinition(name = "credentials", description = "--server SERVER_URL --realm REALM [ARGUMENTS]")
|
||||||
|
public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd implements Command {
|
||||||
|
|
||||||
|
private int sigLifetime = 600;
|
||||||
|
|
||||||
|
public ConfigCredentialsCmd() {}
|
||||||
|
|
||||||
|
public ConfigCredentialsCmd(AbstractAuthOptionsCmd parent) {
|
||||||
|
init(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
processGlobalOptions();
|
||||||
|
|
||||||
|
return process(commandInvocation);
|
||||||
|
} finally {
|
||||||
|
commandInvocation.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
|
||||||
|
|
||||||
|
// check server
|
||||||
|
if (server == null) {
|
||||||
|
throw new RuntimeException("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 RuntimeException("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 RuntimeException("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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String usage() {
|
||||||
|
StringWriter sb = new StringWriter();
|
||||||
|
PrintWriter out = new PrintWriter(sb);
|
||||||
|
out.println("Usage: " + CMD + " config credentials --server SERVER_URL --realm REALM [ARGUMENTS]");
|
||||||
|
out.println(" " + 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. That is enough to establish unauthenticated session.");
|
||||||
|
out.println("If --client is not provided it defaults to 'admin-cli'. The authantication options / requirements depend on how this client is configured.");
|
||||||
|
out.println();
|
||||||
|
out.println("If you have an account configured with the rights to manage clients then you can use username, and password to authenticate.");
|
||||||
|
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,169 @@
|
||||||
|
package org.keycloak.client.registration.cli.commands;
|
||||||
|
|
||||||
|
import org.jboss.aesh.cl.CommandDefinition;
|
||||||
|
import org.jboss.aesh.console.command.Command;
|
||||||
|
import org.jboss.aesh.console.command.CommandException;
|
||||||
|
import org.jboss.aesh.console.command.CommandResult;
|
||||||
|
import org.jboss.aesh.console.command.invocation.CommandInvocation;
|
||||||
|
import org.keycloak.client.registration.cli.config.RealmConfigData;
|
||||||
|
import org.keycloak.client.registration.cli.util.IoUtil;
|
||||||
|
import org.keycloak.client.registration.cli.util.ParseUtil;
|
||||||
|
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
|
||||||
|
import static org.keycloak.client.registration.cli.util.IoUtil.warnfOut;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
@CommandDefinition(name = "initial-token", description = "[--server SERVER] --realm REALM [--delete | TOKEN] [ARGUMENTS]")
|
||||||
|
public class ConfigInitialTokenCmd extends AbstractAuthOptionsCmd implements Command {
|
||||||
|
|
||||||
|
private ConfigCmd parent;
|
||||||
|
|
||||||
|
private boolean delete;
|
||||||
|
private boolean keepDomain;
|
||||||
|
|
||||||
|
public ConfigInitialTokenCmd() {}
|
||||||
|
|
||||||
|
public ConfigInitialTokenCmd(ConfigCmd parent) {
|
||||||
|
this.parent = parent;
|
||||||
|
init(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
|
||||||
|
try {
|
||||||
|
return process(commandInvocation);
|
||||||
|
} finally {
|
||||||
|
commandInvocation.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
|
||||||
|
|
||||||
|
List<String> args = new ArrayList<>();
|
||||||
|
|
||||||
|
Iterator<String> it = parent.args.iterator();
|
||||||
|
// skip the first argument 'initial-token'
|
||||||
|
it.next();
|
||||||
|
|
||||||
|
while (it.hasNext()) {
|
||||||
|
String arg = it.next();
|
||||||
|
switch (arg) {
|
||||||
|
case "-d":
|
||||||
|
case "--delete": {
|
||||||
|
delete = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "-k":
|
||||||
|
case "--keep-domain": {
|
||||||
|
keepDomain = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
args.add(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.size() > 1) {
|
||||||
|
throw new RuntimeException("Invalid option: " + args.get(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
String token = args.size() == 1 ? args.get(0) : null;
|
||||||
|
|
||||||
|
if (realm == null) {
|
||||||
|
throw new RuntimeException("Realm not specified");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token != null && token.startsWith("-")) {
|
||||||
|
warnfOut(ParseUtil.TOKEN_OPTION_WARN, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkUnsupportedOptions(
|
||||||
|
"--client", clientId,
|
||||||
|
"--user", user,
|
||||||
|
"--password", password,
|
||||||
|
"--secret", secret,
|
||||||
|
"--keystore", keystore,
|
||||||
|
"--storepass", storePass,
|
||||||
|
"--keypass", keyPass,
|
||||||
|
"--alias", alias,
|
||||||
|
"--truststore", trustStore,
|
||||||
|
"--trustpass", keyPass);
|
||||||
|
|
||||||
|
|
||||||
|
if (!delete && token == null) {
|
||||||
|
token = IoUtil.readSecret("Enter Initial Access Token: ", commandInvocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// now update the config
|
||||||
|
processGlobalOptions();
|
||||||
|
|
||||||
|
String initialToken = token;
|
||||||
|
saveMergeConfig(config -> {
|
||||||
|
if (!keepDomain && !delete) {
|
||||||
|
config.setServerUrl(server);
|
||||||
|
config.setRealm(realm);
|
||||||
|
}
|
||||||
|
if (delete) {
|
||||||
|
RealmConfigData rdata = config.getRealmConfigData(server, realm);
|
||||||
|
if (rdata != null) {
|
||||||
|
rdata.setInitialToken(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RealmConfigData rdata = config.ensureRealmConfigData(server, realm);
|
||||||
|
rdata.setInitialToken(initialToken);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return CommandResult.SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String usage() {
|
||||||
|
StringWriter sb = new StringWriter();
|
||||||
|
PrintWriter out = new PrintWriter(sb);
|
||||||
|
out.println("Usage: " + CMD + " config initial-token --server SERVER --realm REALM [--delete | TOKEN] [ARGUMENTS]");
|
||||||
|
out.println();
|
||||||
|
out.println("Command to configure an initial access token to be used with '" + CMD + " create' command. Even if an ");
|
||||||
|
out.println("authenticated session exists as a result of '" + CMD + " config credentials' its access token will not");
|
||||||
|
out.println("be used - initial access token will be used instead. By default, current server, and realm will");
|
||||||
|
out.println("be set to the new values thus subsequent commands will use these values as default.");
|
||||||
|
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(" --server SERVER Server endpoint url (e.g. 'http://localhost:8080/auth')");
|
||||||
|
out.println(" --realm REALM Realm name to use");
|
||||||
|
out.println(" -k, --keep-domain Don't overwrite default server and realm");
|
||||||
|
out.println(" -d, --delete Indicates that initial access token should be removed");
|
||||||
|
out.println(" TOKEN Initial access token (prompted for if not specified, unless -d is used)");
|
||||||
|
out.println();
|
||||||
|
out.println();
|
||||||
|
out.println("Examples:");
|
||||||
|
out.println();
|
||||||
|
out.println("Specify initial access token for server, and realm. Token is passed via env variable:");
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " config initial-token --server http://localhost:9080/auth --realm master " + OS_ARCH.envVar("TOKEN"));
|
||||||
|
out.println();
|
||||||
|
out.println("Remove initial access token:");
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " config initial-token --server http://localhost:9080/auth --realm master --delete");
|
||||||
|
out.println();
|
||||||
|
out.println();
|
||||||
|
out.println("Use '" + CMD + " help' for general information and a list of commands");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,158 @@
|
||||||
|
package org.keycloak.client.registration.cli.commands;
|
||||||
|
|
||||||
|
import org.jboss.aesh.cl.CommandDefinition;
|
||||||
|
import org.jboss.aesh.console.command.Command;
|
||||||
|
import org.jboss.aesh.console.command.CommandException;
|
||||||
|
import org.jboss.aesh.console.command.CommandResult;
|
||||||
|
import org.jboss.aesh.console.command.invocation.CommandInvocation;
|
||||||
|
import org.keycloak.client.registration.cli.config.RealmConfigData;
|
||||||
|
import org.keycloak.client.registration.cli.util.IoUtil;
|
||||||
|
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
@CommandDefinition(name = "registration-token", description = "[--server SERVER] --realm REALM --client CLIENT [--delete | TOKEN] [ARGUMENTS]")
|
||||||
|
public class ConfigRegistrationTokenCmd extends AbstractAuthOptionsCmd implements Command {
|
||||||
|
|
||||||
|
private ConfigCmd parent;
|
||||||
|
|
||||||
|
private boolean delete;
|
||||||
|
|
||||||
|
public ConfigRegistrationTokenCmd() {}
|
||||||
|
|
||||||
|
public ConfigRegistrationTokenCmd(ConfigCmd parent) {
|
||||||
|
this.parent = parent;
|
||||||
|
init(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
|
||||||
|
try {
|
||||||
|
return process(commandInvocation);
|
||||||
|
} finally {
|
||||||
|
commandInvocation.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
|
||||||
|
|
||||||
|
List<String> args = new ArrayList<>();
|
||||||
|
|
||||||
|
Iterator<String> it = parent.args.iterator();
|
||||||
|
// skip the first argument 'initial-token'
|
||||||
|
it.next();
|
||||||
|
|
||||||
|
while (it.hasNext()) {
|
||||||
|
String arg = it.next();
|
||||||
|
switch (arg) {
|
||||||
|
case "-d":
|
||||||
|
case "--delete": {
|
||||||
|
delete = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
args.add(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.size() > 1) {
|
||||||
|
throw new RuntimeException("Invalid option: " + args.get(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
String token = args.size() == 1 ? args.get(0) : null;
|
||||||
|
|
||||||
|
if (server == null) {
|
||||||
|
throw new RuntimeException("Required option not specified: --server");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (realm == null) {
|
||||||
|
throw new RuntimeException("Required option not specified: --realm");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientId == null) {
|
||||||
|
throw new RuntimeException("Required option not specified: --client");
|
||||||
|
}
|
||||||
|
|
||||||
|
checkUnsupportedOptions(
|
||||||
|
"--user", user,
|
||||||
|
"--password", password,
|
||||||
|
"--secret", secret,
|
||||||
|
"--keystore", keystore,
|
||||||
|
"--storepass", storePass,
|
||||||
|
"--keypass", keyPass,
|
||||||
|
"--alias", alias,
|
||||||
|
"--truststore", trustStore,
|
||||||
|
"--trustpass", keyPass);
|
||||||
|
|
||||||
|
|
||||||
|
if (!delete && token == null) {
|
||||||
|
token = IoUtil.readSecret("Enter Registration Access Token: ", commandInvocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// now update the config
|
||||||
|
processGlobalOptions();
|
||||||
|
|
||||||
|
String registrationToken = token;
|
||||||
|
saveMergeConfig(config -> {
|
||||||
|
RealmConfigData rdata = config.getRealmConfigData(server, realm);
|
||||||
|
if (delete) {
|
||||||
|
if (rdata != null) {
|
||||||
|
rdata.getClients().remove(clientId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
config.ensureRealmConfigData(server, realm).getClients().put(clientId, registrationToken);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return CommandResult.SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String usage() {
|
||||||
|
StringWriter sb = new StringWriter();
|
||||||
|
PrintWriter out = new PrintWriter(sb);
|
||||||
|
out.println("Usage: " + CMD + " config registration-token --server SERVER --realm REALM --client CLIENT [--delete | TOKEN] [ARGUMENTS]");
|
||||||
|
out.println();
|
||||||
|
out.println("Command to configure a registration access token to be used with 'kcreg get / update / delete' commands. Even if an ");
|
||||||
|
out.println("authenticated session exists as a result of '" + CMD + " config credentials' its access token will not be used - registration");
|
||||||
|
out.println("access token will be used instead.");
|
||||||
|
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(" --server SERVER Server endpoint url (e.g. 'http://localhost:8080/auth')");
|
||||||
|
out.println(" --realm REALM Realm name to use");
|
||||||
|
out.println(" --client CLIENT ClientId of client whose token to set");
|
||||||
|
out.println(" -d, --delete Indicates that registration access token should be removed");
|
||||||
|
out.println(" TOKEN Registration access token (prompted for if not specified, unless -d is used)");
|
||||||
|
out.println();
|
||||||
|
out.println();
|
||||||
|
out.println("Examples:");
|
||||||
|
out.println();
|
||||||
|
out.println("Specify registration access token for server, and realm. Token is passed via env variable:");
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " config registration-token --server http://localhost:9080/auth --realm master --client my_client " + OS_ARCH.envVar("TOKEN"));
|
||||||
|
out.println();
|
||||||
|
out.println("Remove registration access token:");
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " config registration-token --server http://localhost:9080/auth --realm master --client my_client --delete");
|
||||||
|
out.println();
|
||||||
|
out.println();
|
||||||
|
out.println("Use '" + CMD + " help' for general information and a list of commands");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,168 @@
|
||||||
|
package org.keycloak.client.registration.cli.commands;
|
||||||
|
|
||||||
|
import org.jboss.aesh.cl.CommandDefinition;
|
||||||
|
import org.jboss.aesh.console.command.Command;
|
||||||
|
import org.jboss.aesh.console.command.CommandException;
|
||||||
|
import org.jboss.aesh.console.command.CommandResult;
|
||||||
|
import org.jboss.aesh.console.command.invocation.CommandInvocation;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
|
||||||
|
import static org.keycloak.client.registration.cli.util.IoUtil.readSecret;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
@CommandDefinition(name = "truststore", description = "PATH [ARGUMENTS]")
|
||||||
|
public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd implements Command {
|
||||||
|
|
||||||
|
private ConfigCmd parent;
|
||||||
|
|
||||||
|
private boolean delete;
|
||||||
|
|
||||||
|
public ConfigTruststoreCmd() {}
|
||||||
|
|
||||||
|
public ConfigTruststoreCmd(ConfigCmd parent) {
|
||||||
|
this.parent = parent;
|
||||||
|
init(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
|
||||||
|
try {
|
||||||
|
return process(commandInvocation);
|
||||||
|
} finally {
|
||||||
|
commandInvocation.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
|
||||||
|
|
||||||
|
List<String> args = new ArrayList<>();
|
||||||
|
|
||||||
|
Iterator<String> it = parent.args.iterator();
|
||||||
|
// skip the first argument 'truststore'
|
||||||
|
it.next();
|
||||||
|
|
||||||
|
while (it.hasNext()) {
|
||||||
|
String arg = it.next();
|
||||||
|
switch (arg) {
|
||||||
|
case "-d":
|
||||||
|
case "--delete": {
|
||||||
|
delete = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
args.add(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.size() > 1) {
|
||||||
|
throw new RuntimeException("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 RuntimeException("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 RuntimeException("Option --delete is mutually exclusive with specifying a TRUSTSTORE");
|
||||||
|
}
|
||||||
|
if (trustPass != null) {
|
||||||
|
throw new RuntimeException("Options --trustpass and --delete are mutually exclusive");
|
||||||
|
}
|
||||||
|
store = null;
|
||||||
|
pass = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveMergeConfig(config -> {
|
||||||
|
config.setTruststore(store);
|
||||||
|
config.setTrustpass(pass);
|
||||||
|
});
|
||||||
|
|
||||||
|
return CommandResult.SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static String usage() {
|
||||||
|
StringWriter sb = new StringWriter();
|
||||||
|
PrintWriter out = new PrintWriter(sb);
|
||||||
|
out.println("Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWOD] [ARGUMENTS]");
|
||||||
|
out.println();
|
||||||
|
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 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,295 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.client.registration.cli.commands;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
|
||||||
|
import org.jboss.aesh.cl.Arguments;
|
||||||
|
import org.jboss.aesh.cl.CommandDefinition;
|
||||||
|
import org.jboss.aesh.cl.Option;
|
||||||
|
import org.jboss.aesh.console.command.Command;
|
||||||
|
import org.jboss.aesh.console.command.CommandException;
|
||||||
|
import org.jboss.aesh.console.command.CommandResult;
|
||||||
|
import org.jboss.aesh.console.command.invocation.CommandInvocation;
|
||||||
|
import org.keycloak.client.registration.cli.aesh.EndpointTypeConverter;
|
||||||
|
import org.keycloak.client.registration.cli.common.AttributeOperation;
|
||||||
|
import org.keycloak.client.registration.cli.config.ConfigData;
|
||||||
|
import org.keycloak.client.registration.cli.common.CmdStdinContext;
|
||||||
|
import org.keycloak.client.registration.cli.common.EndpointType;
|
||||||
|
import org.keycloak.client.registration.cli.util.HttpUtil;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.keycloak.client.registration.cli.common.AttributeOperation.Type.SET;
|
||||||
|
import static org.keycloak.client.registration.cli.common.EndpointType.DEFAULT;
|
||||||
|
import static org.keycloak.client.registration.cli.common.EndpointType.OIDC;
|
||||||
|
import static org.keycloak.client.registration.cli.common.EndpointType.SAML2;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.credentialsAvailable;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
|
||||||
|
import static org.keycloak.client.registration.cli.util.HttpUtil.getExpectedContentType;
|
||||||
|
import static org.keycloak.client.registration.cli.util.IoUtil.printErr;
|
||||||
|
import static org.keycloak.client.registration.cli.util.IoUtil.readFully;
|
||||||
|
import static org.keycloak.client.registration.cli.util.IoUtil.readSecret;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.OS_ARCH;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ParseUtil.mergeAttributes;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ParseUtil.parseFileOrStdin;
|
||||||
|
import static org.keycloak.client.registration.cli.util.AuthUtil.ensureToken;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.setRegistrationToken;
|
||||||
|
import static org.keycloak.client.registration.cli.util.HttpUtil.doPost;
|
||||||
|
import static org.keycloak.client.registration.cli.util.IoUtil.printOut;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ParseUtil.parseKeyVal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
@CommandDefinition(name = "create", description = "[ARGUMENTS]")
|
||||||
|
public class CreateCmd extends AbstractAuthOptionsCmd implements Command {
|
||||||
|
|
||||||
|
@Option(shortName = 'i', name = "clientId", description = "After creation only print clientId to standard output", hasValue = false)
|
||||||
|
protected boolean returnClientId = false;
|
||||||
|
|
||||||
|
@Option(shortName = 'e', name = "endpoint", description = "Endpoint type / document format to use - one of: 'default', 'oidc', 'saml2'",
|
||||||
|
hasValue = true, converter = EndpointTypeConverter.class)
|
||||||
|
protected EndpointType regType;
|
||||||
|
|
||||||
|
@Option(shortName = 'f', name = "file", description = "Read object from file or standard input if FILENAME is set to '-'", hasValue = true)
|
||||||
|
protected String file;
|
||||||
|
|
||||||
|
@Option(shortName = 'o', name = "output", description = "After creation output the new client configuration to standard output", hasValue = false)
|
||||||
|
protected boolean outputClient = false;
|
||||||
|
|
||||||
|
@Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false)
|
||||||
|
protected boolean compressed = false;
|
||||||
|
|
||||||
|
//@OptionGroup(shortName = 's', name = "set", description = "Set attribute to the specified value")
|
||||||
|
//Map<String, String> attributes = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
@Arguments
|
||||||
|
protected List<String> args;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
|
||||||
|
|
||||||
|
List<AttributeOperation> attrs = new LinkedList<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
processGlobalOptions();
|
||||||
|
|
||||||
|
if (args != null) {
|
||||||
|
Iterator<String> it = args.iterator();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
String option = it.next();
|
||||||
|
switch (option) {
|
||||||
|
case "-s":
|
||||||
|
case "--set": {
|
||||||
|
if (!it.hasNext()) {
|
||||||
|
throw new RuntimeException("Option " + option + " requires a value");
|
||||||
|
}
|
||||||
|
String[] keyVal = parseKeyVal(it.next());
|
||||||
|
attrs.add(new AttributeOperation(SET, keyVal[0], keyVal[1]));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new RuntimeException("Unsupported option: " + option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file == null && attrs.size() == 0) {
|
||||||
|
throw new RuntimeException("No file nor attribute values specified");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputClient && returnClientId) {
|
||||||
|
throw new RuntimeException("Options -o and -i can't be used together");
|
||||||
|
}
|
||||||
|
|
||||||
|
// if --token is specified read it
|
||||||
|
if ("-".equals(token)) {
|
||||||
|
token = readSecret("Enter Initial Access Token: ", commandInvocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
CmdStdinContext ctx = new CmdStdinContext();
|
||||||
|
if (file != null) {
|
||||||
|
ctx = parseFileOrStdin(file, regType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.getEndpointType() == null) {
|
||||||
|
regType = regType != null ? regType : DEFAULT;
|
||||||
|
ctx.setEndpointType(regType);
|
||||||
|
} else if (regType != null && ctx.getEndpointType() != regType) {
|
||||||
|
throw new RuntimeException("Requested endpoint type not compatible with detected configuration format: " + ctx.getEndpointType());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attrs.size() > 0) {
|
||||||
|
ctx = mergeAttributes(ctx, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
String contentType = getExpectedContentType(ctx.getEndpointType());
|
||||||
|
|
||||||
|
ConfigData config = loadConfig();
|
||||||
|
config = copyWithServerInfo(config);
|
||||||
|
|
||||||
|
if (token == null) {
|
||||||
|
// if initial token is not set, try use the one from configuration
|
||||||
|
token = config.sessionRealmConfigData().getInitialToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupTruststore(config, commandInvocation);
|
||||||
|
|
||||||
|
String auth = token;
|
||||||
|
if (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 = config.getRealm();
|
||||||
|
|
||||||
|
InputStream response = doPost(server + "/realms/" + realm + "/clients-registrations/" + ctx.getEndpointType().getEndpoint(),
|
||||||
|
contentType, HttpUtil.APPLICATION_JSON, ctx.getContent(), auth);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (ctx.getEndpointType() == DEFAULT || ctx.getEndpointType() == SAML2) {
|
||||||
|
ClientRepresentation client = JsonSerialization.readValue(response, ClientRepresentation.class);
|
||||||
|
outputResult(client.getClientId(), client);
|
||||||
|
|
||||||
|
saveMergeConfig(cfg -> {
|
||||||
|
setRegistrationToken(cfg.ensureRealmConfigData(server, realm), client.getClientId(), client.getRegistrationAccessToken());
|
||||||
|
});
|
||||||
|
} else if (ctx.getEndpointType() == OIDC) {
|
||||||
|
OIDCClientRepresentation client = JsonSerialization.readValue(response, OIDCClientRepresentation.class);
|
||||||
|
outputResult(client.getClientId(), client);
|
||||||
|
|
||||||
|
saveMergeConfig(cfg -> {
|
||||||
|
setRegistrationToken(cfg.ensureRealmConfigData(server, realm), client.getClientId(), client.getRegistrationAccessToken());
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
printOut("Response from server: " + readFully(response));
|
||||||
|
}
|
||||||
|
} catch (UnrecognizedPropertyException e) {
|
||||||
|
throw new RuntimeException("Failed to process HTTP reponse - " + e.getMessage(), e);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to process HTTP response", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CommandResult.SUCCESS;
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
commandInvocation.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void outputResult(String clientId, Object result) throws IOException {
|
||||||
|
if (returnClientId) {
|
||||||
|
printOut(clientId);
|
||||||
|
} else if (outputClient) {
|
||||||
|
if (compressed) {
|
||||||
|
printOut(JsonSerialization.writeValueAsString(result));
|
||||||
|
} else {
|
||||||
|
printOut(JsonSerialization.writeValueAsPrettyString(result));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printErr("Registered new client with client_id '" + clientId + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String usage() {
|
||||||
|
StringWriter sb = new StringWriter();
|
||||||
|
PrintWriter out = new PrintWriter(sb);
|
||||||
|
out.println("Usage: " + CMD + " create [ARGUMENTS]");
|
||||||
|
out.println();
|
||||||
|
out.println("Command to create new client configurations on the server. If Initial Access Token is specified (-t TOKEN)");
|
||||||
|
out.println("or has previously been set for the server, and realm in the configuration ('" + CMD + " config initial-token'),");
|
||||||
|
out.println("then that will be used, otherwise session access / refresh tokens will be used.");
|
||||||
|
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(" -t, --token TOKEN Use the specified Initial Access Token for authorization or read it from standard input ");
|
||||||
|
out.println(" if '-' is specified. This overrides any token set by '" + CMD + " config initial-token'.");
|
||||||
|
out.println(" If not specified, session credentials are used - see: CREDENTIALS OPTIONS.");
|
||||||
|
out.println(" -e, --endpoint TYPE Endpoint type / document format to use - one of: 'default', 'oidc', 'saml2'.");
|
||||||
|
out.println(" If not specified, the format is deduced from input file or falls back to 'default'.");
|
||||||
|
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(" -o, --output After creation output the new client configuration to standard output");
|
||||||
|
out.println(" -c, --compressed Don't pretty print the output");
|
||||||
|
out.println(" -i, --clientId After creation only print clientId to standard output");
|
||||||
|
out.println();
|
||||||
|
out.println("Examples:");
|
||||||
|
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 -f -");
|
||||||
|
} else {
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " create -f - << EOF");
|
||||||
|
out.println(" {");
|
||||||
|
out.println(" \"clientId\": \"my_client\"");
|
||||||
|
out.println(" }");
|
||||||
|
out.println(" EOF");
|
||||||
|
}
|
||||||
|
out.println();
|
||||||
|
out.println("Since we didn't specify an endpoint type it will be deduced from configuration format.");
|
||||||
|
out.println("Supported formats include Keycloak default format, OIDC format, and SAML SP Metadata.");
|
||||||
|
out.println();
|
||||||
|
out.println("Creating a client using file as a template, and overriding some attributes:");
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " create -f my_client.json -s clientId=my_client2 -s 'redirectUris=[\"http://localhost:8980/myapp/*\"]'");
|
||||||
|
out.println();
|
||||||
|
out.println("Creating a client using an Initial Access Token - you'll be prompted for a token:");
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " create -s clientId=my_client2 -s 'redirectUris=[\"http://localhost:8980/myapp/*\"]' -t -");
|
||||||
|
out.println();
|
||||||
|
out.println("Creating a client using 'oidc' endpoint. Without setting endpoint type here it would be 'default':");
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " create -e oidc -s 'redirect_uris=[\"http://localhost:8980/myapp/*\"]'");
|
||||||
|
out.println();
|
||||||
|
out.println("Creating a client using 'saml2' endpoint. In this case setting endpoint type is redundant since it is deduced ");
|
||||||
|
out.println("from file content:");
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " create -e saml2 -f saml-sp-metadata.xml");
|
||||||
|
out.println();
|
||||||
|
out.println();
|
||||||
|
out.println("Use '" + CMD + " help' for general information and a list of commands");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,145 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.client.registration.cli.commands;
|
||||||
|
|
||||||
|
import org.jboss.aesh.cl.Arguments;
|
||||||
|
import org.jboss.aesh.cl.CommandDefinition;
|
||||||
|
import org.jboss.aesh.console.command.CommandException;
|
||||||
|
import org.jboss.aesh.console.command.CommandResult;
|
||||||
|
import org.jboss.aesh.console.command.invocation.CommandInvocation;
|
||||||
|
import org.keycloak.client.registration.cli.config.ConfigData;
|
||||||
|
import org.keycloak.client.registration.cli.util.ParseUtil;
|
||||||
|
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.keycloak.client.registration.cli.util.AuthUtil.ensureToken;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.credentialsAvailable;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.getRegistrationToken;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
|
||||||
|
import static org.keycloak.client.registration.cli.util.HttpUtil.doDelete;
|
||||||
|
import static org.keycloak.client.registration.cli.util.HttpUtil.urlencode;
|
||||||
|
import static org.keycloak.client.registration.cli.util.IoUtil.warnfErr;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
@CommandDefinition(name = "delete", description = "CLIENT_ID [GLOBAL_OPTIONS]")
|
||||||
|
public class DeleteCmd extends AbstractAuthOptionsCmd {
|
||||||
|
|
||||||
|
@Arguments
|
||||||
|
private List<String> args = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
|
||||||
|
try {
|
||||||
|
processGlobalOptions();
|
||||||
|
|
||||||
|
if (args.isEmpty()) {
|
||||||
|
throw new RuntimeException("CLIENT_ID not specified");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.size() > 1) {
|
||||||
|
throw new RuntimeException("Invalid option: " + args.get(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
String clientId = args.get(0);
|
||||||
|
|
||||||
|
if (clientId.startsWith("-")) {
|
||||||
|
warnfErr(ParseUtil.CLIENTID_OPTION_WARN, clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
String regType = "default";
|
||||||
|
|
||||||
|
ConfigData config = loadConfig();
|
||||||
|
config = copyWithServerInfo(config);
|
||||||
|
|
||||||
|
if (token == null) {
|
||||||
|
// if registration access token is not set via -t, try use the one from configuration
|
||||||
|
token = getRegistrationToken(config.sessionRealmConfigData(), clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupTruststore(config, commandInvocation);
|
||||||
|
|
||||||
|
String auth = token;
|
||||||
|
if (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 = config.getRealm();
|
||||||
|
|
||||||
|
doDelete(server + "/realms/" + realm + "/clients-registrations/" + regType + "/" + urlencode(clientId), auth);
|
||||||
|
|
||||||
|
saveMergeConfig(cfg -> {
|
||||||
|
cfg.ensureRealmConfigData(server, realm).getClients().remove(clientId);
|
||||||
|
});
|
||||||
|
return CommandResult.SUCCESS;
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
commandInvocation.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String usage() {
|
||||||
|
StringWriter sb = new StringWriter();
|
||||||
|
PrintWriter out = new PrintWriter(sb);
|
||||||
|
out.println("Usage: " + CMD + " delete CLIENT [ARGUMENTS]");
|
||||||
|
out.println();
|
||||||
|
out.println("Command to delete a specific client configuration. If registration access token is specified or is available in ");
|
||||||
|
out.println("configuration file, then it is used. Otherwise, current active session is used.");
|
||||||
|
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(" --token TOKEN Registration access token to use");
|
||||||
|
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(" CLIENT ClientId of the client to delete");
|
||||||
|
out.println();
|
||||||
|
out.println("Examples:");
|
||||||
|
out.println();
|
||||||
|
out.println("Delete a client:");
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " delete my_client");
|
||||||
|
out.println();
|
||||||
|
out.println();
|
||||||
|
out.println("Use '" + CMD + " help' for general information and a list of commands");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,215 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.client.registration.cli.commands;
|
||||||
|
|
||||||
|
import org.jboss.aesh.cl.Arguments;
|
||||||
|
import org.jboss.aesh.cl.CommandDefinition;
|
||||||
|
import org.jboss.aesh.cl.Option;
|
||||||
|
import org.jboss.aesh.console.command.CommandException;
|
||||||
|
import org.jboss.aesh.console.command.CommandResult;
|
||||||
|
import org.jboss.aesh.console.command.invocation.CommandInvocation;
|
||||||
|
import org.keycloak.client.registration.cli.config.ConfigData;
|
||||||
|
import org.keycloak.client.registration.cli.common.EndpointType;
|
||||||
|
import org.keycloak.client.registration.cli.util.ParseUtil;
|
||||||
|
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.keycloak.client.registration.cli.util.AuthUtil.ensureToken;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.credentialsAvailable;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.getRegistrationToken;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.setRegistrationToken;
|
||||||
|
import static org.keycloak.client.registration.cli.util.HttpUtil.APPLICATION_JSON;
|
||||||
|
import static org.keycloak.client.registration.cli.util.HttpUtil.doGet;
|
||||||
|
import static org.keycloak.client.registration.cli.util.HttpUtil.urlencode;
|
||||||
|
import static org.keycloak.client.registration.cli.util.IoUtil.warnfErr;
|
||||||
|
import static org.keycloak.client.registration.cli.util.IoUtil.printOut;
|
||||||
|
import static org.keycloak.client.registration.cli.util.IoUtil.readFully;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
@CommandDefinition(name = "get", description = "[ARGUMENTS]")
|
||||||
|
public class GetCmd extends AbstractAuthOptionsCmd {
|
||||||
|
|
||||||
|
@Option(shortName = 'c', name = "compressed", description = "Print full stack trace when exiting with error", hasValue = false)
|
||||||
|
private boolean compressed = false;
|
||||||
|
|
||||||
|
@Option(shortName = 'e', name = "endpoint", description = "Endpoint type to use", hasValue = true)
|
||||||
|
private String endpoint;
|
||||||
|
|
||||||
|
@Arguments
|
||||||
|
private List<String> args = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
|
||||||
|
|
||||||
|
try {
|
||||||
|
processGlobalOptions();
|
||||||
|
|
||||||
|
if (args == null || args.isEmpty()) {
|
||||||
|
throw new RuntimeException("CLIENT not specified");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.size() > 1) {
|
||||||
|
throw new RuntimeException("Invalid option: " + args.get(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
String clientId = args.get(0);
|
||||||
|
EndpointType regType = endpoint != null ? EndpointType.of(endpoint) : EndpointType.DEFAULT;
|
||||||
|
|
||||||
|
|
||||||
|
if (clientId.startsWith("-")) {
|
||||||
|
warnfErr(ParseUtil.CLIENTID_OPTION_WARN, clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigData config = loadConfig();
|
||||||
|
config = copyWithServerInfo(config);
|
||||||
|
|
||||||
|
if (token == null) {
|
||||||
|
// if registration access token is not set via -t, try use the one from configuration
|
||||||
|
token = getRegistrationToken(config.sessionRealmConfigData(), clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupTruststore(config, commandInvocation);
|
||||||
|
|
||||||
|
String auth = token;
|
||||||
|
if (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 = config.getRealm();
|
||||||
|
|
||||||
|
InputStream response = doGet(server + "/realms/" + realm + "/clients-registrations/" + regType.getEndpoint() + "/" + urlencode(clientId),
|
||||||
|
APPLICATION_JSON, auth);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String json = readFully(response);
|
||||||
|
Object result = null;
|
||||||
|
|
||||||
|
switch (regType) {
|
||||||
|
case DEFAULT: {
|
||||||
|
ClientRepresentation client = JsonSerialization.readValue(json, ClientRepresentation.class);
|
||||||
|
result = client;
|
||||||
|
|
||||||
|
saveMergeConfig(cfg -> {
|
||||||
|
setRegistrationToken(cfg.ensureRealmConfigData(server, realm), client.getClientId(), client.getRegistrationAccessToken());
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OIDC: {
|
||||||
|
OIDCClientRepresentation client = JsonSerialization.readValue(json, OIDCClientRepresentation.class);
|
||||||
|
result = client;
|
||||||
|
|
||||||
|
saveMergeConfig(cfg -> {
|
||||||
|
setRegistrationToken(cfg.ensureRealmConfigData(server, realm), client.getClientId(), client.getRegistrationAccessToken());
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case INSTALL: {
|
||||||
|
result = JsonSerialization.readValue(json, AdapterConfig.class);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SAML2: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new RuntimeException("Unexpected type: " + regType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!compressed && result != null) {
|
||||||
|
json = JsonSerialization.writeValueAsPrettyString(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
printOut(json);
|
||||||
|
|
||||||
|
//} catch (UnrecognizedPropertyException e) {
|
||||||
|
// throw new RuntimeException("Failed to parse returned JSON - " + e.getMessage(), e);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to process HTTP response", e);
|
||||||
|
}
|
||||||
|
return CommandResult.SUCCESS;
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
commandInvocation.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String usage() {
|
||||||
|
StringWriter sb = new StringWriter();
|
||||||
|
PrintWriter out = new PrintWriter(sb);
|
||||||
|
out.println("Usage: " + CMD + " get CLIENT [ARGUMENTS]");
|
||||||
|
out.println();
|
||||||
|
out.println("Command to retrieve a client configuration description for a specified client. If registration access token");
|
||||||
|
out.println("is specified or is available in configuration file, then it is used. Otherwise, current active session is used.");
|
||||||
|
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(" -t, --token TOKEN Registration access token to use");
|
||||||
|
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(" CLIENT ClientId of the client to display");
|
||||||
|
out.println(" -c, --compressed Don't pretty print the output");
|
||||||
|
out.println(" -e, --endpoint TYPE Endpoint type to use - one of: 'default', 'oidc', 'install'");
|
||||||
|
out.println();
|
||||||
|
out.println("Examples:");
|
||||||
|
out.println();
|
||||||
|
out.println("Get configuration in default format:");
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " get my_client");
|
||||||
|
out.println();
|
||||||
|
out.println("Get configuration in OIDC format:");
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " get my_client -e oidc");
|
||||||
|
out.println();
|
||||||
|
out.println("Get adapter configuration for the client:");
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " get my_client -e install");
|
||||||
|
out.println();
|
||||||
|
out.println();
|
||||||
|
out.println("Use '" + CMD + " help' for general information and a list of commands");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
package org.keycloak.client.registration.cli.commands;
|
||||||
|
|
||||||
|
import org.jboss.aesh.cl.Arguments;
|
||||||
|
import org.jboss.aesh.cl.CommandDefinition;
|
||||||
|
import org.jboss.aesh.console.command.Command;
|
||||||
|
import org.jboss.aesh.console.command.CommandException;
|
||||||
|
import org.jboss.aesh.console.command.CommandResult;
|
||||||
|
import org.jboss.aesh.console.command.invocation.CommandInvocation;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.keycloak.client.registration.cli.util.IoUtil.printOut;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
@CommandDefinition(name = "help", description = "This help")
|
||||||
|
public class HelpCmd implements Command {
|
||||||
|
|
||||||
|
@Arguments
|
||||||
|
private List<String> args;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
|
||||||
|
try {
|
||||||
|
if (args == null || args.size() == 0) {
|
||||||
|
printOut(KcRegCmd.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 "initial-token": {
|
||||||
|
printOut(ConfigInitialTokenCmd.usage());
|
||||||
|
break outer;
|
||||||
|
}
|
||||||
|
case "registration-token": {
|
||||||
|
printOut(ConfigRegistrationTokenCmd.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 "attrs": {
|
||||||
|
printOut(AttrsCmd.usage());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "update-token": {
|
||||||
|
printOut(UpdateTokenCmd.usage());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new RuntimeException("Unknown command: " + args.get(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CommandResult.SUCCESS;
|
||||||
|
} finally {
|
||||||
|
commandInvocation.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.client.registration.cli.commands;
|
||||||
|
|
||||||
|
import org.jboss.aesh.cl.GroupCommandDefinition;
|
||||||
|
import org.jboss.aesh.console.command.CommandException;
|
||||||
|
import org.jboss.aesh.console.command.CommandResult;
|
||||||
|
import org.jboss.aesh.console.command.invocation.CommandInvocation;
|
||||||
|
import org.keycloak.client.registration.cli.util.IoUtil;
|
||||||
|
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
|
||||||
|
@GroupCommandDefinition(name = "kcreg", description = "COMMAND [ARGUMENTS]", groupCommands = {
|
||||||
|
HelpCmd.class, ConfigCmd.class, CreateCmd.class, UpdateCmd.class, GetCmd.class, DeleteCmd.class, AttrsCmd.class, UpdateTokenCmd.class} )
|
||||||
|
public class KcRegCmd extends AbstractGlobalOptionsCmd {
|
||||||
|
|
||||||
|
//@Arguments
|
||||||
|
//private List<String> args;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
|
||||||
|
try {
|
||||||
|
IoUtil.printOut(usage());
|
||||||
|
|
||||||
|
return CommandResult.SUCCESS;
|
||||||
|
} finally {
|
||||||
|
commandInvocation.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String usage() {
|
||||||
|
StringWriter sb = new StringWriter();
|
||||||
|
PrintWriter out = new PrintWriter(sb);
|
||||||
|
out.println("Keycloak Client Registration 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 client registration operations the user");
|
||||||
|
out.println("needs proper roles, otherwise attempts to create, update, read, or delete clients will fail.");
|
||||||
|
out.println("Alternatively, the user without the necessary roles can use an Initial Access Token provided by realm");
|
||||||
|
out.println("administrator when creating a new client with 'create' command. For example:");
|
||||||
|
out.println();
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " create -f my_client.json -t -");
|
||||||
|
out.println(" Enter Initial Access Token: ");
|
||||||
|
out.println(" Registered new client with client_id 'my_client'");
|
||||||
|
out.println();
|
||||||
|
out.println("When Initial Access Token is used the server issues a Registration Access Token which is automatically");
|
||||||
|
out.println("handled by " + CMD + ", saved into a local config file, and automatically used for any follow-up operations");
|
||||||
|
out.println("on the same client. For example:");
|
||||||
|
out.println();
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " get my_client");
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " update my_client -s enabled=false");
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " delete my_client");
|
||||||
|
out.println();
|
||||||
|
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(" -c, --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 Register a new client");
|
||||||
|
out.println(" get Get configuration of existing client in Keycloak or OIDC format, or adapter install configuration");
|
||||||
|
out.println(" update Update a client configuration");
|
||||||
|
out.println(" delete Delete a client");
|
||||||
|
out.println(" attrs List available attributes");
|
||||||
|
out.println(" update-token Update Registration Access Token for a client");
|
||||||
|
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,410 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.client.registration.cli.commands;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonParseException;
|
||||||
|
import org.jboss.aesh.cl.Arguments;
|
||||||
|
import org.jboss.aesh.cl.CommandDefinition;
|
||||||
|
import org.jboss.aesh.cl.Option;
|
||||||
|
import org.jboss.aesh.console.command.CommandException;
|
||||||
|
import org.jboss.aesh.console.command.CommandResult;
|
||||||
|
import org.jboss.aesh.console.command.invocation.CommandInvocation;
|
||||||
|
import org.keycloak.client.registration.cli.aesh.EndpointTypeConverter;
|
||||||
|
import org.keycloak.client.registration.cli.common.AttributeOperation;
|
||||||
|
import org.keycloak.client.registration.cli.config.ConfigData;
|
||||||
|
import org.keycloak.client.registration.cli.common.CmdStdinContext;
|
||||||
|
import org.keycloak.client.registration.cli.common.EndpointType;
|
||||||
|
import org.keycloak.client.registration.cli.util.ParseUtil;
|
||||||
|
import org.keycloak.client.registration.cli.util.ReflectionUtil;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.keycloak.client.registration.cli.common.AttributeOperation.Type.DELETE;
|
||||||
|
import static org.keycloak.client.registration.cli.common.AttributeOperation.Type.SET;
|
||||||
|
import static org.keycloak.client.registration.cli.util.AuthUtil.ensureToken;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.credentialsAvailable;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.getRegistrationToken;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.setRegistrationToken;
|
||||||
|
import static org.keycloak.client.registration.cli.common.EndpointType.DEFAULT;
|
||||||
|
import static org.keycloak.client.registration.cli.common.EndpointType.OIDC;
|
||||||
|
import static org.keycloak.client.registration.cli.util.HttpUtil.doGet;
|
||||||
|
import static org.keycloak.client.registration.cli.util.HttpUtil.doPut;
|
||||||
|
import static org.keycloak.client.registration.cli.util.HttpUtil.urlencode;
|
||||||
|
import static org.keycloak.client.registration.cli.util.IoUtil.printOut;
|
||||||
|
import static org.keycloak.client.registration.cli.util.IoUtil.warnfErr;
|
||||||
|
import static org.keycloak.client.registration.cli.util.IoUtil.readFully;
|
||||||
|
import static org.keycloak.client.registration.cli.util.HttpUtil.APPLICATION_JSON;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ParseUtil.mergeAttributes;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ParseUtil.parseFileOrStdin;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ParseUtil.parseKeyVal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
@CommandDefinition(name = "update", description = "CLIENT_ID [ARGUMENTS]")
|
||||||
|
public class UpdateCmd extends AbstractAuthOptionsCmd {
|
||||||
|
|
||||||
|
@Option(shortName = 'e', name = "endpoint", description = "Endpoint type to use - one of: 'default', 'oidc'", hasValue = true, converter = EndpointTypeConverter.class)
|
||||||
|
private EndpointType regType = null;
|
||||||
|
|
||||||
|
//@GroupOption(shortName = 's', name = "set", description = "Set specific attribute to a specified value", hasValue = true)
|
||||||
|
//private List<String> attributes = new ArrayList<>();
|
||||||
|
|
||||||
|
@Option(shortName = 'f', name = "file", description = "Use the file or standard input if '-' is specified", hasValue = true)
|
||||||
|
private String file = null;
|
||||||
|
|
||||||
|
@Option(shortName = 'm', name = "merge", description = "Merge new values with existing configuration on the server", hasValue = false)
|
||||||
|
private boolean mergeMode = true;
|
||||||
|
|
||||||
|
@Option(shortName = 'u', name = "unsafe", description = "Allow updating without registration access token - no optimistic locking", hasValue = false)
|
||||||
|
private boolean allowUnsafe = true;
|
||||||
|
|
||||||
|
@Option(shortName = 'o', name = "output", description = "After update output the new client configuration", hasValue = false)
|
||||||
|
private boolean outputClient = false;
|
||||||
|
|
||||||
|
@Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false)
|
||||||
|
private boolean compressed = false;
|
||||||
|
|
||||||
|
@Arguments
|
||||||
|
private List<String> args;
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
|
||||||
|
|
||||||
|
List<AttributeOperation> attrs = new LinkedList<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
processGlobalOptions();
|
||||||
|
|
||||||
|
String clientId = null;
|
||||||
|
|
||||||
|
if (args != null) {
|
||||||
|
Iterator<String> it = args.iterator();
|
||||||
|
if (!it.hasNext()) {
|
||||||
|
throw new RuntimeException("CLIENT_ID not specified");
|
||||||
|
}
|
||||||
|
|
||||||
|
clientId = it.next();
|
||||||
|
|
||||||
|
if (clientId.startsWith("-")) {
|
||||||
|
warnfErr(ParseUtil.CLIENTID_OPTION_WARN, clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (it.hasNext()) {
|
||||||
|
String option = it.next();
|
||||||
|
switch (option) {
|
||||||
|
case "-s":
|
||||||
|
case "--set": {
|
||||||
|
if (!it.hasNext()) {
|
||||||
|
throw new RuntimeException("Option " + option + " requires a value");
|
||||||
|
}
|
||||||
|
String[] keyVal = parseKeyVal(it.next());
|
||||||
|
attrs.add(new AttributeOperation(SET, keyVal[0], keyVal[1]));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "-d":
|
||||||
|
case "--delete": {
|
||||||
|
attrs.add(new AttributeOperation(DELETE, it.next()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new RuntimeException("Unsupported option: " + option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file == null && attrs.size() == 0) {
|
||||||
|
throw new RuntimeException("No file nor attribute values specified");
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have several options for update:
|
||||||
|
//
|
||||||
|
// A) if a file is specified, then we can overwrite server state with that file
|
||||||
|
// (that's the normal flow - get and save locally, edit, post to server)
|
||||||
|
//
|
||||||
|
// update my_client -f new_client.json
|
||||||
|
//
|
||||||
|
// B) if a file is specified, and overrides are specified, then we override the file values with those from command line
|
||||||
|
// (that allows us to have a local file as a template, it's also batch job friendly)
|
||||||
|
//
|
||||||
|
// update my_client -s public=true -s enableDirectGrants=false -f new_client.json
|
||||||
|
//
|
||||||
|
// C) if no file is specified, then we can fetch the client definition from server, apply changes to it, and post it back
|
||||||
|
// (again a batch job friendly mode)
|
||||||
|
//
|
||||||
|
// update my_client -s public=true -s enableDirectGrants=false
|
||||||
|
//
|
||||||
|
// This is merge mode by default - if --merge is additionally specified, it is ignored
|
||||||
|
//
|
||||||
|
// D) if a file is specified, then we can merge the file with current state on the server
|
||||||
|
// (that is similar to previous mode except that the overrides are also taken from a file)
|
||||||
|
//
|
||||||
|
// update my_client --merge -f new_client.json
|
||||||
|
// update my_client --merge -s public=true -s enableDirectGrants=false -f new_client.json
|
||||||
|
//
|
||||||
|
// We could also support environment variables in input file, and apply them before parsing it.
|
||||||
|
//
|
||||||
|
// One problem - what if it is SAML XML? No problem as we don't support update for SAML - only create.
|
||||||
|
//
|
||||||
|
if (file == null && attrs.size() > 0) {
|
||||||
|
mergeMode = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
CmdStdinContext ctx = new CmdStdinContext();
|
||||||
|
if (file != null) {
|
||||||
|
ctx = parseFileOrStdin(file, regType);
|
||||||
|
regType = ctx.getEndpointType();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (regType == null) {
|
||||||
|
regType = DEFAULT;
|
||||||
|
ctx.setEndpointType(regType);
|
||||||
|
} else if (regType != DEFAULT && regType != OIDC) {
|
||||||
|
throw new RuntimeException("Update not supported for endpoint type: " + regType.getEndpoint());
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize config only after reading from stdin,
|
||||||
|
// to allow proper operation when piping 'get' - which consumes the old
|
||||||
|
// registration access token, and saves the new one to the config
|
||||||
|
ConfigData config = loadConfig();
|
||||||
|
config = copyWithServerInfo(config);
|
||||||
|
|
||||||
|
final String server = config.getServerUrl();
|
||||||
|
final String realm = config.getRealm();
|
||||||
|
|
||||||
|
if (token == null) {
|
||||||
|
// if registration access token is not set via --token, see if it's in the body of any input file
|
||||||
|
// but first see if it's overridden by --set, or maybe deliberately muted via -d registrationAccessToken
|
||||||
|
boolean processed = false;
|
||||||
|
for (AttributeOperation op: attrs) {
|
||||||
|
if ("registrationAccessToken".equals(op.getKey().toString())) {
|
||||||
|
processed = true;
|
||||||
|
if (op.getType() == AttributeOperation.Type.SET) {
|
||||||
|
token = op.getValue();
|
||||||
|
}
|
||||||
|
// otherwise it's delete - meaning it should stay null
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!processed) {
|
||||||
|
token = ctx.getRegistrationAccessToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token == null) {
|
||||||
|
// if registration access token is not set, try use the one from configuration
|
||||||
|
token = getRegistrationToken(config.sessionRealmConfigData(), clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupTruststore(config, commandInvocation);
|
||||||
|
|
||||||
|
String auth = token;
|
||||||
|
if (auth == null) {
|
||||||
|
config = ensureAuthInfo(config, commandInvocation);
|
||||||
|
config = copyWithServerInfo(config);
|
||||||
|
if (credentialsAvailable(config)) {
|
||||||
|
auth = ensureToken(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auth = auth != null ? "Bearer " + auth : null;
|
||||||
|
|
||||||
|
|
||||||
|
if (mergeMode) {
|
||||||
|
InputStream response = doGet(server + "/realms/" + realm + "/clients-registrations/" + regType.getEndpoint() + "/" + urlencode(clientId),
|
||||||
|
APPLICATION_JSON, auth);
|
||||||
|
|
||||||
|
String json = readFully(response);
|
||||||
|
|
||||||
|
CmdStdinContext ctxremote = new CmdStdinContext();
|
||||||
|
ctxremote.setContent(json);
|
||||||
|
ctxremote.setEndpointType(regType);
|
||||||
|
try {
|
||||||
|
|
||||||
|
if (regType == DEFAULT) {
|
||||||
|
ctxremote.setClient(JsonSerialization.readValue(json, ClientRepresentation.class));
|
||||||
|
token = ctxremote.getClient().getRegistrationAccessToken();
|
||||||
|
} else if (regType == OIDC) {
|
||||||
|
ctxremote.setOidcClient(JsonSerialization.readValue(json, OIDCClientRepresentation.class));
|
||||||
|
token = ctxremote.getOidcClient().getRegistrationAccessToken();
|
||||||
|
}
|
||||||
|
} catch (JsonParseException e) {
|
||||||
|
throw new RuntimeException("Not a valid JSON document. " + e.getMessage(), e);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Not a valid JSON document", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have to use registration access token retrieved from previous operation
|
||||||
|
// that ensures optimistic locking semantics
|
||||||
|
if (token != null) {
|
||||||
|
// we use auth with doPost later
|
||||||
|
auth = "Bearer " + token;
|
||||||
|
|
||||||
|
String newToken = token;
|
||||||
|
String clientToUpdate = clientId;
|
||||||
|
saveMergeConfig(cfg -> {
|
||||||
|
setRegistrationToken(cfg.ensureRealmConfigData(server, realm), clientToUpdate, newToken);
|
||||||
|
});
|
||||||
|
} else if (!allowUnsafe) {
|
||||||
|
throw new RuntimeException("No Registration Access Token found for client: " + clientId + ". Provide one or use --unsafe.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge local representation over remote one
|
||||||
|
if (ctx.getClient() != null) {
|
||||||
|
ReflectionUtil.merge(ctx.getClient(), ctxremote.getClient());
|
||||||
|
} else if (ctx.getOidcClient() != null) {
|
||||||
|
ReflectionUtil.merge(ctx.getOidcClient(), ctxremote.getOidcClient());
|
||||||
|
}
|
||||||
|
ctx = ctxremote;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attrs.size() > 0) {
|
||||||
|
ctx = mergeAttributes(ctx, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// now update
|
||||||
|
InputStream response = doPut(server + "/realms/" + realm + "/clients-registrations/" + regType.getEndpoint() + "/" + urlencode(clientId),
|
||||||
|
APPLICATION_JSON, APPLICATION_JSON, ctx.getContent(), auth);
|
||||||
|
try {
|
||||||
|
if (regType == DEFAULT) {
|
||||||
|
ClientRepresentation clirep = JsonSerialization.readValue(response, ClientRepresentation.class);
|
||||||
|
outputResult(clirep);
|
||||||
|
token = clirep.getRegistrationAccessToken();
|
||||||
|
} else if (regType == OIDC) {
|
||||||
|
OIDCClientRepresentation clirep = JsonSerialization.readValue(response, OIDCClientRepresentation.class);
|
||||||
|
outputResult(clirep);
|
||||||
|
token = clirep.getRegistrationAccessToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
String newToken = token;
|
||||||
|
String clientToUpdate = clientId;
|
||||||
|
saveMergeConfig(cfg -> {
|
||||||
|
setRegistrationToken(cfg.ensureRealmConfigData(server, realm), clientToUpdate, newToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to process HTTP response", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CommandResult.SUCCESS;
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
commandInvocation.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void outputResult(Object result) throws IOException {
|
||||||
|
if (outputClient) {
|
||||||
|
if (compressed) {
|
||||||
|
printOut(JsonSerialization.writeValueAsString(result));
|
||||||
|
} else {
|
||||||
|
printOut(JsonSerialization.writeValueAsPrettyString(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String usage() {
|
||||||
|
StringWriter sb = new StringWriter();
|
||||||
|
PrintWriter out = new PrintWriter(sb);
|
||||||
|
out.println("Usage: " + CMD + " update CLIENT [ARGUMENTS]");
|
||||||
|
out.println();
|
||||||
|
out.println("Command to update an existing client configuration. If registration access token is specified it is used.");
|
||||||
|
out.println("Otherwise, if 'registrationAccessToken' attribute is set, that is used. Otherwise, if registration access");
|
||||||
|
out.println("token is available in configuration file, we use that. Finally, if it's not available anywhere, the current ");
|
||||||
|
out.println("active session is used.");
|
||||||
|
out.println();
|
||||||
|
out.println("Arguments:");
|
||||||
|
out.println();
|
||||||
|
out.println(" Global options:");
|
||||||
|
out.println(" -x Print full stack trace when exiting with error");
|
||||||
|
out.println(" -c, --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(" --token TOKEN Registration access token to use");
|
||||||
|
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(" CLIENT ClientId of the client to update");
|
||||||
|
out.println(" -s, --set KEY=VALUE Set specific attribute to a specified value");
|
||||||
|
out.println(" KEY+=VALUE Add item to an array");
|
||||||
|
out.println(" -d, --delete NAME Delete the specific attribute, or array item");
|
||||||
|
out.println(" -e, --endpoint TYPE Endpoint type to use - one of: 'default', 'oidc'");
|
||||||
|
out.println(" -f, --file FILENAME Use the file or standard input if '-' is specified");
|
||||||
|
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(" -u, --unsafe Allow updating without registration access token - no optimistic locking");
|
||||||
|
out.println(" -o, --output After update output the new client configuration");
|
||||||
|
out.println(" -c, --compressed Don't pretty print the output");
|
||||||
|
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 current configuration from the server, applies attribute changes to it, and sends it");
|
||||||
|
out.println("back to the server, overwriting existing configuration there. To ensure there are no unexpected changes");
|
||||||
|
out.println("Registration Access Token is used for authorization when doing changes. Alternatively, one can specify to use");
|
||||||
|
out.println("unsafe mode in which case login session's authorization is used - user requires manage-clients permission.");
|
||||||
|
out.println();
|
||||||
|
out.println();
|
||||||
|
out.println("Examples:");
|
||||||
|
out.println();
|
||||||
|
out.println("Update a client by fetching current configuration from server, and applying specified changes.");
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " update my_client -s enabled=true -s 'redirectUris=[\"http://localhost:8080/myapp/*\"]'");
|
||||||
|
out.println();
|
||||||
|
out.println("Update a client by overwriting existing configuration on the server with a new one:");
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " update my_client -f new_my_client.json");
|
||||||
|
out.println();
|
||||||
|
out.println("Update a client by overwriting existing configuration using local file as a template:");
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " update my_client -f new_my_client.json -s enabled=true");
|
||||||
|
out.println();
|
||||||
|
out.println("Update client by fetching current configuration from server and merging with specified changes:");
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " update my_client -f new_my_client.json -s enabled=true --merge");
|
||||||
|
out.println();
|
||||||
|
out.println("Update a client using 'oidc' endpoint:");
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " update my_client -e oidc -s 'redirect_uris=[\"http://localhost:8080/myapp/*\"]'");
|
||||||
|
out.println();
|
||||||
|
out.println();
|
||||||
|
out.println("Use '" + CMD + " help' for general information and a list of commands");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,164 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.client.registration.cli.commands;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import org.jboss.aesh.cl.Arguments;
|
||||||
|
import org.jboss.aesh.cl.CommandDefinition;
|
||||||
|
import org.jboss.aesh.console.command.CommandException;
|
||||||
|
import org.jboss.aesh.console.command.CommandResult;
|
||||||
|
import org.jboss.aesh.console.command.invocation.CommandInvocation;
|
||||||
|
import org.keycloak.client.registration.cli.config.ConfigData;
|
||||||
|
import org.keycloak.client.registration.cli.util.ParseUtil;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.keycloak.client.registration.cli.util.AuthUtil.ensureToken;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.setRegistrationToken;
|
||||||
|
import static org.keycloak.client.registration.cli.util.HttpUtil.APPLICATION_JSON;
|
||||||
|
import static org.keycloak.client.registration.cli.util.HttpUtil.doGet;
|
||||||
|
import static org.keycloak.client.registration.cli.util.HttpUtil.doPost;
|
||||||
|
import static org.keycloak.client.registration.cli.util.IoUtil.printOut;
|
||||||
|
import static org.keycloak.client.registration.cli.util.IoUtil.warnfOut;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.CMD;
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.PROMPT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
@CommandDefinition(name = "update-token", description = "CLIENT [ARGUMENTS]")
|
||||||
|
public class UpdateTokenCmd extends AbstractAuthOptionsCmd {
|
||||||
|
|
||||||
|
@Arguments
|
||||||
|
private List<String> args = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
|
||||||
|
|
||||||
|
try {
|
||||||
|
processGlobalOptions();
|
||||||
|
|
||||||
|
if (args.isEmpty()) {
|
||||||
|
throw new RuntimeException("CLIENT not specified");
|
||||||
|
}
|
||||||
|
|
||||||
|
String clientId = args.get(0);
|
||||||
|
|
||||||
|
if (clientId.startsWith("-")) {
|
||||||
|
warnfOut(ParseUtil.CLIENTID_OPTION_WARN, clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigData config = loadConfig();
|
||||||
|
config = copyWithServerInfo(config);
|
||||||
|
setupTruststore(config, commandInvocation);
|
||||||
|
|
||||||
|
config = ensureAuthInfo(config, commandInvocation);
|
||||||
|
String auth = ensureToken(config);
|
||||||
|
|
||||||
|
String cid = null;
|
||||||
|
|
||||||
|
final String server = config.getServerUrl();
|
||||||
|
final String realm = config.getRealm();
|
||||||
|
|
||||||
|
// first we need to get id of the client with client_id == clientId
|
||||||
|
InputStream response = doGet(server + "/admin/realms/" + realm + "/clients", APPLICATION_JSON, "Bearer " + auth);
|
||||||
|
try {
|
||||||
|
List<ClientRepresentation> clients = JsonSerialization.readValue(response, new TypeReference<List<ClientRepresentation>>() {});
|
||||||
|
for (ClientRepresentation client: clients) {
|
||||||
|
if (clientId.equals(client.getClientId())) {
|
||||||
|
cid = client.getId();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to process response from server", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cid == null) {
|
||||||
|
throw new RuntimeException("No client found for: " + clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
response = doPost(server + "/admin/realms/" + realm + "/clients/" + cid + "/registration-access-token",
|
||||||
|
APPLICATION_JSON, APPLICATION_JSON, null, "Bearer " + auth);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ClientRepresentation client = JsonSerialization.readValue(response, ClientRepresentation.class);
|
||||||
|
|
||||||
|
if (noconfig) {
|
||||||
|
// output to stdout
|
||||||
|
printOut(client.getRegistrationAccessToken());
|
||||||
|
} else {
|
||||||
|
saveMergeConfig(cfg -> {
|
||||||
|
setRegistrationToken(cfg.ensureRealmConfigData(server, realm), client.getClientId(), client.getRegistrationAccessToken());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to process response from server", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
//System.out.println("Token updated for client " + clientId);
|
||||||
|
return CommandResult.SUCCESS;
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
commandInvocation.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String usage() {
|
||||||
|
StringWriter sb = new StringWriter();
|
||||||
|
PrintWriter out = new PrintWriter(sb);
|
||||||
|
out.println("Usage: " + CMD + " update-token CLIENT [ARGUMENTS]");
|
||||||
|
out.println();
|
||||||
|
out.println("Command to reissue, and set a new registration access token if an old one is lost or becomes invalid.");
|
||||||
|
out.println("It requires an authenticated session using an account with administrator priviliges.");
|
||||||
|
out.println();
|
||||||
|
out.println("Arguments:");
|
||||||
|
out.println();
|
||||||
|
out.println(" Global options:");
|
||||||
|
out.println(" -x Print full stack trace when exiting with error");
|
||||||
|
out.println(" -c, --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 leaves");
|
||||||
|
out.println(" no tokens in config file.");
|
||||||
|
out.println();
|
||||||
|
out.println(" Command specific options:");
|
||||||
|
out.println(" CLIENT ClientId of the client to reissue a new Registration Access Token for");
|
||||||
|
out.println(" The new token is saved to a config file or printed to stdout if on-the-fly\n");
|
||||||
|
out.println(" authentication is used");
|
||||||
|
out.println();
|
||||||
|
out.println("Examples:");
|
||||||
|
out.println();
|
||||||
|
out.println("Request a new Registration Access Token from the server using current authenticated session:");
|
||||||
|
out.println(" " + PROMPT + " " + CMD + " update-token my_client");
|
||||||
|
out.println();
|
||||||
|
out.println("Use '" + CMD + " help' for general information and a list of commands");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,154 @@
|
||||||
|
package org.keycloak.client.registration.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,42 @@
|
||||||
|
package org.keycloak.client.registration.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,75 @@
|
||||||
|
/*
|
||||||
|
* 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.common;
|
||||||
|
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public class CmdStdinContext {
|
||||||
|
|
||||||
|
private EndpointType regType;
|
||||||
|
private ClientRepresentation client;
|
||||||
|
private OIDCClientRepresentation oidcClient;
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
public CmdStdinContext() {}
|
||||||
|
|
||||||
|
public EndpointType getEndpointType() {
|
||||||
|
return regType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEndpointType(EndpointType regType) {
|
||||||
|
this.regType = regType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientRepresentation getClient() {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClient(ClientRepresentation client) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OIDCClientRepresentation getOidcClient() {
|
||||||
|
return oidcClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOidcClient(OIDCClientRepresentation oidcClient) {
|
||||||
|
this.oidcClient = oidcClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContent() {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContent(String content) {
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRegistrationAccessToken() {
|
||||||
|
if (client != null) {
|
||||||
|
return client.getRegistrationAccessToken();
|
||||||
|
} else if (oidcClient != null) {
|
||||||
|
return oidcClient.getRegistrationAccessToken();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* 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.common;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public enum EndpointType {
|
||||||
|
DEFAULT("default", "default"),
|
||||||
|
OIDC("openid-connect", "oidc", "oidc"),
|
||||||
|
INSTALL("install", "install", "adapter"),
|
||||||
|
SAML2("saml2-entity-descriptor", "saml2", "saml2");
|
||||||
|
|
||||||
|
private String endpoint;
|
||||||
|
private String preferredName;
|
||||||
|
private Set<String> alternativeNames;
|
||||||
|
|
||||||
|
private EndpointType(String endpoint, String preferredName, String ... alternativeNames) {
|
||||||
|
this.endpoint = endpoint;
|
||||||
|
this.preferredName = preferredName;
|
||||||
|
this.alternativeNames = new HashSet(Arrays.asList(alternativeNames));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EndpointType of(String name) {
|
||||||
|
if (DEFAULT.endpoint.equals(name) || DEFAULT.alternativeNames.contains(name)) {
|
||||||
|
return DEFAULT;
|
||||||
|
} else if (OIDC.endpoint.equals(name) || OIDC.alternativeNames.contains(name)) {
|
||||||
|
return OIDC;
|
||||||
|
} else if (INSTALL.endpoint.equals(name) || INSTALL.alternativeNames.contains(name)) {
|
||||||
|
return INSTALL;
|
||||||
|
} else if (SAML2.endpoint.equals(name) || SAML2.alternativeNames.contains(name)) {
|
||||||
|
return SAML2;
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Endpoint not supported: " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEndpoint() {
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return preferredName;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
package org.keycloak.client.registration.cli.common;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An iterator wrapping command line
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public class ParsingContext {
|
||||||
|
|
||||||
|
private int offset;
|
||||||
|
private int pos = -1;
|
||||||
|
private String [] args;
|
||||||
|
|
||||||
|
public ParsingContext(String [] args) {
|
||||||
|
this(args, 0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ParsingContext(String [] args, int offset) {
|
||||||
|
this(args, offset, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ParsingContext(String [] args, int offset, int pos) {
|
||||||
|
this.args = args.clone();
|
||||||
|
this.offset = offset;
|
||||||
|
this.pos = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasNext() {
|
||||||
|
return pos < args.length-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean hasNext(int count) {
|
||||||
|
return pos < args.length - count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasPrevious() {
|
||||||
|
return pos > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get next argument
|
||||||
|
*
|
||||||
|
* @return Next argument or null if beyond the end of arguments
|
||||||
|
*/
|
||||||
|
public String next() {
|
||||||
|
if (hasNext()) {
|
||||||
|
return args[++pos];
|
||||||
|
} else {
|
||||||
|
pos = args.length;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that a next argument is available
|
||||||
|
*
|
||||||
|
* @return Next argument or RuntimeException if next argument is not available
|
||||||
|
*/
|
||||||
|
public String nextRequired() {
|
||||||
|
if (!hasNext()) {
|
||||||
|
throw new RuntimeException("Option " + current() + " requires a value");
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get next n-th argument
|
||||||
|
*
|
||||||
|
* @return Next n-th argument or null if beyond the end of arguments
|
||||||
|
*/
|
||||||
|
public String next(int n) {
|
||||||
|
if (hasNext(n)) {
|
||||||
|
pos += n;
|
||||||
|
return args[pos];
|
||||||
|
} else {
|
||||||
|
pos = args.length;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get previous argument
|
||||||
|
*
|
||||||
|
* @return Previous argument or null if previous call was at the beginning of the arguments (pos == 0)
|
||||||
|
*/
|
||||||
|
public String previous() {
|
||||||
|
if (hasPrevious()) {
|
||||||
|
return args[--pos];
|
||||||
|
} else {
|
||||||
|
pos = -1;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current argument
|
||||||
|
*
|
||||||
|
* @return Current argument or null if current parsing position is beyond end, or before start
|
||||||
|
*/
|
||||||
|
public String current() {
|
||||||
|
if (pos < 0 || pos >= args.length) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return args[pos];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String [] getArgs() {
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.registration.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,12 @@
|
||||||
|
package org.keycloak.client.registration.cli.config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public interface ConfigHandler {
|
||||||
|
|
||||||
|
void saveMergeConfig(ConfigUpdateOperation op);
|
||||||
|
|
||||||
|
ConfigData loadConfig();
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package org.keycloak.client.registration.cli.config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public interface ConfigUpdateOperation {
|
||||||
|
|
||||||
|
void update(ConfigData data);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
package org.keycloak.client.registration.cli.config;
|
||||||
|
|
||||||
|
import org.keycloak.client.registration.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.registration.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,24 @@
|
||||||
|
package org.keycloak.client.registration.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,220 @@
|
||||||
|
/*
|
||||||
|
* 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.config;
|
||||||
|
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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;
|
||||||
|
|
||||||
|
private String initialToken;
|
||||||
|
|
||||||
|
private Map<String, String> clients = new LinkedHashMap<String, String>();
|
||||||
|
|
||||||
|
|
||||||
|
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 String getInitialToken() {
|
||||||
|
return initialToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInitialToken(String initialToken) {
|
||||||
|
this.initialToken = initialToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getClients() {
|
||||||
|
return clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
initialToken = source.initialToken;
|
||||||
|
|
||||||
|
mergeClients(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mergeClients(RealmConfigData source) {
|
||||||
|
if (source.clients != null) {
|
||||||
|
if (clients == null) {
|
||||||
|
clients = source.clients;
|
||||||
|
} else {
|
||||||
|
for (String key: source.clients.keySet()) {
|
||||||
|
String val = source.clients.get(key);
|
||||||
|
if (!"".equals(val)) {
|
||||||
|
clients.put(key, val);
|
||||||
|
} else {
|
||||||
|
clients.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void mergeRefreshTokens(RealmConfigData source) {
|
||||||
|
token = source.token;
|
||||||
|
refreshToken = source.refreshToken;
|
||||||
|
expiresAt = source.expiresAt;
|
||||||
|
refreshExpiresAt = source.refreshExpiresAt;
|
||||||
|
|
||||||
|
mergeClients(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void mergeRegistrationTokens(RealmConfigData source) {
|
||||||
|
initialToken = source.initialToken;
|
||||||
|
mergeClients(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
data.initialToken = initialToken;
|
||||||
|
data.clients = new LinkedHashMap<>(clients);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package org.keycloak.client.registration.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,206 @@
|
||||||
|
/*
|
||||||
|
* 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.util;
|
||||||
|
|
||||||
|
import org.keycloak.client.registration.cli.config.ConfigData;
|
||||||
|
import org.keycloak.client.registration.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.security.PrivateKey;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static java.lang.System.currentTimeMillis;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.checkAuthInfo;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ConfigUtil.saveMergeConfig;
|
||||||
|
import static org.keycloak.client.registration.cli.util.HttpUtil.APPLICATION_FORM_URL_ENCODED;
|
||||||
|
import static org.keycloak.client.registration.cli.util.HttpUtil.APPLICATION_JSON;
|
||||||
|
import static org.keycloak.client.registration.cli.util.HttpUtil.doPost;
|
||||||
|
import static org.keycloak.client.registration.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 (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException("Unexpected error", e);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to read Refresh Token response", 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,114 @@
|
||||||
|
/*
|
||||||
|
* 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.util;
|
||||||
|
|
||||||
|
import org.keycloak.client.registration.cli.config.ConfigData;
|
||||||
|
import org.keycloak.client.registration.cli.config.ConfigHandler;
|
||||||
|
import org.keycloak.client.registration.cli.config.ConfigUpdateOperation;
|
||||||
|
import org.keycloak.client.registration.cli.config.InMemoryConfigHandler;
|
||||||
|
import org.keycloak.client.registration.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_CONFIG_FILE_STRING = OsUtil.OS_ARCH.isWindows() ? "%HOMEDRIVE%%HOMEPATH%\\.keycloak\\kcreg.config" : "~/.keycloak/kcreg.config";
|
||||||
|
|
||||||
|
public static final String DEFAULT_CONFIG_FILE_PATH = System.getProperty("user.home") + "/.keycloak/kcreg.config";
|
||||||
|
|
||||||
|
private static ConfigHandler handler;
|
||||||
|
|
||||||
|
public static ConfigHandler getHandler() {
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setHandler(ConfigHandler handler) {
|
||||||
|
ConfigUtil.handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getRegistrationToken(RealmConfigData data, String clientId) {
|
||||||
|
String token = data.getClients().get(clientId);
|
||||||
|
return token == null || token.length() == 0 ? null : token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setRegistrationToken(RealmConfigData data, String clientId, String token) {
|
||||||
|
data.getClients().put(clientId, token == null ? "" : token);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package org.keycloak.client.registration.cli.util;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public class DebugBufferedInputStream extends BufferedInputStream {
|
||||||
|
|
||||||
|
public DebugBufferedInputStream(InputStream in) {
|
||||||
|
super(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized int read() throws IOException {
|
||||||
|
log("read() >>>");
|
||||||
|
int b = super.read();
|
||||||
|
log("read() <<< " + (char) b + " (" + b + ")");
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized int read(byte[] b, int off, int len) throws IOException {
|
||||||
|
log("read(buf, off, len) >>>");
|
||||||
|
int c = super.read(b, off, len);
|
||||||
|
log("read(buf, off, len) <<< " + (c != -1 ? "[" + new String(b, off, c) + "]" : "-1"));
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized long skip(long n) throws IOException {
|
||||||
|
log("skip()");
|
||||||
|
return super.skip(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized int available() throws IOException {
|
||||||
|
log("available() >>>");
|
||||||
|
int c = super.available();
|
||||||
|
log("available() >>> " + c);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void mark(int readlimit) {
|
||||||
|
log("mark()");
|
||||||
|
super.mark(readlimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void reset() throws IOException {
|
||||||
|
log("reset()");
|
||||||
|
super.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean markSupported() {
|
||||||
|
log("markSupported()");
|
||||||
|
return super.markSupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
log("close()");
|
||||||
|
super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] b) throws IOException {
|
||||||
|
return read(b, 0, b.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void log(String msg) {
|
||||||
|
System.err.println(msg);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,188 @@
|
||||||
|
/*
|
||||||
|
* 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.util;
|
||||||
|
|
||||||
|
import org.apache.http.Header;
|
||||||
|
import org.apache.http.HttpHeaders;
|
||||||
|
import org.apache.http.HttpResponse;
|
||||||
|
import org.apache.http.client.HttpClient;
|
||||||
|
import org.apache.http.client.methods.HttpDelete;
|
||||||
|
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
|
||||||
|
import org.apache.http.client.methods.HttpGet;
|
||||||
|
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.ssl.SSLContexts;
|
||||||
|
import org.apache.http.entity.StringEntity;
|
||||||
|
import org.apache.http.impl.client.HttpClientBuilder;
|
||||||
|
import org.keycloak.client.registration.cli.common.EndpointType;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
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.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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 getExpectedContentType(EndpointType type) {
|
||||||
|
switch (type) {
|
||||||
|
case DEFAULT:
|
||||||
|
case OIDC:
|
||||||
|
return APPLICATION_JSON;
|
||||||
|
case SAML2:
|
||||||
|
return APPLICATION_XML;
|
||||||
|
default:
|
||||||
|
throw new RuntimeException("Unsupported endpoint type: " + type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,235 @@
|
||||||
|
/*
|
||||||
|
* 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.util;
|
||||||
|
|
||||||
|
import org.jboss.aesh.console.AeshConsoleBufferBuilder;
|
||||||
|
import org.jboss.aesh.console.AeshInputProcessorBuilder;
|
||||||
|
import org.jboss.aesh.console.ConsoleBuffer;
|
||||||
|
import org.jboss.aesh.console.InputProcessor;
|
||||||
|
import org.jboss.aesh.console.Prompt;
|
||||||
|
import org.jboss.aesh.console.command.invocation.CommandInvocation;
|
||||||
|
import org.keycloak.client.registration.cli.aesh.Globals;
|
||||||
|
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
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.registration.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 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,60 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2014 Red Hat, Inc. and/or its affiliates.
|
||||||
|
*
|
||||||
|
* Licensed under the Eclipse Public License version 1.0, available at http://www.eclipse.org/legal/epl-v10.html
|
||||||
|
*/
|
||||||
|
package org.keycloak.client.registration.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,48 @@
|
||||||
|
package org.keycloak.client.registration.cli.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public class OsUtil {
|
||||||
|
|
||||||
|
public static final OsArch OS_ARCH = determineOSAndArch();
|
||||||
|
|
||||||
|
public static final String CMD = OS_ARCH.isWindows() ? "kcreg.bat" : "kcreg.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,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.registration.cli.util;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonParseException;
|
||||||
|
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
|
||||||
|
import org.keycloak.client.registration.cli.common.AttributeOperation;
|
||||||
|
import org.keycloak.client.registration.cli.common.CmdStdinContext;
|
||||||
|
import org.keycloak.client.registration.cli.common.EndpointType;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static java.lang.System.arraycopy;
|
||||||
|
import static org.keycloak.client.registration.cli.util.IoUtil.readFileOrStdin;
|
||||||
|
import static org.keycloak.client.registration.cli.util.ReflectionUtil.setAttributes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public class ParseUtil {
|
||||||
|
|
||||||
|
public static final String CLIENTID_OPTION_WARN = "You're using what looks like an OPTION as CLIENT_ID: %s";
|
||||||
|
public static final String TOKEN_OPTION_WARN = "You're using what looks like an OPTION as TOKEN: %s";
|
||||||
|
|
||||||
|
public static String[] shift(String[] args) {
|
||||||
|
if (args.length == 1)
|
||||||
|
return new String[0];
|
||||||
|
String [] nu = new String [args.length-1];
|
||||||
|
arraycopy(args, 1, nu, 0, args.length-1);
|
||||||
|
return nu;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 parseFileOrStdin(String file, EndpointType type) {
|
||||||
|
|
||||||
|
String content = readFileOrStdin(file).trim();
|
||||||
|
ClientRepresentation client = null;
|
||||||
|
OIDCClientRepresentation oidcClient = null;
|
||||||
|
|
||||||
|
if (type == null) {
|
||||||
|
// guess the correct endpoint from content of the file
|
||||||
|
if (content.startsWith("<")) {
|
||||||
|
// looks like XML
|
||||||
|
type = EndpointType.SAML2;
|
||||||
|
} else if (content.startsWith("{")) {
|
||||||
|
// looks like JSON?
|
||||||
|
// try parse as ClientRepresentation
|
||||||
|
try {
|
||||||
|
client = JsonSerialization.readValue(content, ClientRepresentation.class);
|
||||||
|
type = EndpointType.DEFAULT;
|
||||||
|
|
||||||
|
} catch (JsonParseException e) {
|
||||||
|
throw new RuntimeException("Failed to read the input document as JSON: " + e.getMessage(), e);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// deliberately not logged
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client == null) {
|
||||||
|
// try parse as OIDCClientRepresentation
|
||||||
|
try {
|
||||||
|
oidcClient = JsonSerialization.readValue(content, OIDCClientRepresentation.class);
|
||||||
|
type = EndpointType.OIDC;
|
||||||
|
} catch (IOException ne) {
|
||||||
|
throw new RuntimeException("Unable to determine input document type. Use -e TYPE to specify the registration endpoint to use");
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to read the input document as JSON", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (content.length() == 0) {
|
||||||
|
throw new RuntimeException("Document provided by --file option is empty");
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Unable to determine input document type. Use -e TYPE to specify the registration endpoint to use");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check content type, making sure it can be parsed into .json if it's not saml xml
|
||||||
|
if (content != null) {
|
||||||
|
try {
|
||||||
|
if (type == EndpointType.DEFAULT && client == null) {
|
||||||
|
client = JsonSerialization.readValue(content, ClientRepresentation.class);
|
||||||
|
} else if (type == EndpointType.OIDC && oidcClient == null) {
|
||||||
|
oidcClient = JsonSerialization.readValue(content, OIDCClientRepresentation.class);
|
||||||
|
}
|
||||||
|
} catch (JsonParseException e) {
|
||||||
|
throw new RuntimeException("Not a valid JSON document - " + e.getMessage(), e);
|
||||||
|
} catch (UnrecognizedPropertyException e) {
|
||||||
|
throw new RuntimeException("Attribute '" + e.getPropertyName() + "' not supported on document type '" + type.getName() + "'", e);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Not a valid JSON document", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CmdStdinContext ctx = new CmdStdinContext();
|
||||||
|
ctx.setEndpointType(type);
|
||||||
|
ctx.setContent(content);
|
||||||
|
ctx.setClient(client);
|
||||||
|
ctx.setOidcClient(oidcClient);
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CmdStdinContext mergeAttributes(CmdStdinContext ctx, List<AttributeOperation> attrs) {
|
||||||
|
String content = ctx.getContent();
|
||||||
|
ClientRepresentation client = ctx.getClient();
|
||||||
|
OIDCClientRepresentation oidcClient = ctx.getOidcClient();
|
||||||
|
EndpointType type = ctx.getEndpointType();
|
||||||
|
try {
|
||||||
|
if (content == null) {
|
||||||
|
if (type == EndpointType.DEFAULT) {
|
||||||
|
client = new ClientRepresentation();
|
||||||
|
} else if (type == EndpointType.OIDC) {
|
||||||
|
oidcClient = new OIDCClientRepresentation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Object rep = client != null ? client : oidcClient;
|
||||||
|
if (rep != null) {
|
||||||
|
try {
|
||||||
|
setAttributes(rep, attrs);
|
||||||
|
} catch (AttributeException e) {
|
||||||
|
throw new RuntimeException("Failed to set attribute '" + e.getAttributeName() + "' on document type '" + type.getName() + "'", e);
|
||||||
|
}
|
||||||
|
content = JsonSerialization.writeValueAsString(rep);
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Setting attributes is not supported for type: " + type.getName());
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to merge set attributes with configuration from file", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.setContent(content);
|
||||||
|
ctx.setClient(client);
|
||||||
|
ctx.setOidcClient(oidcClient);
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,498 @@
|
||||||
|
/*
|
||||||
|
* 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.util;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonParseException;
|
||||||
|
import org.keycloak.client.registration.cli.common.AttributeKey;
|
||||||
|
import org.keycloak.client.registration.cli.common.AttributeOperation;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.ParameterizedType;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public class ReflectionUtil {
|
||||||
|
|
||||||
|
static Map<Class, Map<String, Field>> index = new HashMap<>();
|
||||||
|
|
||||||
|
static void populateAttributesIndex(Class type) {
|
||||||
|
// We are using fields rather than getters / setters
|
||||||
|
// because it seems like JSON mapping sometimes also uses fields as well
|
||||||
|
// This may have to be changed some day due to reliance on Field.setAccessible()
|
||||||
|
Map<String, Field> map = new HashMap<>();
|
||||||
|
Field [] fields = type.getDeclaredFields();
|
||||||
|
for (Field f: fields) {
|
||||||
|
// make sure to also have access to non-public fields
|
||||||
|
f.setAccessible(true);
|
||||||
|
map.put(f.getName(), f);
|
||||||
|
}
|
||||||
|
index.put(type, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<String, Field> getAttrFieldsForType(Type gtype) {
|
||||||
|
Class type;
|
||||||
|
if (gtype instanceof Class) {
|
||||||
|
type = (Class) gtype;
|
||||||
|
} else if (gtype instanceof ParameterizedType) {
|
||||||
|
type = (Class) ((ParameterizedType) gtype).getRawType();
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Unexpected type: " + gtype);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isListType(type) || isMapType(type)) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
Map<String, Field> map = index.get(type);
|
||||||
|
if (map == null) {
|
||||||
|
populateAttributesIndex(type);
|
||||||
|
map = index.get(type);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isListType(Class type) {
|
||||||
|
return List.class.isAssignableFrom(type) || type.isArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isBasicType(Type type) {
|
||||||
|
return type == String.class || type == Boolean.class || type == boolean.class
|
||||||
|
|| type == Integer.class || type == int.class || type == Long.class || type == long.class
|
||||||
|
|| type == Float.class || type == float.class || type == Double.class || type == double.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isMapType(Class type) {
|
||||||
|
return Map.class.isAssignableFrom(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Object convertValueToType(Object value, Class<?> type) throws IOException {
|
||||||
|
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
|
||||||
|
} else if (value instanceof String) {
|
||||||
|
if (type == String.class) {
|
||||||
|
return value;
|
||||||
|
} else if (type == Boolean.class) {
|
||||||
|
return Boolean.valueOf((String) value);
|
||||||
|
} else if (type == Integer.class) {
|
||||||
|
return Integer.valueOf((String) value);
|
||||||
|
} else if (type == Long.class) {
|
||||||
|
return Long.valueOf((String) value);
|
||||||
|
} else {
|
||||||
|
return JsonSerialization.readValue((String) value, type);
|
||||||
|
}
|
||||||
|
} else if (value instanceof Number) {
|
||||||
|
if (type == Integer.class) {
|
||||||
|
return ((Number) value).intValue();
|
||||||
|
} else if (type == Long.class) {
|
||||||
|
return ((Long) value).longValue();
|
||||||
|
} else if (type == String.class) {
|
||||||
|
return String.valueOf(value);
|
||||||
|
}
|
||||||
|
} else if (value instanceof Boolean) {
|
||||||
|
if (type == Boolean.class) {
|
||||||
|
return value;
|
||||||
|
} else if (type == String.class) {
|
||||||
|
return String.valueOf(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new RuntimeException("Unable to handle type [" + type + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setAttributes(Object client, List<AttributeOperation> attrs) {
|
||||||
|
|
||||||
|
for (AttributeOperation item: attrs) {
|
||||||
|
|
||||||
|
AttributeKey attr = item.getKey();
|
||||||
|
Object nested = client;
|
||||||
|
|
||||||
|
List<AttributeKey.Component> cs = attr.getComponents();
|
||||||
|
for (int i = 0; i < cs.size(); i++) {
|
||||||
|
AttributeKey.Component c = cs.get(i);
|
||||||
|
|
||||||
|
Class type = nested.getClass();
|
||||||
|
Field field = null;
|
||||||
|
|
||||||
|
if (!isMapType(type)) {
|
||||||
|
Map<String, Field> fields = getAttrFieldsForType(type);
|
||||||
|
if (fields == null) {
|
||||||
|
throw new AttributeException(attr.toString(), "Unexpected condition - unknown type: " + type);
|
||||||
|
}
|
||||||
|
|
||||||
|
field = fields.get(c.getName());
|
||||||
|
Class parent = type;
|
||||||
|
while (field == null) {
|
||||||
|
parent = parent.getSuperclass();
|
||||||
|
if (parent == Object.class) {
|
||||||
|
throw new AttributeException(attr.toString(), "Unknown attribute '" + c.getName() + "' on " + client.getClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
fields = getAttrFieldsForType(parent);
|
||||||
|
field = fields.get(c.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if it's a 'basic' type we directly use setter
|
||||||
|
type = field == null ? type : field.getType();
|
||||||
|
if (isBasicType(type)) {
|
||||||
|
if (i < cs.size() - 1) {
|
||||||
|
throw new AttributeException(attr.toString(), "Attribute is of primitive type, and can't be nested further: " + c);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Object val = convertValueToType(item.getValue(), type);
|
||||||
|
field.set(nested, val);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AttributeException(attr.toString(), "Failed to set attribute " + attr, e);
|
||||||
|
}
|
||||||
|
} else if (isListType(type)) {
|
||||||
|
if (i < cs.size() -1) {
|
||||||
|
// not the target component
|
||||||
|
try {
|
||||||
|
nested = field.get(nested);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AttributeException(attr.toString(), "Failed to get attribute \"" + c + "\" in " + attr, e);
|
||||||
|
}
|
||||||
|
if (c.getIndex() >= 0) {
|
||||||
|
// list item
|
||||||
|
// get idx-th item
|
||||||
|
List l = (List) nested;
|
||||||
|
if (c.getIndex() >= l.size()) {
|
||||||
|
throw new AttributeException(attr.toString(), "Array index out of bounds for \"" + c + "\" in " + attr);
|
||||||
|
}
|
||||||
|
nested = l.get(c.getIndex());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// target component
|
||||||
|
Class itype = type;
|
||||||
|
Type gtype = field.getGenericType();
|
||||||
|
if (gtype instanceof ParameterizedType) {
|
||||||
|
Type[] typeArgs = ((ParameterizedType) gtype).getActualTypeArguments();
|
||||||
|
if (typeArgs.length >= 1 && typeArgs[0] instanceof Class) {
|
||||||
|
itype = (Class) typeArgs[0];
|
||||||
|
} else {
|
||||||
|
itype = String.class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (c.getIndex() >= 0 || attr.isAppend()) {
|
||||||
|
// some list item
|
||||||
|
// get the list first
|
||||||
|
List target;
|
||||||
|
try {
|
||||||
|
target = (List) field.get(nested);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AttributeException(attr.toString(), "Failed to get list attribute: " + attr, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// now replace or add idx-th item
|
||||||
|
if (target == null) {
|
||||||
|
target = createNewList(type);
|
||||||
|
try {
|
||||||
|
field.set(nested, target);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AttributeException(attr.toString(), "Failed to set list attribute " + attr, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (c.getIndex() >= target.size()) {
|
||||||
|
throw new AttributeException(attr.toString(), "Array index out of bounds for \"" + c + "\" in " + attr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attr.isAppend()) {
|
||||||
|
try {
|
||||||
|
Object value = convertValueToType(item.getValue(), itype);
|
||||||
|
if (c.getIndex() >= 0) {
|
||||||
|
target.add(c.getIndex(), value);
|
||||||
|
} else {
|
||||||
|
target.add(value);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AttributeException(attr.toString(), "Failed to set list attribute " + attr, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (item.getType() == AttributeOperation.Type.SET) {
|
||||||
|
try {
|
||||||
|
Object value = convertValueToType(item.getValue(), itype);
|
||||||
|
target.set(c.getIndex(), value);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AttributeException(attr.toString(), "Failed to set list attribute " + attr, e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
target.remove(c.getIndex());
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AttributeException(attr.toString(), "Failed to remove list attribute " + attr, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// set the whole list field itself
|
||||||
|
List value = createNewList(type);;
|
||||||
|
if (item.getType() == AttributeOperation.Type.SET) {
|
||||||
|
List converted = convertValueToList(item.getValue(), itype);
|
||||||
|
value.addAll(converted);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
field.set(nested, value);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AttributeException(attr.toString(), "Failed to set list attribute " + attr, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// object type
|
||||||
|
if (i < cs.size() -1) {
|
||||||
|
// not the target component
|
||||||
|
Object value;
|
||||||
|
if (field == null) {
|
||||||
|
if (isMapType(nested.getClass())) {
|
||||||
|
value = ((Map) nested).get(c.getName());
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Unexpected condition while processing: " + attr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
value = field.get(nested);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AttributeException(attr.toString(), "Failed to get attribute \"" + c + "\" in " + attr, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (value == null) {
|
||||||
|
// create the target attribute
|
||||||
|
if (isMapType(nested.getClass())) {
|
||||||
|
throw new RuntimeException("Creating nested object trees not supported");
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
value = createNewObject(type);
|
||||||
|
field.set(nested, value);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AttributeException(attr.toString(), "Failed to set attribute " + attr, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nested = value;
|
||||||
|
} else {
|
||||||
|
// target component
|
||||||
|
// todo implement map put
|
||||||
|
if (isMapType(nested.getClass())) {
|
||||||
|
try {
|
||||||
|
((Map) nested).put(c.getName(), item.getValue());
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AttributeException(attr.toString(), "Failed to set map key " + attr, e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
Object value = convertValueToType(item.getValue(), type);
|
||||||
|
field.set(nested, value);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AttributeException(attr.toString(), "Failed to set attribute " + attr, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object createNewObject(Class type) throws Exception {
|
||||||
|
return type.newInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List createNewList(Class type) {
|
||||||
|
|
||||||
|
if (type == List.class) {
|
||||||
|
return new ArrayList();
|
||||||
|
} else if (type.isInterface()) {
|
||||||
|
throw new RuntimeException("Can't instantiate a list type: " + type);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return (List) type.newInstance();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to instantiate a list type: " + type, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List convertValueToList(String value, Class itemType) {
|
||||||
|
try {
|
||||||
|
List result = new LinkedList();
|
||||||
|
if (!value.startsWith("[")) {
|
||||||
|
throw new RuntimeException("List attribute value has to start with '[' - '" + value + "'");
|
||||||
|
}
|
||||||
|
List parsed = JsonSerialization.readValue(value, List.class);
|
||||||
|
for (Object item: parsed) {
|
||||||
|
if (itemType.isAssignableFrom(item.getClass())) {
|
||||||
|
result.add(item);
|
||||||
|
} else {
|
||||||
|
result.add(convertValueToType(item, itemType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (JsonParseException e) {
|
||||||
|
throw new RuntimeException("Failed to parse list value: " + e.getMessage(), e);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to parse list value: " + value, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> void merge(T source, T dest) {
|
||||||
|
// Use existing index for type, then iterate over all attributes and
|
||||||
|
// use setter on dest, and getter on source to copy value over
|
||||||
|
Map<String, Field> fieldMap = getAttrFieldsForType(source.getClass());
|
||||||
|
try {
|
||||||
|
for (String attrName : fieldMap.keySet()) {
|
||||||
|
Field field = fieldMap.get(attrName);
|
||||||
|
Object localValue = field.get(source);
|
||||||
|
if (localValue != null) {
|
||||||
|
field.set(dest, localValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to merge changes", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static LinkedHashMap<String, String> getAttributeListWithJSonTypes(Class type, AttributeKey attr) {
|
||||||
|
|
||||||
|
LinkedHashMap<String, String> result = new LinkedHashMap<>();
|
||||||
|
attr = attr != null ? attr : new AttributeKey();
|
||||||
|
|
||||||
|
Map<String, Field> fields = getAttrFieldsForType(type);
|
||||||
|
for (AttributeKey.Component c: attr.getComponents()) {
|
||||||
|
Field f = fields.get(c.getName());
|
||||||
|
if (f == null) {
|
||||||
|
throw new AttributeException(attr.toString(), "No such attribute: " + attr);
|
||||||
|
}
|
||||||
|
|
||||||
|
type = f.getType();
|
||||||
|
if (isBasicType(type) || isListType(type) || isMapType(type)) {
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
fields = getAttrFieldsForType(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<String, Field> item : fields.entrySet()) {
|
||||||
|
String key = item.getKey();
|
||||||
|
Class clazz = item.getValue().getType();
|
||||||
|
String t = getTypeString(clazz, item.getValue());
|
||||||
|
|
||||||
|
result.put(key, t);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Field resolveField(Class type, AttributeKey attr) {
|
||||||
|
Field f = null;
|
||||||
|
Type gtype = type;
|
||||||
|
|
||||||
|
for (AttributeKey.Component c: attr.getComponents()) {
|
||||||
|
if (f != null) {
|
||||||
|
gtype = f.getGenericType();
|
||||||
|
if (gtype instanceof ParameterizedType) {
|
||||||
|
Type[] typeargs = ((ParameterizedType) gtype).getActualTypeArguments();
|
||||||
|
if (typeargs.length > 0) {
|
||||||
|
gtype = typeargs[typeargs.length-1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<String, Field> fields = getAttrFieldsForType(gtype);
|
||||||
|
f = fields.get(c.getName());
|
||||||
|
if (f == null) {
|
||||||
|
throw new AttributeException(attr.toString(), "No such attribute: " + attr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getTypeString(Type type, Field field) {
|
||||||
|
Class clazz = null;
|
||||||
|
if (type == null) {
|
||||||
|
if (field == null) {
|
||||||
|
throw new IllegalArgumentException("type == null and field == null");
|
||||||
|
}
|
||||||
|
type = field.getGenericType();
|
||||||
|
}
|
||||||
|
if (type instanceof Class) {
|
||||||
|
clazz = (Class) type;
|
||||||
|
} else if (type instanceof ParameterizedType) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
String rtype = getTypeString(((ParameterizedType) type).getRawType(), null);
|
||||||
|
|
||||||
|
sb.append(rtype);
|
||||||
|
sb.append(" ").append("(");
|
||||||
|
Type[] typeArgs = ((ParameterizedType) type).getActualTypeArguments();
|
||||||
|
|
||||||
|
for (int i = 0; i < typeArgs.length; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
sb.append(", ");
|
||||||
|
}
|
||||||
|
sb.append(getTypeString(typeArgs[i], null));
|
||||||
|
}
|
||||||
|
sb.append(")");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CharSequence.class.isAssignableFrom(clazz)) {
|
||||||
|
return "string";
|
||||||
|
} else if (Integer.class.isAssignableFrom(clazz) || int.class.isAssignableFrom(clazz)) {
|
||||||
|
return "int";
|
||||||
|
} else if (Long.class.isAssignableFrom(clazz) || long.class.isAssignableFrom(clazz)) {
|
||||||
|
return "long";
|
||||||
|
} else if (Float.class.isAssignableFrom(clazz) || float.class.isAssignableFrom(clazz)) {
|
||||||
|
return "float";
|
||||||
|
} else if (Double.class.isAssignableFrom(clazz) || double.class.isAssignableFrom(clazz)) {
|
||||||
|
return "double";
|
||||||
|
} else if (Number.class.isAssignableFrom(clazz)) {
|
||||||
|
return "number";
|
||||||
|
} else if (Boolean.class.isAssignableFrom(clazz) || boolean.class.isAssignableFrom(clazz)) {
|
||||||
|
return "boolean";
|
||||||
|
} else if (isListType(clazz)) {
|
||||||
|
if (field != null) {
|
||||||
|
Type gtype = field.getGenericType();
|
||||||
|
if (gtype == clazz && clazz.isArray()) {
|
||||||
|
return "array (" + getTypeString(clazz.getComponentType(), null) + ")";
|
||||||
|
}
|
||||||
|
return getTypeString(gtype, null);
|
||||||
|
}
|
||||||
|
return "array";
|
||||||
|
} else if (isMapType(clazz)) {
|
||||||
|
if (field != null) {
|
||||||
|
Type gtype = field.getGenericType();
|
||||||
|
return getTypeString(gtype, null);
|
||||||
|
}
|
||||||
|
return "object";
|
||||||
|
} else {
|
||||||
|
return "object";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,355 @@
|
||||||
|
package org.keycloak.client.registration.cli.util;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Ignore;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.client.registration.cli.common.AttributeKey;
|
||||||
|
import org.keycloak.client.registration.cli.common.AttributeKey.Component;
|
||||||
|
import org.keycloak.client.registration.cli.common.AttributeOperation;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.ParameterizedType;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.Arrays;
|
||||||
|
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.registration.cli.common.AttributeOperation.Type.DELETE;
|
||||||
|
import static org.keycloak.client.registration.cli.common.AttributeOperation.Type.SET;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public class ReflectionUtilTest {
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
public void testListAttributes() {
|
||||||
|
LinkedHashMap<String, String> items = null;
|
||||||
|
/*
|
||||||
|
items = getAttributeListWithJSonTypes(Data.class, new AttributeKey(""));
|
||||||
|
|
||||||
|
for (Map.Entry<String, String> item: items.entrySet()) {
|
||||||
|
System.out.printf("%-40s %s\n", item.getKey(), item.getValue());
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
System.out.println("\n-- nested ------------------------\n");
|
||||||
|
|
||||||
|
items = getAttributeListWithJSonTypes(Data.class, new AttributeKey("nested"));
|
||||||
|
for (Map.Entry<String, String> item: items.entrySet()) {
|
||||||
|
System.out.printf("%-40s %s\n", item.getKey(), item.getValue());
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
System.out.println("\n-- dataList ----------------------\n");
|
||||||
|
|
||||||
|
items = ReflectionUtil.getAttributeListWithJSonTypes(Data.class, new AttributeKey("dataList"));
|
||||||
|
for (Map.Entry<String, String> item: items.entrySet()) {
|
||||||
|
System.out.printf("%-40s %s\n", item.getKey(), item.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.size() == 0) {
|
||||||
|
Field f = ReflectionUtil.resolveField(Data.class, new AttributeKey("dataList"));
|
||||||
|
String ts = ReflectionUtil.getTypeString(null, f);
|
||||||
|
Type t = f.getGenericType();
|
||||||
|
if ((List.class.isAssignableFrom(f.getType()) || f.getType().isArray()) && t instanceof ParameterizedType) {
|
||||||
|
System.out.printf("%s, where object is:\n", ts);
|
||||||
|
}
|
||||||
|
t = ((ParameterizedType) t).getActualTypeArguments()[0];
|
||||||
|
if (t instanceof Class) {
|
||||||
|
items = ReflectionUtil.getAttributeListWithJSonTypes((Class) t, null);
|
||||||
|
for (Map.Entry<String, String> item: items.entrySet()) {
|
||||||
|
System.out.printf(" %-37s %s\n", item.getKey(), item.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSettingAttibutes() {
|
||||||
|
Data data = new Data();
|
||||||
|
|
||||||
|
LinkedList<AttributeOperation> attrs = new LinkedList<>();
|
||||||
|
|
||||||
|
attrs.add(new AttributeOperation(SET, "longAttr", "42"));
|
||||||
|
attrs.add(new AttributeOperation(SET, "strAttr", "not null"));
|
||||||
|
attrs.add(new AttributeOperation(SET, "strList+", "two"));
|
||||||
|
attrs.add(new AttributeOperation(SET, "strList+", "three"));
|
||||||
|
attrs.add(new AttributeOperation(SET, "strList[0]+", "one"));
|
||||||
|
attrs.add(new AttributeOperation(SET, "config", "{\"key1\": \"value1\"}"));
|
||||||
|
attrs.add(new AttributeOperation(SET, "config.key2", "value2"));
|
||||||
|
attrs.add(new AttributeOperation(SET, "nestedConfig", "{\"key1\": {\"sub key1\": \"sub value1\"}}"));
|
||||||
|
attrs.add(new AttributeOperation(SET, "nestedConfig.key1.\"sub key2\"", "sub value2"));
|
||||||
|
attrs.add(new AttributeOperation(SET, "nested.strList", "[1,2,3,4]"));
|
||||||
|
attrs.add(new AttributeOperation(SET, "nested.dataList+", "{\"baseAttr\": \"item1\", \"strList\": [\"confidential\", \"public\"]}"));
|
||||||
|
attrs.add(new AttributeOperation(SET, "nested.dataList+", "{\"baseAttr\": \"item2\", \"strList\": [\"external\"]}"));
|
||||||
|
attrs.add(new AttributeOperation(SET, "nested.dataList[1].baseAttr", "changed item2"));
|
||||||
|
attrs.add(new AttributeOperation(SET, "nested.nested.strList", "[\"first\",\"second\"]"));
|
||||||
|
attrs.add(new AttributeOperation(DELETE, "nested.strList[1]"));
|
||||||
|
attrs.add(new AttributeOperation(SET, "nested.nested.nested", "{\"baseAttr\": \"NEW VALUE\", \"strList\": [true, false]}"));
|
||||||
|
attrs.add(new AttributeOperation(SET, "nested.strAttr", "NOT NULL"));
|
||||||
|
attrs.add(new AttributeOperation(DELETE, "nested.strAttr"));
|
||||||
|
|
||||||
|
ReflectionUtil.setAttributes(data, attrs);
|
||||||
|
|
||||||
|
Assert.assertEquals("longAttr", Long.valueOf(42), data.getLongAttr());
|
||||||
|
Assert.assertEquals("strAttr", "not null", data.getStrAttr());
|
||||||
|
Assert.assertEquals("strList", Arrays.asList("one", "two", "three"), data.getStrList());
|
||||||
|
|
||||||
|
Map<String, String> expectedMap = new HashMap<>();
|
||||||
|
expectedMap.put("key1", "value1");
|
||||||
|
expectedMap.put("key2", "value2");
|
||||||
|
Assert.assertEquals("config", expectedMap, data.getConfig());
|
||||||
|
|
||||||
|
|
||||||
|
expectedMap = new HashMap<>();
|
||||||
|
expectedMap.put("sub key1", "sub value1");
|
||||||
|
expectedMap.put("sub key2", "sub value2");
|
||||||
|
|
||||||
|
Assert.assertNotNull("nestedConfig", data.getNestedConfig());
|
||||||
|
Assert.assertEquals("nestedConfig has one element", 1, data.getNestedConfig().size());
|
||||||
|
Assert.assertEquals("nestedConfig.key1", expectedMap, data.getNestedConfig().get("key1"));
|
||||||
|
|
||||||
|
|
||||||
|
Data nested = data.getNested();
|
||||||
|
Assert.assertEquals("nested.strAttr", null, nested.getStrAttr());
|
||||||
|
Assert.assertEquals("nested.strList", Arrays.asList("1", "3", "4"), nested.getStrList());
|
||||||
|
Assert.assertEquals("nested.dataList[0].baseAttr", "item1", nested.getDataList().get(0).getBaseAttr());
|
||||||
|
Assert.assertEquals("nested.dataList[0].strList", Arrays.asList("confidential", "public"), nested.getDataList().get(0).getStrList());
|
||||||
|
Assert.assertEquals("nested.dataList[1].baseAttr", "changed item2", nested.getDataList().get(1).getBaseAttr());
|
||||||
|
Assert.assertEquals("nested.dataList[1].strList", Arrays.asList("external"), nested.getDataList().get(1).getStrList());
|
||||||
|
|
||||||
|
nested = nested.getNested();
|
||||||
|
Assert.assertEquals("nested.nested.strList", Arrays.asList("first", "second"), nested.getStrList());
|
||||||
|
|
||||||
|
nested = nested.getNested();
|
||||||
|
Assert.assertEquals("nested.nested.nested.baseAttr", "NEW VALUE", nested.getBaseAttr());
|
||||||
|
Assert.assertEquals("nested.nested.nested.strList", Arrays.asList("true", "false"), nested.getStrList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testKeyParsing() {
|
||||||
|
|
||||||
|
assertAttributeKey(new AttributeKey("am.bam.pet"), "am", -1, "bam", -1, "pet", -1);
|
||||||
|
|
||||||
|
assertAttributeKey(new AttributeKey("a"), "a", -1);
|
||||||
|
|
||||||
|
assertAttributeKey(new AttributeKey("a.b"), "a", -1, "b", -1);
|
||||||
|
|
||||||
|
assertAttributeKey(new AttributeKey("a.b[1]"), "a", -1, "b", 1);
|
||||||
|
|
||||||
|
assertAttributeKey(new AttributeKey("a[12].b"), "a", 12, "b", -1);
|
||||||
|
|
||||||
|
assertAttributeKey(new AttributeKey("a[10].b[20]"), "a", 10, "b", 20);
|
||||||
|
|
||||||
|
assertAttributeKey(new AttributeKey("\"am\".\"bam\".\"pet\""), "am", -1, "bam", -1, "pet", -1);
|
||||||
|
|
||||||
|
assertAttributeKey(new AttributeKey("\"am\".bam.\"pet\""), "am", -1, "bam", -1, "pet", -1);
|
||||||
|
|
||||||
|
assertAttributeKey(new AttributeKey("\"am.bam\".\"pet\""), "am.bam", -1, "pet", -1);
|
||||||
|
|
||||||
|
assertAttributeKey(new AttributeKey("\"am.bam[2]\".\"pet[6]\""), "am.bam", 2, "pet", 6);
|
||||||
|
|
||||||
|
try {
|
||||||
|
new AttributeKey("a.");
|
||||||
|
|
||||||
|
Assert.fail("Should have failed");
|
||||||
|
} catch (RuntimeException expected) {
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
new AttributeKey("a[]");
|
||||||
|
|
||||||
|
Assert.fail("Should have failed");
|
||||||
|
} catch (RuntimeException expected) {
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
new AttributeKey("a[lala]");
|
||||||
|
|
||||||
|
Assert.fail("Should have failed");
|
||||||
|
} catch (RuntimeException expected) {
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
new AttributeKey("a[\"lala\"]");
|
||||||
|
|
||||||
|
Assert.fail("Should have failed");
|
||||||
|
} catch (RuntimeException expected) {
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
new AttributeKey(".a");
|
||||||
|
|
||||||
|
Assert.fail("Should have failed");
|
||||||
|
} catch (RuntimeException expected) {
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
new AttributeKey("\"am\"..\"bam\".\"pet\"");
|
||||||
|
|
||||||
|
Assert.fail("Should have failed");
|
||||||
|
} catch (RuntimeException expected) {
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
new AttributeKey("\"am\"ups.\"bam\".\"pet\"");
|
||||||
|
|
||||||
|
Assert.fail("Should have failed");
|
||||||
|
} catch (RuntimeException expected) {
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
new AttributeKey("ups\"am\"ups.\"bam\".\"pet\"");
|
||||||
|
|
||||||
|
Assert.fail("Should have failed");
|
||||||
|
} catch (RuntimeException expected) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertAttributeKey(AttributeKey key, Object ... args) {
|
||||||
|
Iterator<Component> it = key.getComponents().iterator();
|
||||||
|
|
||||||
|
for (int i = 0; i < args.length; i++) {
|
||||||
|
String name = String.valueOf(args[i++]);
|
||||||
|
int idx = Integer.valueOf(String.valueOf(args[i]));
|
||||||
|
|
||||||
|
Component component = it.next();
|
||||||
|
Assert.assertEquals(name, component.getName());
|
||||||
|
Assert.assertEquals(idx, component.getIndex());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static class BaseData {
|
||||||
|
|
||||||
|
String baseAttr;
|
||||||
|
|
||||||
|
public String getBaseAttr() {
|
||||||
|
return baseAttr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBaseAttr(String baseAttr) {
|
||||||
|
this.baseAttr = baseAttr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Data extends BaseData {
|
||||||
|
|
||||||
|
String strAttr;
|
||||||
|
|
||||||
|
Integer intAttr;
|
||||||
|
|
||||||
|
Long longAttr;
|
||||||
|
|
||||||
|
Boolean boolAttr;
|
||||||
|
|
||||||
|
List<String> strList;
|
||||||
|
|
||||||
|
List<Integer> intList;
|
||||||
|
|
||||||
|
List<Data> dataList;
|
||||||
|
|
||||||
|
List<List<String>> deepList;
|
||||||
|
|
||||||
|
Data nested;
|
||||||
|
|
||||||
|
Map<String, String> config;
|
||||||
|
|
||||||
|
Map<String, Map<String, Data>> nestedConfig;
|
||||||
|
|
||||||
|
|
||||||
|
public String getStrAttr() {
|
||||||
|
return strAttr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStrAttr(String strAttr) {
|
||||||
|
this.strAttr = strAttr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getIntAttr() {
|
||||||
|
return intAttr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIntAttr(Integer intAttr) {
|
||||||
|
this.intAttr = intAttr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getLongAttr() {
|
||||||
|
return longAttr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLongAttr(Long longAttr) {
|
||||||
|
this.longAttr = longAttr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getBoolAttr() {
|
||||||
|
return boolAttr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBoolAttr(Boolean boolAttr) {
|
||||||
|
this.boolAttr = boolAttr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getStrList() {
|
||||||
|
return strList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStrList(List<String> strList) {
|
||||||
|
this.strList = strList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Integer> getIntList() {
|
||||||
|
return intList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIntList(List<Integer> intList) {
|
||||||
|
this.intList = intList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Data> getDataList() {
|
||||||
|
return dataList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDataList(List<Data> dataList) {
|
||||||
|
this.dataList = dataList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Data getNested() {
|
||||||
|
return nested;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNested(Data nested) {
|
||||||
|
this.nested = nested;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<List<String>> getDeepList() {
|
||||||
|
return deepList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeepList(List<List<String>> deepList) {
|
||||||
|
this.deepList = deepList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfig(Map<String, String> config) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getConfig() {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNestedConfig(Map<String, Map<String, Data>> nestedConfig) {
|
||||||
|
this.nestedConfig = nestedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Map<String, Data>> getNestedConfig() {
|
||||||
|
return nestedConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
integration/client-registration-cli/pom.xml → integration/client-cli/pom.xml
Executable file → Normal file
34
integration/client-registration-cli/pom.xml → integration/client-cli/pom.xml
Executable file → Normal file
|
@ -1,4 +1,3 @@
|
||||||
<?xml version="1.0"?>
|
|
||||||
<!--
|
<!--
|
||||||
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
|
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
~ and other contributors as indicated by the @author tags.
|
~ and other contributors as indicated by the @author tags.
|
||||||
|
@ -21,31 +20,18 @@
|
||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-integration-parent</artifactId>
|
<artifactId>keycloak-integration-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version></version>
|
<version>2.3.0-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
<name>Keycloak Client CLI</name>
|
||||||
|
<description/>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<artifactId>keycloak-client-registration-cli</artifactId>
|
<artifactId>keycloak-client-cli-parent</artifactId>
|
||||||
<name>Keycloak Client Registration CLI</name>
|
<packaging>pom</packaging>
|
||||||
<description/>
|
|
||||||
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.keycloak</groupId>
|
|
||||||
<artifactId>keycloak-core</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.keycloak</groupId>
|
|
||||||
<artifactId>keycloak-client-registration-api</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.httpcomponents</groupId>
|
|
||||||
<artifactId>httpclient</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.jboss.aesh</groupId>
|
|
||||||
<artifactId>aesh</artifactId>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
|
|
||||||
|
<modules>
|
||||||
|
<module>client-registration-cli</module>
|
||||||
|
<module>client-cli-dist</module>
|
||||||
|
</modules>
|
||||||
</project>
|
</project>
|
|
@ -1,72 +0,0 @@
|
||||||
package org.keycloak.client.registration.cli;
|
|
||||||
|
|
||||||
import org.jboss.aesh.cl.parser.CommandLineParserException;
|
|
||||||
import org.jboss.aesh.console.AeshConsole;
|
|
||||||
import org.jboss.aesh.console.AeshConsoleBuilder;
|
|
||||||
import org.jboss.aesh.console.Prompt;
|
|
||||||
import org.jboss.aesh.console.command.Command;
|
|
||||||
import org.jboss.aesh.console.command.CommandNotFoundException;
|
|
||||||
import org.jboss.aesh.console.command.registry.AeshCommandRegistryBuilder;
|
|
||||||
import org.jboss.aesh.console.settings.Settings;
|
|
||||||
import org.jboss.aesh.console.settings.SettingsBuilder;
|
|
||||||
import org.jboss.aesh.terminal.Color;
|
|
||||||
import org.jboss.aesh.terminal.TerminalColor;
|
|
||||||
import org.jboss.aesh.terminal.TerminalString;
|
|
||||||
import org.keycloak.client.registration.Auth;
|
|
||||||
import org.keycloak.client.registration.ClientRegistration;
|
|
||||||
import org.keycloak.client.registration.cli.commands.CreateCommand;
|
|
||||||
import org.keycloak.client.registration.cli.commands.ExitCommand;
|
|
||||||
import org.keycloak.client.registration.cli.commands.SetupCommand;
|
|
||||||
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
|
||||||
*/
|
|
||||||
public class ClientRegistrationCLI {
|
|
||||||
|
|
||||||
private static ClientRegistration reg;
|
|
||||||
|
|
||||||
public static void main(String[] args) throws CommandLineParserException, CommandNotFoundException {
|
|
||||||
reg = ClientRegistration.create().url("http://localhost:8080/auth/realms/master").build();
|
|
||||||
reg.auth(Auth.token("..."));
|
|
||||||
|
|
||||||
Context context = new Context();
|
|
||||||
|
|
||||||
List<Command> commands = new LinkedList<>();
|
|
||||||
commands.add(new SetupCommand(context));
|
|
||||||
commands.add(new CreateCommand(context));
|
|
||||||
commands.add(new ExitCommand(context));
|
|
||||||
|
|
||||||
SettingsBuilder builder = new SettingsBuilder().logging(true);
|
|
||||||
builder.enableMan(true).readInputrc(false);
|
|
||||||
|
|
||||||
Settings settings = builder.create();
|
|
||||||
|
|
||||||
AeshCommandRegistryBuilder commandRegistryBuilder = new AeshCommandRegistryBuilder();
|
|
||||||
for (Command c : commands) {
|
|
||||||
commandRegistryBuilder.command(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
AeshConsole aeshConsole = new AeshConsoleBuilder()
|
|
||||||
.commandRegistry(commandRegistryBuilder.create())
|
|
||||||
.settings(settings)
|
|
||||||
.prompt(new Prompt(new TerminalString("[clientreg]$ ",
|
|
||||||
new TerminalColor(Color.GREEN, Color.DEFAULT, Color.Intensity.BRIGHT))))
|
|
||||||
.create();
|
|
||||||
|
|
||||||
aeshConsole.start();
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
if (args.length > 0) {
|
|
||||||
CommandContainer command = registry.getCommand(args[0], null);
|
|
||||||
ParserGenerator.parseAndPopulate(command, args[0], Arrays.copyOfRange(args, 1, args.length));
|
|
||||||
}*/
|
|
||||||
|
|
||||||
//commandInvocation.getCommandRegistry().getAllCommandNames()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
package org.keycloak.client.registration.cli;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
|
||||||
import org.keycloak.client.registration.ClientRegistration;
|
|
||||||
import org.keycloak.util.SystemPropertiesJsonParserFactory;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
|
||||||
*/
|
|
||||||
public class Context {
|
|
||||||
|
|
||||||
private static final ObjectMapper mapper = new ObjectMapper(new SystemPropertiesJsonParserFactory());
|
|
||||||
static {
|
|
||||||
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
|
|
||||||
mapper.enable(SerializationFeature.INDENT_OUTPUT);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ClientRegistration reg;
|
|
||||||
|
|
||||||
public ClientRegistration getReg() {
|
|
||||||
return reg;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setReg(ClientRegistration reg) {
|
|
||||||
this.reg = reg;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> T readJson(InputStream bytes, Class<T> type) throws IOException {
|
|
||||||
return mapper.readValue(bytes, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
package org.keycloak.client.registration.cli.commands;
|
|
||||||
|
|
||||||
import org.jboss.aesh.cl.Arguments;
|
|
||||||
import org.jboss.aesh.cl.CommandDefinition;
|
|
||||||
import org.jboss.aesh.cl.Option;
|
|
||||||
import org.jboss.aesh.console.command.Command;
|
|
||||||
import org.jboss.aesh.console.command.CommandResult;
|
|
||||||
import org.jboss.aesh.console.command.invocation.CommandInvocation;
|
|
||||||
import org.jboss.aesh.io.Resource;
|
|
||||||
import org.keycloak.client.registration.ClientRegistrationException;
|
|
||||||
import org.keycloak.client.registration.cli.Context;
|
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
|
||||||
import org.keycloak.util.JsonSerialization;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
|
||||||
*/
|
|
||||||
@CommandDefinition(name="create", description = "[OPTIONS] FILE")
|
|
||||||
public class CreateCommand implements Command {
|
|
||||||
|
|
||||||
@Option(shortName = 'h', hasValue = false, description = "display this help and exit")
|
|
||||||
private boolean help;
|
|
||||||
|
|
||||||
@Arguments(description = "files or directories thats listed")
|
|
||||||
private List<Resource> arguments;
|
|
||||||
|
|
||||||
private Context context;
|
|
||||||
|
|
||||||
public CreateCommand(Context context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CommandResult execute(CommandInvocation commandInvocation) throws IOException, InterruptedException {
|
|
||||||
System.out.println(help);
|
|
||||||
|
|
||||||
|
|
||||||
if(help) {
|
|
||||||
commandInvocation.getShell().out().println(commandInvocation.getHelpInfo("create"));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
|
|
||||||
if(arguments != null) {
|
|
||||||
for(Resource f : arguments) {
|
|
||||||
System.out.println(f.getAbsolutePath());
|
|
||||||
ClientRepresentation rep = JsonSerialization.readValue(f.read(), ClientRepresentation.class);
|
|
||||||
try {
|
|
||||||
context.getReg().create(rep);
|
|
||||||
} catch (ClientRegistrationException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// reg.create();
|
|
||||||
|
|
||||||
return CommandResult.SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
package org.keycloak.client.registration.cli.commands;
|
|
||||||
|
|
||||||
import org.jboss.aesh.cl.CommandDefinition;
|
|
||||||
import org.jboss.aesh.console.command.Command;
|
|
||||||
import org.jboss.aesh.console.command.CommandResult;
|
|
||||||
import org.jboss.aesh.console.command.invocation.CommandInvocation;
|
|
||||||
import org.keycloak.client.registration.cli.Context;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
|
||||||
*/
|
|
||||||
@CommandDefinition(name="exit", description = "Exit the program")
|
|
||||||
public class ExitCommand implements Command {
|
|
||||||
|
|
||||||
private Context context;
|
|
||||||
|
|
||||||
public ExitCommand(Context context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CommandResult execute(CommandInvocation commandInvocation) throws IOException, InterruptedException {
|
|
||||||
commandInvocation.stop();
|
|
||||||
return CommandResult.SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
package org.keycloak.client.registration.cli.commands;
|
|
||||||
|
|
||||||
import org.jboss.aesh.cl.CommandDefinition;
|
|
||||||
import org.jboss.aesh.cl.Option;
|
|
||||||
import org.jboss.aesh.console.command.Command;
|
|
||||||
import org.jboss.aesh.console.command.CommandResult;
|
|
||||||
import org.jboss.aesh.console.command.invocation.CommandInvocation;
|
|
||||||
import org.keycloak.client.registration.cli.Context;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
|
||||||
*/
|
|
||||||
@CommandDefinition(name="setup", description = "")
|
|
||||||
public class SetupCommand implements Command {
|
|
||||||
|
|
||||||
@Option(shortName = 'h', hasValue = false, description = "display this help and exit")
|
|
||||||
private boolean help;
|
|
||||||
|
|
||||||
private Context context;
|
|
||||||
|
|
||||||
public SetupCommand(Context context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CommandResult execute(CommandInvocation commandInvocation) throws IOException, InterruptedException {
|
|
||||||
System.out.println(help);
|
|
||||||
|
|
||||||
if(help) {
|
|
||||||
commandInvocation.getShell().out().println(commandInvocation.getHelpInfo("create"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return CommandResult.SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private String promptForUsername(CommandInvocation invocation) throws InterruptedException {
|
|
||||||
invocation.print("username: ");
|
|
||||||
return invocation.getInputLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -33,5 +33,6 @@
|
||||||
<modules>
|
<modules>
|
||||||
<module>admin-client</module>
|
<module>admin-client</module>
|
||||||
<module>client-registration</module>
|
<module>client-registration</module>
|
||||||
|
<module>client-cli</module>
|
||||||
</modules>
|
</modules>
|
||||||
</project>
|
</project>
|
||||||
|
|
11
pom.xml
11
pom.xml
|
@ -1305,6 +1305,17 @@
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
<type>war</type>
|
<type>war</type>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-client-registration-cli</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-client-cli-dist</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<type>zip</type>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,8 @@
|
||||||
<description></description>
|
<description></description>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
|
<cli.log.output>false</cli.log.output>
|
||||||
|
<test.intermittent>false</test.intermittent>
|
||||||
<exclude.test>-</exclude.test>
|
<exclude.test>-</exclude.test>
|
||||||
<exclude.console>-</exclude.console>
|
<exclude.console>-</exclude.console>
|
||||||
<exclude.account>-</exclude.account>
|
<exclude.account>-</exclude.account>
|
||||||
|
@ -150,6 +152,29 @@
|
||||||
</executions>
|
</executions>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-dependency-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>unpack-client-cli-dist</id>
|
||||||
|
<phase>generate-test-resources</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>unpack</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<artifactItems>
|
||||||
|
<artifactItem>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-client-cli-dist</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<type>zip</type>
|
||||||
|
<outputDirectory>${containers.home}</outputDirectory>
|
||||||
|
</artifactItem>
|
||||||
|
</artifactItems>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
|
|
||||||
</build>
|
</build>
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
package org.keycloak.testsuite.cli;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:marko.strukelj@gmail.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public class ExecutionException extends RuntimeException {
|
||||||
|
|
||||||
|
private int exitCode = -1;
|
||||||
|
|
||||||
|
public ExecutionException(int exitCode) {
|
||||||
|
this.exitCode = exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExecutionException(String message, int exitCode) {
|
||||||
|
super(message);
|
||||||
|
this.exitCode = exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int exitCode() {
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return super.toString() + ", exitCode: " + exitCode;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,487 @@
|
||||||
|
package org.keycloak.testsuite.cli;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FilterOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.InterruptedIOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public class KcRegExec {
|
||||||
|
|
||||||
|
public static final String WORK_DIR = System.getProperty("user.dir") + "/target/containers/keycloak-client-tools";
|
||||||
|
|
||||||
|
public static final OsArch OS_ARCH = OsUtils.determineOSAndArch();
|
||||||
|
|
||||||
|
public static final String CMD = OS_ARCH.isWindows() ? "kcreg.bat" : "kcreg.sh";
|
||||||
|
|
||||||
|
private long waitTimeout = 30000;
|
||||||
|
|
||||||
|
private Process process;
|
||||||
|
|
||||||
|
private int exitCode = -1;
|
||||||
|
|
||||||
|
private boolean logStreams = Boolean.valueOf(System.getProperty("cli.log.output", "true"));
|
||||||
|
|
||||||
|
private boolean dumpStreams;
|
||||||
|
|
||||||
|
private String workDir = WORK_DIR;
|
||||||
|
|
||||||
|
private String env;
|
||||||
|
|
||||||
|
private String argsLine;
|
||||||
|
|
||||||
|
private ByteArrayOutputStream stdout = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
private ByteArrayOutputStream stderr = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
private InputStream stdin = new InteractiveInputStream();
|
||||||
|
|
||||||
|
private Throwable err;
|
||||||
|
|
||||||
|
private KcRegExec(String workDir, String argsLine, InputStream stdin) {
|
||||||
|
this(workDir, argsLine, null, stdin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private KcRegExec(String workDir, String argsLine, String env, InputStream stdin) {
|
||||||
|
if (workDir != null) {
|
||||||
|
this.workDir = workDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.argsLine = argsLine;
|
||||||
|
this.env = env;
|
||||||
|
|
||||||
|
if (stdin != null) {
|
||||||
|
this.stdin = stdin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder newBuilder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static KcRegExec execute(String args) {
|
||||||
|
return newBuilder()
|
||||||
|
.argsLine(args)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void execute() {
|
||||||
|
executeAsync();
|
||||||
|
if (err == null) {
|
||||||
|
waitCompletion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void executeAsync() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (OS_ARCH.isWindows()) {
|
||||||
|
String cmd = (env != null ? "set " + env + " & " : "") + "bin\\" + CMD + " " + fixQuotes(argsLine);
|
||||||
|
System.out.println("Executing: cmd.exe /c " + cmd);
|
||||||
|
process = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", cmd}, null, new File(workDir));
|
||||||
|
} else {
|
||||||
|
String cmd = (env != null ? env + " " : "") + "bin/" + CMD + " " + argsLine;
|
||||||
|
System.out.println("Executing: sh -c " + cmd);
|
||||||
|
process = Runtime.getRuntime().exec(new String[]{"sh", "-c", cmd}, null, new File(workDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
new StreamReaderThread(process.getInputStream(), logStreams ? new LoggingOutputStream("STDOUT", stdout) : stdout)
|
||||||
|
.start();
|
||||||
|
|
||||||
|
new StreamReaderThread(process.getErrorStream(), logStreams ? new LoggingOutputStream("STDERR", stderr) : stderr)
|
||||||
|
.start();
|
||||||
|
|
||||||
|
new StreamReaderThread(stdin, process.getOutputStream())
|
||||||
|
.start();
|
||||||
|
|
||||||
|
} catch (Throwable t) {
|
||||||
|
err = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String fixQuotes(String argsLine) {
|
||||||
|
argsLine = argsLine + " ";
|
||||||
|
argsLine = argsLine.replaceAll("\"", "\\\\\"");
|
||||||
|
argsLine = argsLine.replaceAll(" '", " \"");
|
||||||
|
argsLine = argsLine.replaceAll("' ", "\" ");
|
||||||
|
return argsLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void waitCompletion() {
|
||||||
|
|
||||||
|
//if (stdin instanceof InteractiveInputStream) {
|
||||||
|
// ((InteractiveInputStream) stdin).close();
|
||||||
|
//}
|
||||||
|
try {
|
||||||
|
if (process.waitFor(waitTimeout, TimeUnit.MILLISECONDS)) {
|
||||||
|
exitCode = process.exitValue();
|
||||||
|
if (exitCode != 0) {
|
||||||
|
dumpStreams = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (process.isAlive()) {
|
||||||
|
process.destroyForcibly();
|
||||||
|
}
|
||||||
|
throw new RuntimeException("Timeout after " + (waitTimeout / 1000) + " seconds.");
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
dumpStreams = true;
|
||||||
|
throw new RuntimeException("Interrupted ...", e);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
dumpStreams = true;
|
||||||
|
err = t;
|
||||||
|
} finally {
|
||||||
|
if (!logStreams && dumpStreams) try {
|
||||||
|
System.out.println("STDOUT: ");
|
||||||
|
copyStream(new ByteArrayInputStream(stdout.toByteArray()), System.out);
|
||||||
|
System.out.println("STDERR: ");
|
||||||
|
copyStream(new ByteArrayInputStream(stderr.toByteArray()), System.out);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int exitCode() {
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Throwable error() {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream stdout() {
|
||||||
|
return new ByteArrayInputStream(stdout.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> stdoutLines() {
|
||||||
|
return parseStreamAsLines(new ByteArrayInputStream(stdout.toByteArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String stdoutString() {
|
||||||
|
return new String(stdout.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream stderr() {
|
||||||
|
return new ByteArrayInputStream(stderr.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> stderrLines() {
|
||||||
|
return parseStreamAsLines(new ByteArrayInputStream(stderr.toByteArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String stderrString() {
|
||||||
|
return new String(stderr.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<String> parseStreamAsLines(InputStream stream) {
|
||||||
|
List<String> lines = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
|
||||||
|
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
lines.add(line);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Unexpected I/O error", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void waitForStdout(String content) {
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
while (System.currentTimeMillis() - start < waitTimeout) {
|
||||||
|
if (stdoutString().indexOf(content) != -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Thread.sleep(10);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException("Interrupted ...", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new RuntimeException("Timed while waiting for content to appear in stdout");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendToStdin(String s) {
|
||||||
|
if (stdin instanceof InteractiveInputStream) {
|
||||||
|
((InteractiveInputStream) stdin).pushBytes(s.getBytes());
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Can't push to stdin - not interactive");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class StreamReaderThread extends Thread {
|
||||||
|
|
||||||
|
private InputStream is;
|
||||||
|
private OutputStream os;
|
||||||
|
|
||||||
|
StreamReaderThread(InputStream is, OutputStream os) {
|
||||||
|
this.is = is;
|
||||||
|
this.os = os;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
copyStream(is, os);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Unexpected I/O error", e);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
os.close();
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
System.err.print("IGNORED: error while closing output stream: ");
|
||||||
|
ignored.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void copyStream(InputStream is, OutputStream os) throws IOException {
|
||||||
|
byte [] buf = new byte[8192];
|
||||||
|
|
||||||
|
try (InputStream iss = is) {
|
||||||
|
int c;
|
||||||
|
while ((c = iss.read(buf)) != -1) {
|
||||||
|
os.write(buf, 0, c);
|
||||||
|
os.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
|
||||||
|
private String workDir;
|
||||||
|
private String argsLine;
|
||||||
|
private InputStream stdin;
|
||||||
|
private String env;
|
||||||
|
private boolean dumpStreams;
|
||||||
|
|
||||||
|
public Builder workDir(String path) {
|
||||||
|
this.workDir = path;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder argsLine(String cmd) {
|
||||||
|
this.argsLine = cmd;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder stdin(InputStream is) {
|
||||||
|
this.stdin = is;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder env(String env) {
|
||||||
|
this.env = env;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder fullStreamDump() {
|
||||||
|
this.dumpStreams = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KcRegExec execute() {
|
||||||
|
KcRegExec exe = new KcRegExec(workDir, argsLine, env, stdin);
|
||||||
|
exe.dumpStreams = dumpStreams;
|
||||||
|
exe.execute();
|
||||||
|
return exe;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KcRegExec executeAsync() {
|
||||||
|
KcRegExec exe = new KcRegExec(workDir, argsLine, env, stdin);
|
||||||
|
exe.dumpStreams = dumpStreams;
|
||||||
|
exe.executeAsync();
|
||||||
|
return exe;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2014 Red Hat, Inc. and/or its affiliates.
|
||||||
|
*
|
||||||
|
* Licensed under the Eclipse Public License version 1.0, available at http://www.eclipse.org/legal/epl-v10.html
|
||||||
|
*/
|
||||||
|
package org.keycloak.testsuite.cli;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2014 Red Hat, Inc. and/or its affiliates.
|
||||||
|
*
|
||||||
|
* Licensed under the Eclipse Public License version 1.0, available at http://www.eclipse.org/legal/epl-v10.html
|
||||||
|
*/
|
||||||
|
package org.keycloak.testsuite.cli;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:marko.strukelj@gmail.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public class OsUtils {
|
||||||
|
|
||||||
|
public static OsArch determineOSAndArch() {
|
||||||
|
String os = System.getProperty("os.name").toLowerCase();
|
||||||
|
String arch = System.getProperty("os.arch");
|
||||||
|
|
||||||
|
//System.out.println("OS: " + os + ", architecture: " + 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,27 @@ package org.keycloak.testsuite;
|
||||||
|
|
||||||
import org.apache.commons.configuration.ConfigurationException;
|
import org.apache.commons.configuration.ConfigurationException;
|
||||||
import org.apache.commons.configuration.PropertiesConfiguration;
|
import org.apache.commons.configuration.PropertiesConfiguration;
|
||||||
|
import org.apache.http.ssl.SSLContexts;
|
||||||
|
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
|
import org.keycloak.testsuite.arquillian.TestContext;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.security.KeyManagementException;
|
||||||
|
import java.security.KeyStoreException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.ws.rs.NotFoundException;
|
||||||
import org.jboss.arquillian.container.test.api.RunAsClient;
|
import org.jboss.arquillian.container.test.api.RunAsClient;
|
||||||
import org.jboss.arquillian.drone.api.annotation.Drone;
|
import org.jboss.arquillian.drone.api.annotation.Drone;
|
||||||
import org.jboss.arquillian.graphene.page.Page;
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
|
@ -33,8 +54,6 @@ import org.keycloak.admin.client.resource.RealmResource;
|
||||||
import org.keycloak.admin.client.resource.RealmsResource;
|
import org.keycloak.admin.client.resource.RealmsResource;
|
||||||
import org.keycloak.admin.client.resource.UserResource;
|
import org.keycloak.admin.client.resource.UserResource;
|
||||||
import org.keycloak.admin.client.resource.UsersResource;
|
import org.keycloak.admin.client.resource.UsersResource;
|
||||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
|
||||||
import org.keycloak.common.util.Time;
|
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
||||||
|
@ -42,7 +61,6 @@ import org.keycloak.representations.idm.UserRepresentation;
|
||||||
import org.keycloak.testsuite.admin.ApiUtil;
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
|
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
|
||||||
import org.keycloak.testsuite.arquillian.SuiteContext;
|
import org.keycloak.testsuite.arquillian.SuiteContext;
|
||||||
import org.keycloak.testsuite.arquillian.TestContext;
|
|
||||||
import org.keycloak.testsuite.auth.page.AuthRealm;
|
import org.keycloak.testsuite.auth.page.AuthRealm;
|
||||||
import org.keycloak.testsuite.auth.page.AuthServer;
|
import org.keycloak.testsuite.auth.page.AuthServer;
|
||||||
import org.keycloak.testsuite.auth.page.AuthServerContextRoot;
|
import org.keycloak.testsuite.auth.page.AuthServerContextRoot;
|
||||||
|
@ -56,16 +74,6 @@ import org.keycloak.testsuite.util.TestEventsLogger;
|
||||||
import org.keycloak.testsuite.util.WaitUtils;
|
import org.keycloak.testsuite.util.WaitUtils;
|
||||||
import org.openqa.selenium.WebDriver;
|
import org.openqa.selenium.WebDriver;
|
||||||
|
|
||||||
import javax.ws.rs.NotFoundException;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import static org.keycloak.testsuite.admin.Users.setPasswordFor;
|
import static org.keycloak.testsuite.admin.Users.setPasswordFor;
|
||||||
import static org.keycloak.testsuite.auth.page.AuthRealm.ADMIN;
|
import static org.keycloak.testsuite.auth.page.AuthRealm.ADMIN;
|
||||||
import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER;
|
import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER;
|
||||||
|
@ -124,8 +132,12 @@ public abstract class AbstractKeycloakTest {
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void beforeAbstractKeycloakTest() throws Exception {
|
public void beforeAbstractKeycloakTest() throws Exception {
|
||||||
|
SSLContext ssl = null;
|
||||||
|
if ("true".equals(System.getProperty("auth.server.ssl.required"))) {
|
||||||
|
ssl = getSSLContextWithTrustore(new File("src/test/resources/keystore/keycloak.truststore"), "secret");
|
||||||
|
}
|
||||||
adminClient = Keycloak.getInstance(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth",
|
adminClient = Keycloak.getInstance(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth",
|
||||||
MASTER, ADMIN, ADMIN, Constants.ADMIN_CLI_CLIENT_ID);
|
MASTER, ADMIN, ADMIN, Constants.ADMIN_CLI_CLIENT_ID, null, ssl);
|
||||||
|
|
||||||
getTestingClient();
|
getTestingClient();
|
||||||
|
|
||||||
|
@ -366,4 +378,14 @@ public abstract class AbstractKeycloakTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static SSLContext getSSLContextWithTrustore(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();
|
||||||
|
return theContext;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,539 @@
|
||||||
|
package org.keycloak.testsuite.cli.registration;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.keycloak.admin.client.resource.ClientInitialAccessResource;
|
||||||
|
import org.keycloak.admin.client.resource.ClientRegistrationTrustedHostResource;
|
||||||
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
|
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
|
||||||
|
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
|
||||||
|
import org.keycloak.client.registration.cli.config.ConfigData;
|
||||||
|
import org.keycloak.client.registration.cli.config.FileConfigHandler;
|
||||||
|
import org.keycloak.client.registration.cli.config.RealmConfigData;
|
||||||
|
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
|
||||||
|
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
|
||||||
|
import org.keycloak.representations.idm.ClientRegistrationTrustedHostRepresentation;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyManager;
|
||||||
|
import org.keycloak.services.clientregistration.policy.RegistrationAuth;
|
||||||
|
import org.keycloak.services.clientregistration.policy.impl.TrustedHostClientRegistrationPolicyFactory;
|
||||||
|
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||||
|
import org.keycloak.testsuite.cli.KcRegExec;
|
||||||
|
import org.keycloak.testsuite.util.ClientBuilder;
|
||||||
|
import org.keycloak.testsuite.util.UserBuilder;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
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.KcRegExec.execute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public abstract class AbstractCliTest extends AbstractKeycloakTest {
|
||||||
|
|
||||||
|
protected String serverUrl = isAuthServerSSL() ?
|
||||||
|
"https://localhost:" + getAuthServerHttpsPort() + "/auth" :
|
||||||
|
"http://localhost:" + getAuthServerHttpPort() + "/auth";
|
||||||
|
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void deleteDefaultConfig() {
|
||||||
|
getDefaultConfigFilePath().delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean runIntermittentlyFailingTests() {
|
||||||
|
return "true".equals(System.getProperty("test.intermittent"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isAuthServerSSL() {
|
||||||
|
return "true".equals(System.getProperty("auth.server.ssl.required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
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") + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static File getDefaultConfigFilePath() {
|
||||||
|
return new File(System.getProperty("user.home") + "/.keycloak/kcreg.config");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@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("manage-clients"));
|
||||||
|
admin.setClientRoles(clientRoles);
|
||||||
|
realmRepresentation.getUsers().add(admin);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// create client with service account to use Signed JWT credentials with
|
||||||
|
ClientRepresentation regClient = ClientBuilder.create()
|
||||||
|
.clientId("reg-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, "reg-cli-jwt");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// create client to use with user account - enable direct grants
|
||||||
|
regClient = ClientBuilder.create()
|
||||||
|
.clientId("reg-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("reg-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, "reg-cli-secret");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// create client to use with user account - enable direct grants
|
||||||
|
regClient = ClientBuilder.create()
|
||||||
|
.clientId("reg-cli-secret-direct")
|
||||||
|
.secret("password")
|
||||||
|
.authenticatorType(ClientIdAndSecretAuthenticator.PROVIDER_ID)
|
||||||
|
.directAccessGrants()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
realmRepresentation.getClients().add(regClient);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void loginAsUser(File configFile, String server, String realm, String user, String password) {
|
||||||
|
|
||||||
|
KcRegExec exe = 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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(RealmConfigData data1, RealmConfigData data2, String ... excluded) {
|
||||||
|
assertFieldsEqualWithExclusions(null, null, data1, data2, 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 + "initialToken";
|
||||||
|
if (!exclusions.contains(ekey)) {
|
||||||
|
Assert.assertEquals(ekey, data1.getInitialToken(), data2.getInitialToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
ekey = pfix + "clients";
|
||||||
|
if (!exclusions.contains(ekey)) {
|
||||||
|
Map<String, String> clients1 = data1.getClients();
|
||||||
|
Map<String, String> clients2 = data2.getClients();
|
||||||
|
|
||||||
|
Iterator<Map.Entry<String, String>> cit1 = clients1.entrySet().iterator();
|
||||||
|
Iterator<Map.Entry<String, String>> cit2 = clients2.entrySet().iterator();
|
||||||
|
|
||||||
|
while (cit1.hasNext() || cit2.hasNext()) {
|
||||||
|
Map.Entry<String, String> ckey1 = cit1.hasNext() ? cit1.next() : null;
|
||||||
|
Map.Entry<String, String> ckey2 = cit2.hasNext() ? cit2.next() : null;
|
||||||
|
|
||||||
|
String ckey = ekey + "." + (ckey1 != null ? ckey1.getKey() : ckey2.getKey());
|
||||||
|
if (!exclusions.contains(ckey)) {
|
||||||
|
Assert.assertNotNull(ckey + " left not null", ckey1);
|
||||||
|
Assert.assertNotNull(ckey + " right not null", ckey2);
|
||||||
|
Assert.assertEquals(ckey, ckey1.getKey(), ckey2.getKey());
|
||||||
|
Assert.assertEquals(ckey + " value", ckey1.getValue(), ckey2.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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("manage-clients"));
|
||||||
|
|
||||||
|
account.setClientRoles(clientRoles);
|
||||||
|
|
||||||
|
realm.getUsers().add(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void waitFor(long millis) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(millis);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException("Interrupted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FileConfigHandler initCustomConfigFile() {
|
||||||
|
String filename = UUID.randomUUID().toString() + ".config";
|
||||||
|
File cfgFile = new File(KcRegExec.WORK_DIR + "/" + filename);
|
||||||
|
FileConfigHandler handler = new FileConfigHandler();
|
||||||
|
handler.setConfigFile(cfgFile.getAbsolutePath());
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(KcRegExec.WORK_DIR + "/" + filename);
|
||||||
|
if (content != null) {
|
||||||
|
OutputStream os = new FileOutputStream(file);
|
||||||
|
os.write(content.getBytes(Charset.forName("iso_8859_1")));
|
||||||
|
os.close();
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
String issueInitialAccessToken(String realm) {
|
||||||
|
ClientInitialAccessResource resource = adminClient.realm(realm).clientInitialAccess();
|
||||||
|
|
||||||
|
ClientInitialAccessCreatePresentation rep = new ClientInitialAccessCreatePresentation();
|
||||||
|
rep.setCount(10);
|
||||||
|
rep.setExpiration(100);
|
||||||
|
|
||||||
|
ClientInitialAccessPresentation response = resource.create(rep);
|
||||||
|
|
||||||
|
String token = response.getToken();
|
||||||
|
Assert.assertNotNull("Issued initial access token not null", token);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ComponentRepresentation findPolicyByProviderAndAuth(String realm, String providerId, String authType) {
|
||||||
|
// Change the policy to avoid checking hosts
|
||||||
|
List<ComponentRepresentation> reps = adminClient.realm(realm).components().query(realm, ClientRegistrationPolicy.class.getName());
|
||||||
|
for (ComponentRepresentation rep : reps) {
|
||||||
|
if (rep.getSubType().equals(authType) && rep.getProviderId().equals(providerId)) {
|
||||||
|
return rep;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addLocalhostToAllowedHosts(String realm) {
|
||||||
|
RealmResource realmResource = adminClient.realm(realm);
|
||||||
|
String anonPolicy = ClientRegistrationPolicyManager.getComponentTypeKey(RegistrationAuth.ANONYMOUS);
|
||||||
|
|
||||||
|
ComponentRepresentation trustedHostRep = findPolicyByProviderAndAuth(realm, TrustedHostClientRegistrationPolicyFactory.PROVIDER_ID, anonPolicy);
|
||||||
|
trustedHostRep.getConfig().putSingle(TrustedHostClientRegistrationPolicyFactory.TRUSTED_HOSTS, "localhost");
|
||||||
|
realmResource.components().component(trustedHostRep.getId()).update(trustedHostRep);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
KcRegExec exe = execute("create --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());
|
||||||
|
Assert.assertNotNull("registrationAccessToken not null", client.getRegistrationAccessToken());
|
||||||
|
|
||||||
|
long lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
|
||||||
|
Assert.assertEquals("config file not modified", lastModified, lastModified2);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
exe = execute("get test-client --no-config --server " + serverUrl + " --realm test " + credentials + " " + extraOptions);
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
|
||||||
|
ClientRepresentation client2 = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
|
||||||
|
Assert.assertEquals("clientId", "test-client", client2.getClientId());
|
||||||
|
|
||||||
|
// we did not provide a token, thus no registrationAccessToken is present
|
||||||
|
Assert.assertNull("registrationAccessToken is null", client2.getRegistrationAccessToken());
|
||||||
|
|
||||||
|
lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
|
||||||
|
Assert.assertEquals("config file not modified", lastModified, lastModified2);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// the token works even though an intermediary invocation was performed,
|
||||||
|
// because the previous invocation didn't use a registration access token
|
||||||
|
exe = execute("get test-client --no-config --server " + serverUrl + " --realm test " + extraOptions + " -t " + client.getRegistrationAccessToken());
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
|
||||||
|
ClientRepresentation client3 = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
|
||||||
|
Assert.assertEquals("clientId", "test-client", client3.getClientId());
|
||||||
|
|
||||||
|
Assert.assertNotEquals("registrationAccessToken in returned json is different than one returned by create",
|
||||||
|
client.getRegistrationAccessToken(), client3.getRegistrationAccessToken());
|
||||||
|
|
||||||
|
lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
|
||||||
|
Assert.assertEquals("config file not modified", lastModified, lastModified2);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
exe = execute("update test-client --no-config --server " + serverUrl + " --realm test " +
|
||||||
|
credentials + " " + extraOptions + " -s enabled=false -o --unsafe");
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
|
||||||
|
ClientRepresentation client4 = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
|
||||||
|
Assert.assertEquals("clientId", "test-client", client4.getClientId());
|
||||||
|
Assert.assertFalse("enabled", client4.isEnabled());
|
||||||
|
|
||||||
|
Assert.assertNull("registrationAccessToken in null", client4.getRegistrationAccessToken());
|
||||||
|
|
||||||
|
lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
|
||||||
|
Assert.assertEquals("config file not modified", lastModified, lastModified2);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
exe = execute("update test-client --no-config --server " + serverUrl + " --realm test " + extraOptions +
|
||||||
|
" -s enabled=true -o -t " + client3.getRegistrationAccessToken());
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
|
||||||
|
ClientRepresentation client5 = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
|
||||||
|
Assert.assertEquals("clientId", "test-client", client5.getClientId());
|
||||||
|
Assert.assertTrue("enabled", client5.isEnabled());
|
||||||
|
|
||||||
|
Assert.assertNotEquals("registrationAccessToken in returned json is different than one returned by get",
|
||||||
|
client3.getRegistrationAccessToken(), client5.getRegistrationAccessToken());
|
||||||
|
|
||||||
|
lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
|
||||||
|
Assert.assertEquals("config file not modified", lastModified, lastModified2);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
exe = execute("delete test-client --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 test-client --no-config --server " + serverUrl + " --realm test " + credentials + " " + extraOptions);
|
||||||
|
|
||||||
|
assertExitCodeAndStreamSizes(exe, 1, 0, 2);
|
||||||
|
Assert.assertEquals("error message", "Client not found [invalid_request]", exe.stderrLines().get(1));
|
||||||
|
|
||||||
|
lastModified2 = configFile.exists() ? configFile.lastModified() : 0;
|
||||||
|
Assert.assertEquals("config file not modified", lastModified, lastModified2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void assertExitCodeAndStreamSizes(KcRegExec exe, int exitCode, int stdOutLineCount, int stdErrLineCount) {
|
||||||
|
Assert.assertEquals("exitCode == " + exitCode, exitCode, exe.exitCode());
|
||||||
|
Assert.assertTrue("stdout output has " + stdOutLineCount + " lines", exe.stdoutLines().size() == stdOutLineCount);
|
||||||
|
Assert.assertTrue("stderr output has " + stdErrLineCount + " lines", exe.stderrLines().size() == stdErrLineCount);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
package org.keycloak.testsuite.cli.registration;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.client.registration.cli.config.FileConfigHandler;
|
||||||
|
import org.keycloak.testsuite.cli.KcRegExec;
|
||||||
|
import org.keycloak.testsuite.util.TempFileResource;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.EOL;
|
||||||
|
import static org.keycloak.testsuite.cli.KcRegExec.execute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public class KcRegConfigTest extends AbstractCliTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRegistrationToken() throws IOException {
|
||||||
|
|
||||||
|
FileConfigHandler handler = initCustomConfigFile();
|
||||||
|
|
||||||
|
try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
|
||||||
|
|
||||||
|
// forget --server
|
||||||
|
KcRegExec exe = execute("config registration-token --config '" + configFile.getName() + "' ");
|
||||||
|
assertExitCodeAndStreamSizes(exe, 1, 0, 1);
|
||||||
|
Assert.assertEquals("error message", "Required option not specified: --server", exe.stderrLines().get(0));
|
||||||
|
|
||||||
|
// forget --realm
|
||||||
|
exe = execute("config registration-token --config '" + configFile.getName() + "' --server http://localhost:8080/auth");
|
||||||
|
assertExitCodeAndStreamSizes(exe, 1, 0, 1);
|
||||||
|
Assert.assertEquals("error message", "Required option not specified: --realm", exe.stderrLines().get(0));
|
||||||
|
|
||||||
|
// forget --client
|
||||||
|
exe = execute("config registration-token --config '" + configFile.getName() + "' --server http://localhost:8080/auth --realm test");
|
||||||
|
assertExitCodeAndStreamSizes(exe, 1, 0, 1);
|
||||||
|
Assert.assertEquals("error message", "Required option not specified: --client", exe.stderrLines().get(0));
|
||||||
|
|
||||||
|
// specify token on cmdline
|
||||||
|
exe = execute("config registration-token --config '" + configFile.getName() + "' --server http://localhost:8080/auth --realm test --client my_client NEWTOKEN");
|
||||||
|
assertExitCodeAndStreamSizes(exe, 0, 0, 0);
|
||||||
|
|
||||||
|
if (runIntermittentlyFailingTests()) {
|
||||||
|
// don't specify token - must be prompted for it
|
||||||
|
exe = KcRegExec.newBuilder()
|
||||||
|
.argsLine("config registration-token --config '" + configFile.getName() + "' --server http://localhost:8080/auth --realm test --client my_client")
|
||||||
|
.executeAsync();
|
||||||
|
|
||||||
|
exe.waitForStdout("Enter Registration Access Token:");
|
||||||
|
exe.sendToStdin("NEWTOKEN" + EOL);
|
||||||
|
exe.waitCompletion();
|
||||||
|
assertExitCodeAndStreamSizes(exe, 0, 1, 0);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
System.out.println("TEST SKIPPED PARTIALLY - This test currently suffers from intermittent failures. Use -Dtest.intermittent=true to run it in full.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete non-existent token
|
||||||
|
exe = execute("config registration-token --config '" + configFile.getName() + "' --server http://localhost:8080/auth --realm test --client nonexistent --delete");
|
||||||
|
assertExitCodeAndStreamSizes(exe, 0, 0, 0);
|
||||||
|
|
||||||
|
// delete token
|
||||||
|
exe = execute("config registration-token --config '" + configFile.getName() + "' --server http://localhost:8080/auth --realm test --client my_client --delete");
|
||||||
|
assertExitCodeAndStreamSizes(exe, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,231 @@
|
||||||
|
package org.keycloak.testsuite.cli.registration;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.client.registration.cli.config.ConfigData;
|
||||||
|
import org.keycloak.client.registration.cli.config.FileConfigHandler;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||||
|
import org.keycloak.testsuite.cli.KcRegExec;
|
||||||
|
import org.keycloak.testsuite.util.TempFileResource;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import static org.keycloak.testsuite.cli.KcRegExec.execute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public class KcRegCreateTest extends AbstractCliTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateWithRealmOverride() throws IOException {
|
||||||
|
|
||||||
|
FileConfigHandler handler = initCustomConfigFile();
|
||||||
|
|
||||||
|
try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
|
||||||
|
|
||||||
|
// authenticate as a regular user against one realm
|
||||||
|
KcRegExec exe = execute("config credentials -x --config '" + configFile.getName() +
|
||||||
|
"' --server " + serverUrl + " --realm master --user admin --password admin");
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
|
||||||
|
// use initial token of another realm with server, and realm override
|
||||||
|
String token = issueInitialAccessToken("test");
|
||||||
|
exe = execute("create --config '" + configFile.getName() + "' --server " + serverUrl + " --realm test -s clientId=my_first_client -t " + token);
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateThoroughly() throws IOException {
|
||||||
|
|
||||||
|
FileConfigHandler handler = initCustomConfigFile();
|
||||||
|
|
||||||
|
try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
|
||||||
|
// set initial access token in config
|
||||||
|
String token = issueInitialAccessToken("test");
|
||||||
|
|
||||||
|
final String realm = "test";
|
||||||
|
|
||||||
|
KcRegExec exe = execute("config initial-token -x --config '" + configFile.getName() +
|
||||||
|
"' --server " + serverUrl + " --realm " + realm + " " + token);
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
|
||||||
|
// check that current server, realm, and initial token are saved in the file
|
||||||
|
ConfigData config = handler.loadConfig();
|
||||||
|
Assert.assertEquals("Config serverUrl", serverUrl, config.getServerUrl());
|
||||||
|
Assert.assertEquals("Config realm", realm, config.getRealm());
|
||||||
|
Assert.assertEquals("Config initial access token", token, config.ensureRealmConfigData(serverUrl, realm).getInitialToken());
|
||||||
|
|
||||||
|
// 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" +
|
||||||
|
" \"protocol\": \"leycloak-oidc\",\n" +
|
||||||
|
" \"webOrigins\": [\"http://localhost:8980/myapp\"],\n" +
|
||||||
|
" \"consentRequired\": false,\n" +
|
||||||
|
" \"baseUrl\": \"http://localhost:8980/myapp\",\n" +
|
||||||
|
" \"rootUrl\": \"http://localhost:8980/myapp\",\n" +
|
||||||
|
" \"bearerOnly\": true,\n" +
|
||||||
|
" \"standardFlowEnabled\": true\n" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
try (TempFileResource tmpFile = new TempFileResource(initTempFile(".json", content))) {
|
||||||
|
|
||||||
|
exe = execute("create --config '" + configFile.getName() + "' -o -f - < '" + tmpFile.getName() + "'");
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
Assert.assertTrue("stderr is empty", exe.stderrLines().isEmpty());
|
||||||
|
|
||||||
|
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("protocol", "leycloak-oidc", client.getProtocol());
|
||||||
|
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("rootUrl", "http://localhost:8980/myapp", client.getRootUrl());
|
||||||
|
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 --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 protocol=keycloak-oidc -s 'webOrigins=[\"http://localhost:8980/myapp2\"]'" +
|
||||||
|
" -s baseUrl=http://localhost:8980/myapp2 -s rootUrl=http://localhost:8980/myapp2");
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
Assert.assertTrue("stderr is empty", exe.stderrLines().isEmpty());
|
||||||
|
|
||||||
|
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("protocol", "keycloak-oidc", client2.getProtocol());
|
||||||
|
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());
|
||||||
|
|
||||||
|
|
||||||
|
// check that using an invalid attribute key is not ignored
|
||||||
|
exe = execute("create --config '" + configFile.getName() + "' -o -f '" + tmpFile.getName() + "' -s client_id=my_client3");
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 1", 1, exe.exitCode());
|
||||||
|
Assert.assertEquals("stderr has one line", 1, exe.stderrLines().size());
|
||||||
|
Assert.assertEquals("Failed to set attribute 'client_id' on document type 'default'", exe.stderrLines().get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// simple create, output an id
|
||||||
|
exe = execute("create --config '" + configFile.getName() + "' -i -s clientId=my_client3");
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
Assert.assertEquals("stderr is empty", 0, exe.stderrLines().size());
|
||||||
|
|
||||||
|
Assert.assertEquals("stdout has 1 line", 1, exe.stdoutLines().size());
|
||||||
|
Assert.assertEquals("only clientId returned", "my_client3", exe.stdoutLines().get(0));
|
||||||
|
|
||||||
|
// simple create, default output
|
||||||
|
exe = execute("create --config '" + configFile.getName() + "' -s clientId=my_client4");
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
Assert.assertEquals("stderr has 1 line", 1, exe.stderrLines().size());
|
||||||
|
Assert.assertEquals("only clientId returned", "Registered new client with client_id 'my_client4'", exe.stderrLines().get(0));
|
||||||
|
|
||||||
|
Assert.assertEquals("stdout is empty", 0, exe.stdoutLines().size());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// create using oidc endpoint - autodetect format
|
||||||
|
content = " {\n" +
|
||||||
|
" \"redirect_uris\" : [ \"http://localhost:8980/myapp/*\" ],\n" +
|
||||||
|
" \"grant_types\" : [ \"authorization_code\", \"client_credentials\", \"refresh_token\" ],\n" +
|
||||||
|
" \"response_types\" : [ \"code\", \"none\" ],\n" +
|
||||||
|
" \"client_name\" : \"My Client App\",\n" +
|
||||||
|
" \"client_uri\" : \"http://localhost:8980/myapp\"\n" +
|
||||||
|
" }";
|
||||||
|
|
||||||
|
try (TempFileResource tmpFile = new TempFileResource(initTempFile(".json", content))) {
|
||||||
|
|
||||||
|
exe = execute("create --config '" + configFile.getName() + "' -s 'client_name=My Client App V' " +
|
||||||
|
" -s 'redirect_uris=[\"http://localhost:8980/myapp5/*\"]' -s client_uri=http://localhost:8980/myapp5" +
|
||||||
|
" -o -f - < '" + tmpFile.getName() + "'");
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
Assert.assertTrue("stderr is empty", exe.stderrLines().isEmpty());
|
||||||
|
|
||||||
|
OIDCClientRepresentation client = JsonSerialization.readValue(exe.stdout(), OIDCClientRepresentation.class);
|
||||||
|
|
||||||
|
Assert.assertNotNull("clientId", client.getClientId());
|
||||||
|
Assert.assertEquals("redirect_uris", Arrays.asList("http://localhost:8980/myapp5/*"), client.getRedirectUris());
|
||||||
|
Assert.assertEquals("grant_types", Arrays.asList("authorization_code", "client_credentials", "refresh_token"), client.getGrantTypes());
|
||||||
|
Assert.assertEquals("response_types", Arrays.asList("code", "none"), client.getResponseTypes());
|
||||||
|
Assert.assertEquals("client_name", "My Client App V", client.getClientName());
|
||||||
|
Assert.assertEquals("client_uri", "http://localhost:8980/myapp5", client.getClientUri());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// try use incompatible endpoint override
|
||||||
|
exe = execute("create --config '" + configFile.getName() + "' -e default -f '" + tmpFile.getName() + "'");
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 1", 1, exe.exitCode());
|
||||||
|
Assert.assertFalse("stderr not empty", exe.stderrLines().isEmpty());
|
||||||
|
Assert.assertEquals("Error message", "Attribute 'redirect_uris' not supported on document type 'default'", exe.stderrLines().get(0));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// test create saml formated xml - format autodetection
|
||||||
|
File samlSpMetaFile = new File(System.getProperty("user.dir") + "/src/test/resources/cli/kcreg/saml-sp-metadata.xml");
|
||||||
|
Assert.assertTrue("saml-sp-metadata.xml exists", samlSpMetaFile.isFile());
|
||||||
|
|
||||||
|
exe = execute("create --config '" + configFile.getName() + "' -o -f - < '" + samlSpMetaFile.getAbsolutePath() + "'");
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
Assert.assertTrue("stderr is empty", exe.stderrLines().isEmpty());
|
||||||
|
|
||||||
|
ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
|
||||||
|
Assert.assertNotNull("id", client.getId());
|
||||||
|
Assert.assertEquals("clientId", "http://localhost:8080/sales-post-enc/", client.getClientId());
|
||||||
|
Assert.assertEquals("redirectUris", Arrays.asList("http://localhost:8081/sales-post-enc/saml"), client.getRedirectUris());
|
||||||
|
Assert.assertEquals("attributes.saml_name_id_format", "username", client.getAttributes().get("saml_name_id_format"));
|
||||||
|
Assert.assertEquals("attributes.saml_assertion_consumer_url_post", "http://localhost:8081/sales-post-enc/saml", client.getAttributes().get("saml_assertion_consumer_url_post"));
|
||||||
|
Assert.assertEquals("attributes.saml.signature.algorithm", "RSA_SHA256", client.getAttributes().get("saml.signature.algorithm"));
|
||||||
|
|
||||||
|
|
||||||
|
// delete initial token
|
||||||
|
exe = execute("config initial-token --config '" + configFile.getName() + "' --server " + serverUrl + " --realm " + realm + " --delete");
|
||||||
|
assertExitCodeAndStreamSizes(exe, 0, 0, 0);
|
||||||
|
|
||||||
|
config = handler.loadConfig();
|
||||||
|
Assert.assertNull("initial token == null", config.ensureRealmConfigData(serverUrl, realm).getInitialToken());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,512 @@
|
||||||
|
package org.keycloak.testsuite.cli.registration;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.client.registration.cli.config.ConfigData;
|
||||||
|
import org.keycloak.client.registration.cli.config.FileConfigHandler;
|
||||||
|
import org.keycloak.client.registration.cli.config.RealmConfigData;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.testsuite.cli.KcRegExec;
|
||||||
|
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.registration.cli.util.OsUtil.EOL;
|
||||||
|
import static org.keycloak.testsuite.cli.KcRegExec.execute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public class KcRegTest extends AbstractCliTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNoArgs() {
|
||||||
|
/*
|
||||||
|
* Test most basic execution that returns the initial help
|
||||||
|
*/
|
||||||
|
KcRegExec exe = execute("");
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
|
||||||
|
List<String> lines = exe.stdoutLines();
|
||||||
|
Assert.assertTrue("stdout output not empty", lines.size() > 0);
|
||||||
|
Assert.assertEquals("stdout first line", "Keycloak Client Registration CLI", lines.get(0));
|
||||||
|
Assert.assertEquals("stdout one but last line", "Use '" + KcRegExec.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));
|
||||||
|
|
||||||
|
lines = exe.stderrLines();
|
||||||
|
Assert.assertTrue("stderr output empty", lines.size() == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBadCommand() {
|
||||||
|
/*
|
||||||
|
* Test most basic execution with non-existent command
|
||||||
|
*/
|
||||||
|
KcRegExec exe = execute("nonexistent");
|
||||||
|
|
||||||
|
assertExitCodeAndStreamSizes(exe, 1, 0, 1);
|
||||||
|
Assert.assertEquals("stderr first line", "Unknown command: nonexistent", exe.stderrLines().get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBadOptionInPlaceOfCommand() {
|
||||||
|
/*
|
||||||
|
* Test most basic execution with non-existent option
|
||||||
|
*/
|
||||||
|
KcRegExec exe = 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
KcRegExec exe = execute("get my_client --nonexistent");
|
||||||
|
|
||||||
|
assertExitCodeAndStreamSizes(exe, 1, 0, 1);
|
||||||
|
Assert.assertEquals("stderr first line", "Invalid option: --nonexistent", exe.stderrLines().get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCredentialsServerAndRealmWithDefaultConfig() {
|
||||||
|
/*
|
||||||
|
* Test without --server specified
|
||||||
|
*/
|
||||||
|
KcRegExec exe = execute("config credentials --server " + serverUrl + " --realm master");
|
||||||
|
|
||||||
|
assertExitCodeAndStreamSizes(exe, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCredentialsNoServerWithDefaultConfig() {
|
||||||
|
/*
|
||||||
|
* Test without --server specified
|
||||||
|
*/
|
||||||
|
KcRegExec exe = execute("config credentials --realm master --user admin --password admin");
|
||||||
|
|
||||||
|
assertExitCodeAndStreamSizes(exe, 1, 0, 1);
|
||||||
|
Assert.assertEquals("stderr first line", "Required option not specified: --server", exe.stderrLines().get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCredentialsNoRealmWithDefaultConfig() {
|
||||||
|
/*
|
||||||
|
* Test without --server specified
|
||||||
|
*/
|
||||||
|
KcRegExec exe = execute("config credentials --server " + serverUrl + " --user admin --password admin");
|
||||||
|
|
||||||
|
assertExitCodeAndStreamSizes(exe, 1, 0, 1);
|
||||||
|
Assert.assertEquals("stderr first line", "Required option not specified: --realm", exe.stderrLines().get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUserLoginWithDefaultConfig() {
|
||||||
|
/*
|
||||||
|
* Test most basic user login, using the default admin-cli as a client
|
||||||
|
*/
|
||||||
|
KcRegExec exe = 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
KcRegExec exe = KcRegExec.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(KcRegExec.WORK_DIR + "/" + UUID.randomUUID().toString() + ".tmp");
|
||||||
|
try {
|
||||||
|
FileOutputStream tmpos = new FileOutputStream(tmpFile);
|
||||||
|
tmpos.write("admin".getBytes());
|
||||||
|
tmpos.write(EOL.getBytes());
|
||||||
|
tmpos.close();
|
||||||
|
|
||||||
|
exe = 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
|
||||||
|
KcRegExec exe = KcRegExec.newBuilder()
|
||||||
|
.argsLine("config credentials --server " + serverUrl + " --realm test --client reg-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-reg-cli-secret of realm test", exe.stderrLines().get(0));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run the test one more time with stdin redirect
|
||||||
|
*/
|
||||||
|
File tmpFile = new File(KcRegExec.WORK_DIR + "/" + UUID.randomUUID().toString() + ".tmp");
|
||||||
|
try {
|
||||||
|
FileOutputStream tmpos = new FileOutputStream(tmpFile);
|
||||||
|
tmpos.write("password".getBytes());
|
||||||
|
tmpos.write(EOL.getBytes());
|
||||||
|
tmpos.close();
|
||||||
|
|
||||||
|
exe = KcRegExec.newBuilder()
|
||||||
|
.argsLine("config credentials --server " + serverUrl + " --realm test --client reg-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-reg-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 {
|
||||||
|
KcRegExec exe = 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());
|
||||||
|
Assert.assertTrue("clients is empty", realmcfg.getClients().isEmpty());
|
||||||
|
|
||||||
|
} 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())) {
|
||||||
|
|
||||||
|
KcRegExec exe = execute("config credentials --server " + serverUrl +
|
||||||
|
" --realm master --user admin --password admin --config '" + configFile.getName() + "'");
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
|
||||||
|
// remember the state of config file
|
||||||
|
ConfigData config1 = handler.loadConfig();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
exe = execute("create --config '" + configFile.getName() + "' -s clientId=test-client -o");
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
|
||||||
|
// check changes to config file
|
||||||
|
ConfigData config2 = handler.loadConfig();
|
||||||
|
assertFieldsEqualWithExclusions(config1, config2, "endpoints." + serverUrl + ".master.clients.test-client");
|
||||||
|
|
||||||
|
// check that registration access token is now set
|
||||||
|
Assert.assertNotNull(config2.sessionRealmConfigData().getClients().get("test-client"));
|
||||||
|
|
||||||
|
|
||||||
|
ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
|
||||||
|
Assert.assertEquals("clientId", "test-client", client.getClientId());
|
||||||
|
Assert.assertNotNull("registrationAccessToken", client.getRegistrationAccessToken());
|
||||||
|
Assert.assertEquals("registrationAccessToken in returned json same as in config",
|
||||||
|
config2.sessionRealmConfigData().getClients().get("test-client"), client.getRegistrationAccessToken());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
exe = execute("delete test-client --config '" + configFile.getName() + "'");
|
||||||
|
|
||||||
|
assertExitCodeAndStreamSizes(exe, 0, 0, 0);
|
||||||
|
|
||||||
|
// check changes to config file
|
||||||
|
ConfigData config3 = handler.loadConfig();
|
||||||
|
assertFieldsEqualWithExclusions(config2, config3, "endpoints." + serverUrl + ".master.clients.test-client");
|
||||||
|
|
||||||
|
// check that registration access token is no longer there
|
||||||
|
Assert.assertTrue("clients empty", config3.sessionRealmConfigData().getClients().isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
KcRegExec exe = execute("get test-client --no-config --server " + serverUrl + " --realm test" +
|
||||||
|
" --user user1 --password userpass --client reg-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 = execute("get test-client --no-config --server " + serverUrl + " --realm test" +
|
||||||
|
" --user user1 --password wrong --client reg-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 = execute("get test-client --no-config --server " + serverUrl + " --realm test" +
|
||||||
|
" --user user1 --password userpass --client reg-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 reg-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/kcreg/reg-cli-keystore.jks");
|
||||||
|
Assert.assertTrue("reg-cli-keystore.jks exists", keystore.isFile());
|
||||||
|
|
||||||
|
// try client without direct grants enabled
|
||||||
|
KcRegExec exe = execute("get test-client --no-config --server " + serverUrl + " --realm test" +
|
||||||
|
" --user user1 --password userpass --client reg-cli-jwt --keystore '" + keystore.getAbsolutePath() + "'" +
|
||||||
|
" --storepass storepass --keypass keypass --alias reg-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 = execute("get test-client --no-config --server " + serverUrl + " --realm test" +
|
||||||
|
" --user user1 --password wrong --client reg-cli-jwt-direct --keystore '" + keystore.getAbsolutePath() + "'" +
|
||||||
|
" --storepass storepass --keypass keypass --alias reg-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 = execute("get test-client --no-config --server " + serverUrl + " --realm test" +
|
||||||
|
" --user user1 --password userpass --client reg-cli-jwt-direct --keystore '" + keystore.getAbsolutePath() + "'" +
|
||||||
|
" --storepass wrong --keypass keypass --alias reg-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 reg-cli-jwt-direct --keystore '" + keystore.getAbsolutePath() + "'" +
|
||||||
|
" --storepass storepass --keypass keypass --alias reg-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 reg-cli-secret --secret password", "",
|
||||||
|
"Logging into " + serverUrl + " as service-account-reg-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/kcreg/reg-cli-keystore.jks");
|
||||||
|
Assert.assertTrue("reg-cli-keystore.jks exists", keystore.isFile());
|
||||||
|
|
||||||
|
testCRUDWithOnTheFlyAuth(serverUrl,
|
||||||
|
"--client reg-cli-jwt --keystore '" + keystore.getAbsolutePath() + "' --storepass storepass --keypass keypass --alias reg-cli", "",
|
||||||
|
"Logging into " + serverUrl + " as service-account-reg-cli-jwt of realm test");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateDeleteWithInitialAndRegistrationTokens() throws IOException {
|
||||||
|
/*
|
||||||
|
* Test create using initial client token, and subsequent delete using registration access token.
|
||||||
|
* A config file is used to save registration access token for newly created client.
|
||||||
|
*/
|
||||||
|
testCreateDeleteWithInitialAndRegistrationTokens(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateDeleteWithInitialAndRegistrationTokensNoConfig() throws IOException {
|
||||||
|
/*
|
||||||
|
* Test create using initial client token, and subsequent delete using registration access token.
|
||||||
|
* No config file is used so registration access token for newly created client is not saved to config.
|
||||||
|
*/
|
||||||
|
testCreateDeleteWithInitialAndRegistrationTokens(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testCreateDeleteWithInitialAndRegistrationTokens(boolean useConfig) throws IOException {
|
||||||
|
|
||||||
|
// prepare for loading a config file
|
||||||
|
// only used when useConfig is true
|
||||||
|
FileConfigHandler handler = initCustomConfigFile();
|
||||||
|
|
||||||
|
try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
|
||||||
|
|
||||||
|
String token = issueInitialAccessToken("master");
|
||||||
|
|
||||||
|
final String realm = "master";
|
||||||
|
|
||||||
|
KcRegExec exe = execute("create " + (useConfig ? ("--config '" + configFile.getAbsolutePath()) + "'" : "--no-config")
|
||||||
|
+ " --server " + serverUrl + " --realm " + realm + " -s clientId=test-client2 -o -t " + token);
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
|
||||||
|
|
||||||
|
ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
|
||||||
|
|
||||||
|
Assert.assertEquals("clientId", "test-client2", client.getClientId());
|
||||||
|
Assert.assertNotNull("registrationAccessToken", client.getRegistrationAccessToken());
|
||||||
|
|
||||||
|
|
||||||
|
if (useConfig) {
|
||||||
|
ConfigData config = handler.loadConfig();
|
||||||
|
Assert.assertEquals("Registration Access Token in config file", client.getRegistrationAccessToken(),
|
||||||
|
config.ensureRealmConfigData(serverUrl, realm).getClients().get("test-client2"));
|
||||||
|
} else {
|
||||||
|
Assert.assertFalse("There should be no config file", configFile.isFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
exe = execute("delete test-client2 " + (useConfig ? ("--config '" + configFile.getAbsolutePath()) + "'" : "--no-config")
|
||||||
|
+ " --server " + serverUrl + " --realm " + realm + " -t " + client.getRegistrationAccessToken());
|
||||||
|
|
||||||
|
assertExitCodeAndStreamSizes(exe, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateWithAllowedHostsWithoutAuthenticationNoConfig() throws IOException {
|
||||||
|
|
||||||
|
testCreateWithAllowedHostsWithoutAuthentication("test", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateWithAllowedHostsWithoutAuthentication() throws IOException {
|
||||||
|
|
||||||
|
testCreateWithAllowedHostsWithoutAuthentication("test", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testCreateWithAllowedHostsWithoutAuthentication(String realm, boolean useConfig) throws IOException {
|
||||||
|
|
||||||
|
addLocalhostToAllowedHosts(realm);
|
||||||
|
|
||||||
|
FileConfigHandler handler = initCustomConfigFile();
|
||||||
|
|
||||||
|
try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
|
||||||
|
KcRegExec exe = execute("create " + (useConfig ? ("--config '" + configFile.getAbsolutePath()) + "'" : "--no-config")
|
||||||
|
+ " --server " + serverUrl + " --realm " + realm + " -s clientId=test-client -o");
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
|
||||||
|
ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
|
||||||
|
|
||||||
|
Assert.assertEquals("clientId", "test-client", client.getClientId());
|
||||||
|
Assert.assertNotNull("registrationAccessToken", client.getRegistrationAccessToken());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
package org.keycloak.testsuite.cli.registration;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.client.registration.cli.config.ConfigData;
|
||||||
|
import org.keycloak.client.registration.cli.config.FileConfigHandler;
|
||||||
|
import org.keycloak.client.registration.cli.util.ConfigUtil;
|
||||||
|
import org.keycloak.testsuite.cli.KcRegExec;
|
||||||
|
import org.keycloak.testsuite.util.TempFileResource;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.EOL;
|
||||||
|
import static org.keycloak.testsuite.cli.KcRegExec.execute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public class KcRegTruststoreTest extends AbstractCliTest {
|
||||||
|
|
||||||
|
@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
|
||||||
|
KcRegExec 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 = KcRegExec.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 = KcRegExec.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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check missing argument error
|
||||||
|
KcRegExec exe = execute("config truststore");
|
||||||
|
assertExitCodeAndStreamSizes(exe, 1, 0, 1);
|
||||||
|
Assert.assertEquals("no truststore error", "No truststore specified", exe.stderrLines().get(0));
|
||||||
|
|
||||||
|
// configure truststore with password
|
||||||
|
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, 1);
|
||||||
|
Assert.assertEquals("incompatible", "Option --delete is mutually exclusive with specifying a TRUSTSTORE", exe.stderrLines().get(0));
|
||||||
|
|
||||||
|
exe = execute("config truststore --delete --trustpass secret");
|
||||||
|
assertExitCodeAndStreamSizes(exe, 1, 0, 1);
|
||||||
|
Assert.assertEquals("no truststore error", "Options --trustpass and --delete are mutually exclusive", exe.stderrLines().get(0));
|
||||||
|
|
||||||
|
FileConfigHandler cfghandler = new FileConfigHandler();
|
||||||
|
cfghandler.setConfigFile(ConfigUtil.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");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUpdateTokenTruststore() {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,170 @@
|
||||||
|
package org.keycloak.testsuite.cli.registration;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.client.registration.cli.config.FileConfigHandler;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.testsuite.cli.KcRegExec;
|
||||||
|
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.KcRegExec.execute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public class KcRegUpdateTest extends AbstractCliTest {
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
KcRegExec exe = execute("create --config '" + configFile.getName() + "' -o -s clientId=my_client");
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
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 my_client --config '" + configFile.getName() + "' -o " +
|
||||||
|
" -s enabled=false -s 'redirectUris=[\"http://localhost:8980/myapp/*\"]'");
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
Assert.assertTrue("stderr is empty", exe.stderrLines().isEmpty());
|
||||||
|
|
||||||
|
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 my_client --config '" + configFile.getName() + "' -o -d redirectUris -s webOrigins+=http://localhost:8980/myapp -s webOrigins+=http://localhost:8981/myapp -d webOrigins[0]");
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
Assert.assertTrue("stderr is empty", exe.stderrLines().isEmpty());
|
||||||
|
|
||||||
|
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\"}'");
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
Assert.assertTrue("stderr is empty", exe.stderrLines().isEmpty());
|
||||||
|
|
||||||
|
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 my_client --nonexisting --config '" + configFile.getName() + "'");
|
||||||
|
|
||||||
|
assertExitCodeAndStreamSizes(exe, 1, 0, 1);
|
||||||
|
Assert.assertEquals("error message", "Unsupported option: --nonexisting", exe.stderrLines().get(0));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// try use incompatible endpoint
|
||||||
|
exe = execute("update my_client --config '" + configFile.getName() + "' -o -s enabled=true -e oidc");
|
||||||
|
|
||||||
|
assertExitCodeAndStreamSizes(exe, 1, 0, 1);
|
||||||
|
Assert.assertEquals("error message", "Failed to set attribute 'enabled' on document type 'oidc'", exe.stderrLines().get(0));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// test overwrite from file
|
||||||
|
exe = KcRegExec.newBuilder()
|
||||||
|
.argsLine("update my_client --config '" + configFile.getName() +
|
||||||
|
"' -o -s clientId=my_client -s 'redirectUris=[\"http://localhost:8980/myapp/*\"]' -f -")
|
||||||
|
.stdin(new ByteArrayInputStream("{ \"enabled\": false }".getBytes()))
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
Assert.assertTrue("stderr has error", exe.stderrLines().isEmpty());
|
||||||
|
|
||||||
|
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 = KcRegExec.newBuilder()
|
||||||
|
.argsLine("update my_client --config '" + configFile.getName() +
|
||||||
|
"' -o -s enabled=true -m -f -")
|
||||||
|
.stdin(new ByteArrayInputStream("{ \"webOrigins\": [\"http://localhost:8980/myapp\"] }".getBytes()))
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
Assert.assertTrue("stderr has error", exe.stderrLines().isEmpty());
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// remove registration access token
|
||||||
|
exe = execute("config registration-token --config '" + configFile.getName() + "' --server " + serverUrl +
|
||||||
|
" --realm " + realm + " --client my_client -d");
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
Assert.assertTrue("stderr is empty", exe.stderrLines().isEmpty());
|
||||||
|
|
||||||
|
Assert.assertNull("my_client registration token", handler.loadConfig().ensureRealmConfigData(serverUrl, realm).getClients().get("my_client"));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// test update without registration access token to produce 'unsafe' error
|
||||||
|
exe = execute("update my_client --config '" + configFile.getName() + "' -o -s bearerOnly=true");
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 1", 1, exe.exitCode());
|
||||||
|
Assert.assertFalse("stderr is not empty", exe.stderrLines().isEmpty());
|
||||||
|
Assert.assertEquals("error message", "No Registration Access Token found for client: my_client. Provide one or use --unsafe.", exe.stderrLines().get(0));
|
||||||
|
|
||||||
|
|
||||||
|
// test using unsafe to perform update
|
||||||
|
exe = execute("update my_client --config '" + configFile.getName() + "' -o -s bearerOnly=true --unsafe");
|
||||||
|
|
||||||
|
Assert.assertEquals("exitCode == 0", 0, exe.exitCode());
|
||||||
|
Assert.assertTrue("stderr is empty", exe.stderrLines().isEmpty());
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
package org.keycloak.testsuite.cli.registration;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.client.registration.cli.config.ConfigData;
|
||||||
|
import org.keycloak.client.registration.cli.config.FileConfigHandler;
|
||||||
|
import org.keycloak.client.registration.cli.config.RealmConfigData;
|
||||||
|
import org.keycloak.client.registration.cli.util.ConfigUtil;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.testsuite.cli.KcRegExec;
|
||||||
|
import org.keycloak.testsuite.util.TempFileResource;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.keycloak.client.registration.cli.util.OsUtil.EOL;
|
||||||
|
import static org.keycloak.testsuite.cli.KcRegExec.execute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public class KcRegUpdateTokenTest extends AbstractCliTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUpdateToken() throws IOException {
|
||||||
|
|
||||||
|
FileConfigHandler handler = initCustomConfigFile();
|
||||||
|
ConfigUtil.setHandler(handler);
|
||||||
|
|
||||||
|
try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) {
|
||||||
|
|
||||||
|
KcRegExec exe = execute("config credentials --config '" + configFile.getName() + "' --server " + serverUrl + " --realm master --user admin --password admin");
|
||||||
|
assertExitCodeAndStreamSizes(exe, 0, 0, 1);
|
||||||
|
|
||||||
|
// read current registration access token
|
||||||
|
ConfigData data = ConfigUtil.loadConfig();
|
||||||
|
RealmConfigData rdata = data.getRealmConfigData(serverUrl, "test");
|
||||||
|
Assert.assertNull("realm info set", rdata);
|
||||||
|
|
||||||
|
// update registration access token
|
||||||
|
exe = execute("update-token --config '" + configFile.getName() + "' reg-cli-secret-direct --server " + serverUrl + " --realm test --user user1 --password userpass");
|
||||||
|
|
||||||
|
assertExitCodeAndStreamSizes(exe, 0, 0, 1);
|
||||||
|
|
||||||
|
// read current registration token
|
||||||
|
data = ConfigUtil.loadConfig();
|
||||||
|
rdata = data.getRealmConfigData(serverUrl, "test");
|
||||||
|
Assert.assertEquals("current session realm unchanged", "master", data.getRealm());
|
||||||
|
Assert.assertNotNull("realm info set", rdata);
|
||||||
|
Assert.assertNull("on the fly login was transient", rdata.getToken());
|
||||||
|
Assert.assertNotNull("client info has registration access token", rdata.getClients().get("reg-cli-secret-direct"));
|
||||||
|
|
||||||
|
// use --no-config and on-the-fly auth
|
||||||
|
exe = execute("update-token reg-cli-secret-direct --no-config --server " + serverUrl + " --realm test --user user1 --password userpass");
|
||||||
|
assertExitCodeAndStreamSizes(exe, 0, 1, 1);
|
||||||
|
|
||||||
|
// save the token
|
||||||
|
String token = exe.stdoutLines().get(0);
|
||||||
|
|
||||||
|
// test that the token works
|
||||||
|
exe = execute("get reg-cli-secret-direct --no-config --server " + serverUrl + " --realm test -t " + token);
|
||||||
|
Assert.assertEquals("exit code", 0, exe.exitCode());
|
||||||
|
|
||||||
|
ClientRepresentation client = JsonSerialization.readValue(exe.stdout(), ClientRepresentation.class);
|
||||||
|
Assert.assertEquals("client representation returned", "reg-cli-secret-direct", client.getClientId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package org.keycloak.testsuite.util;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||||
|
*/
|
||||||
|
public class TempFileResource implements Closeable {
|
||||||
|
|
||||||
|
private File file;
|
||||||
|
|
||||||
|
public TempFileResource(String filepath) {
|
||||||
|
file = new File(filepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TempFileResource(File file) {
|
||||||
|
this.file = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getFile() {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return file.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAbsolutePath() {
|
||||||
|
return file.getAbsolutePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFile() {
|
||||||
|
return file.isFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
// delete file if it exists
|
||||||
|
file.delete();
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
|
@ -0,0 +1,20 @@
|
||||||
|
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="http://localhost:8080/sales-post-enc/">
|
||||||
|
<SPSSODescriptor AuthnRequestsSigned="true"
|
||||||
|
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol http://schemas.xmlsoap.org/ws/2003/07/secext">
|
||||||
|
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8081/sales-post-enc/saml"/>
|
||||||
|
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
|
||||||
|
</NameIDFormat>
|
||||||
|
<AssertionConsumerService
|
||||||
|
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8081/sales-post-enc/saml"
|
||||||
|
index="1" isDefault="true" />
|
||||||
|
<KeyDescriptor use="signing">
|
||||||
|
<dsig:KeyInfo xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
|
||||||
|
<dsig:X509Data>
|
||||||
|
<dsig:X509Certificate>
|
||||||
|
MIIB1DCCAT0CBgFJGVacCDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1lbmMvMB4XDTE0MTAxNjE0MjA0NloXDTI0MTAxNjE0MjIyNlowMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3QtZW5jLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA2+5MCT5BnVN+IYnKZcH6ev1pjXGi4feE0nOycq/VJ3aeaZMi4G9AxOxCBPupErOC7Kgm/Bw5AdJyw+Q12wSRXfJ9FhqCrLXpb7YOhbVSTJ8De5O8mW35DxAlh/cxe9FXjqPb286wKTUZ3LfGYR+X235UQeCTAPS/Ufi21EXaEikCAwEAATANBgkqhkiG9w0BAQsFAAOBgQBMrfGD9QFfx5v7ld/OAto5rjkTe3R1Qei8XRXfcs83vLaqEzjEtTuLGrJEi55kXuJgBpVmQpnwCCkkjSy0JxbqLDdVi9arfWUxEGmOr01ZHycELhDNaQcFqVMPr5kRHIHgktT8hK2IgCvd3Fy9/JCgUgCPxKfhwecyEOKxUc857g==
|
||||||
|
</dsig:X509Certificate>
|
||||||
|
</dsig:X509Data>
|
||||||
|
</dsig:KeyInfo>
|
||||||
|
</KeyDescriptor>
|
||||||
|
</SPSSODescriptor>
|
||||||
|
</EntityDescriptor>
|
|
@ -172,6 +172,8 @@
|
||||||
<migration.import.properties>${migration.import.properties}</migration.import.properties>
|
<migration.import.properties>${migration.import.properties}</migration.import.properties>
|
||||||
|
|
||||||
<testsuite.constants>${testsuite.constants}</testsuite.constants>
|
<testsuite.constants>${testsuite.constants}</testsuite.constants>
|
||||||
|
<cli.log.output>${cli.log.output}</cli.log.output>
|
||||||
|
<test.intermittent>${test.intermittent}</test.intermittent>
|
||||||
|
|
||||||
<browser>${browser}</browser>
|
<browser>${browser}</browser>
|
||||||
<firefox_binary>${firefox_binary}</firefox_binary>
|
<firefox_binary>${firefox_binary}</firefox_binary>
|
||||||
|
@ -781,6 +783,18 @@
|
||||||
<version>${mariadb.version}</version>
|
<version>${mariadb.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- CLI -->
|
||||||
|
<!--
|
||||||
|
- This dependency must come after org.bouncycastle dependencies since it contains BC classes,
|
||||||
|
- and MAC signature check on classes would fail otherwise with:
|
||||||
|
- 'java.lang.SecurityException: JCE cannot authenticate the provider BC'
|
||||||
|
-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-client-cli-dist</artifactId>
|
||||||
|
<type>zip</type>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
|
Loading…
Reference in a new issue