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.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.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 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);
}
}