Skip to content

Commit

Permalink
[issue/#23] Support exported JSON files (#24)
Browse files Browse the repository at this point in the history
* Extend clone-utils to ignore deeper properties

* Add integration test to import exported JSON

* Stablized code and ignore presence of possible 'id' properties in import files

* Use different export files for different keycloak versions in integration tests
  • Loading branch information
Boris Skert authored May 15, 2019
1 parent 6de92e6 commit 635e2cd
Show file tree
Hide file tree
Showing 20 changed files with 10,044 additions and 73 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ target/
.settings/
pom.xml.versionsBackup
dependency-reduced-pom.xml
tmp/
17 changes: 17 additions & 0 deletions config-cli/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,23 @@
</execution>
</executions>
</plugin>
<plugin>
<!--https://stackoverflow.com/questions/11500533/access-maven-properties-defined-in-the-pom-->
<groupId>org.codehaus.mojo</groupId>
<artifactId>properties-maven-plugin</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<phase>generate-resources</phase>
<goals>
<goal>write-project-properties</goal>
</goals>
<configuration>
<outputFile>${project.build.testOutputDirectory}/maven.properties</outputFile>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.keycloak.admin.client.resource.AuthenticationManagementResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -26,22 +27,21 @@ public AuthenticationFlowRepository(RealmRepository realmRepository) {
}

public Optional<AuthenticationFlowRepresentation> tryToGetTopLevelFlow(String realm, String alias) {
if(logger.isTraceEnabled()) logger.trace("Try to get top-level-flow '{}' from realm '{}'", alias, realm);

AuthenticationManagementResource flowsResource = getFlows(realm);

// keycloak is returning here only so-called toplevel-flows
List<AuthenticationFlowRepresentation> existingTopLevelFlows = flowsResource.getFlows();

return existingTopLevelFlows.stream()
.filter(f -> f.getAlias().equals(alias))
if (logger.isTraceEnabled()) logger.trace("Try to get top-level-flow '{}' from realm '{}'", alias, realm);

// with `AuthenticationManagementResource.getFlows()` keycloak is NOT returning all so-called top-level-flows so
// we need a partial export
RealmRepresentation realmExport = realmRepository.partialExport(realm);
return realmExport.getAuthenticationFlows()
.stream()
.filter(flow -> flow.getAlias().equals(alias))
.findFirst();
}

public AuthenticationFlowRepresentation getTopLevelFlow(String realm, String alias) throws KeycloakRepositoryException {
Optional<AuthenticationFlowRepresentation> maybeTopLevelFlow = tryToGetTopLevelFlow(realm, alias);

if(maybeTopLevelFlow.isPresent()) {
if (maybeTopLevelFlow.isPresent()) {
return maybeTopLevelFlow.get();
}

Expand All @@ -52,7 +52,8 @@ public AuthenticationFlowRepresentation getTopLevelFlow(String realm, String ali
* creates only the top-level flow WITHOUT its executions or execution-flows
*/
public void createTopLevelFlow(String realm, AuthenticationFlowRepresentation topLevelFlowToImport) throws KeycloakRepositoryException {
if(logger.isTraceEnabled()) logger.trace("Create top-level-flow '{}' in realm '{}'", topLevelFlowToImport.getAlias(), realm);
if (logger.isTraceEnabled())
logger.trace("Create top-level-flow '{}' in realm '{}'", topLevelFlowToImport.getAlias(), realm);

AuthenticationManagementResource flowsResource = getFlows(realm);
Response response = flowsResource.createFlow(topLevelFlowToImport);
Expand All @@ -61,7 +62,7 @@ public void createTopLevelFlow(String realm, AuthenticationFlowRepresentation to
}

public AuthenticationFlowRepresentation getFlowById(String realm, String id) {
if(logger.isTraceEnabled()) logger.trace("Get flow by id '{}' in realm '{}'", id, realm);
if (logger.isTraceEnabled()) logger.trace("Get flow by id '{}' in realm '{}'", id, realm);

AuthenticationManagementResource flowsResource = getFlows(realm);
return flowsResource.getFlow(id);
Expand All @@ -73,12 +74,12 @@ public void deleteTopLevelFlow(String realm, String topLevelFlowId) {
}

AuthenticationManagementResource getFlows(String realm) {
if(logger.isTraceEnabled()) logger.trace("Get flows-resource for realm '{}'...", realm);
if (logger.isTraceEnabled()) logger.trace("Get flows-resource for realm '{}'...", realm);

RealmResource realmResource = realmRepository.loadRealm(realm);
AuthenticationManagementResource flows = realmResource.flows();

if(logger.isTraceEnabled()) logger.trace("Got flows-resource for realm '{}'", realm);
if (logger.isTraceEnabled()) logger.trace("Got flows-resource for realm '{}'", realm);

return flows;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,12 @@ public Optional<ClientRepresentation> tryToFindClient(String realm, String clien
}

public ClientRepresentation getClient(String realm, String clientId) {
List<ClientRepresentation> foundClients = realmRepository.loadRealm(realm)
.clients()
.findByClientId(clientId);

if(foundClients.isEmpty()) {
throw new RuntimeException("Cannot find client by clientId '" + clientId + "'");
}
return loadClient(realm, clientId);
}

return foundClients.get(0);
public String getClientSecret(String realm, String clientId) {
ClientResource clientResource = getClientResource(realm, clientId);
return clientResource.getSecret().getValue();
}

public void create(String realm, ClientRepresentation clientToCreate) {
Expand All @@ -63,4 +60,24 @@ public void update(String realm, ClientRepresentation clientToUpdate) {

clientResource.update(clientToUpdate);
}


private ClientRepresentation loadClient(String realm, String clientId) {
List<ClientRepresentation> foundClients = realmRepository.loadRealm(realm)
.clients()
.findByClientId(clientId);

if(foundClients.isEmpty()) {
throw new RuntimeException("Cannot find client by clientId '" + clientId + "'");
}

return foundClients.get(0);
}

private ClientResource getClientResource(String realm, String clientId) {
ClientRepresentation client = loadClient(realm, clientId);
return realmRepository.loadRealm(realm)
.clients()
.get(client.getId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,8 @@ public RealmRepresentation get(String realm) {
public void update(RealmRepresentation realmToUpdate) {
loadRealm(realmToUpdate.getRealm()).update(realmToUpdate);
}

public RealmRepresentation partialExport(String realm) {
return loadRealm(realm).partialExport(false, false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ public void doImport(RealmImport realmImport) {
Optional<ClientRepresentation> maybeClient = clientRepository.tryToFindClient(realm, clientId);

if(maybeClient.isPresent()) {
if(logger.isDebugEnabled()) logger.debug("Update client '{}' in realm '{}'", clientId, realm);
updateClient(realm, maybeClient.get(), client);
updateClientIfNeeded(realm, client, maybeClient.get());
} else {
if(logger.isDebugEnabled()) logger.debug("Create client '{}' in realm '{}'", clientId, realm);
clientRepository.create(realm, client);
Expand All @@ -46,8 +45,26 @@ public void doImport(RealmImport realmImport) {
}
}

private void updateClientIfNeeded(String realm, ClientRepresentation clientToUpdate, ClientRepresentation existingClient) {
if(!areClientsEqual(realm, clientToUpdate, existingClient)) {
if(logger.isDebugEnabled()) logger.debug("Update client '{}' in realm '{}'", clientToUpdate.getClientId(), realm);
updateClient(realm, existingClient, clientToUpdate);
} else {
if(logger.isDebugEnabled()) logger.debug("No need to update client '{}' in realm '{}'", clientToUpdate.getClientId(), realm);
}
}

private boolean areClientsEqual(String realm, ClientRepresentation clientToUpdate, ClientRepresentation existingClient) {
if(CloneUtils.deepEquals(clientToUpdate, existingClient, "id", "secret")) {
String clientSecret = clientRepository.getClientSecret(realm, clientToUpdate.getClientId());
return clientSecret.equals(clientToUpdate.getSecret());
}

return false;
}

private void updateClient(String realm, ClientRepresentation existingClient, ClientRepresentation clientToImport) {
ClientRepresentation patchedClient = CloneUtils.patch(existingClient, clientToImport);
ClientRepresentation patchedClient = CloneUtils.patch(existingClient, clientToImport, "id");
clientRepository.update(realm, patchedClient);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ private void updateComponentIfNeeded(
ComponentRepresentation existingComponent,
MultivaluedHashMap<String, ComponentExportRepresentation> subComponentChildren
) {
ComponentRepresentation patchedComponent = CloneUtils.patch(existingComponent, componentToImport);
ComponentRepresentation patchedComponent = CloneUtils.patch(existingComponent, componentToImport, "id");

boolean hasToBeUpdated = !CloneUtils.deepEquals(existingComponent, patchedComponent);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.StringJoiner;

public class CloneUtils {
private static ObjectMapper nonNullMapper;
Expand Down Expand Up @@ -91,7 +94,7 @@ public static <S, T> boolean deepEquals(S origin, T other, String... ignoredProp
}

private static <S> Map<String, Object> toMap(S object, String... ignoredProperties) {
JsonNode objectAsNode = nonNullMapper.valueToTree(object);
JsonNode objectAsNode = toJsonNode(object, ignoredProperties);
Map objectAsMap;

try {
Expand All @@ -100,15 +103,17 @@ private static <S> Map<String, Object> toMap(S object, String... ignoredProperti
throw new RuntimeException(e);
}

for (String ignoredProperty : ignoredProperties) {
if (objectAsMap.containsKey(ignoredProperty)) {
objectAsMap.remove(ignoredProperty);
}
}

return objectAsMap;
}

private static <S> JsonNode toJsonNode(S object, String... ignoredProperties) {
JsonNode objectAsNode = nonNullMapper.valueToTree(object);

removeIgnoredProperties(objectAsNode, ignoredProperties);

return objectAsNode;
}

private static <S> Map<String, Object> toMapFilteredBy(S object, String... allowedKeys) {
JsonNode objectAsNode = nonNullMapper.valueToTree(object);
Map<String, Object> objectAsMap;
Expand Down Expand Up @@ -173,4 +178,59 @@ private static <T, P, C> C patch(T origin, P patch, Class<C> targetClass) {
throw new RuntimeException(e);
}
}

private static void removeIgnoredProperties(JsonNode jsonNode, String[] ignoredProperties) {
if (jsonNode.isObject()) {
ObjectNode objectNode = (ObjectNode) jsonNode;

for (String ignoredProperty : ignoredProperties) {
if (objectNode.has(ignoredProperty)) {
objectNode.remove(ignoredProperty);
} else {
removeDeepPropertiesIfAny(jsonNode, ignoredProperty);
}
}
} else if (jsonNode.isArray()) {
ArrayNode arrayNode = (ArrayNode) jsonNode;

for (JsonNode childNode : arrayNode) {
removeIgnoredProperties(childNode, ignoredProperties);
}
}
}

private static void removeDeepPropertiesIfAny(JsonNode jsonNode, String ignoredProperty) {
ObjectNode objectNode = (ObjectNode) jsonNode;
String[] splitProperty = ignoredProperty.split("\\.");

if (splitProperty.length > 1) {
String propertyKey = splitProperty[0];
Object originPropertyValue = objectNode.get(propertyKey);
String deepIgnoredProperties = buildDeepIgnoredProperties(splitProperty);

JsonNode propertyValue = toJsonNode(originPropertyValue, deepIgnoredProperties);

objectNode.set(propertyKey, propertyValue);
}
}

private static String buildDeepIgnoredProperties(String[] array) {
StringJoiner joiner = new StringJoiner(".");

for (String element : cutFirstElement(array)) {
joiner.add(element);
}

return joiner.toString();
}

private static String[] cutFirstElement(String[] array) {
String[] arrayWithoutFirst = new String[array.length - 1];

if (array.length - 1 >= 0) {
System.arraycopy(array, 1, arrayWithoutFirst, 0, array.length - 1);
}

return arrayWithoutFirst;
}
}
Loading

0 comments on commit 635e2cd

Please sign in to comment.