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