Skip to content

Commit

Permalink
[issue/#7] Omit already imported import-files (#10)
Browse files Browse the repository at this point in the history
* [issue/#7] Omit keycloak import run if checksum is same

* [issue/#7] Provide opt-in mechanism to force imports
  • Loading branch information
borisskert authored Apr 24, 2019
1 parent 418334e commit eb69e19
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 13 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ $ docker run -e KEYCLOAK_URL=http://<your keycloak host>:8080 \
-e KEYCLOAK_ADMIN_PASSWORD=<keycloak admin password> \
-e KEYCLOAK_ENCRYPT_ADMIN_PASSWORD=<true/false> \
-e WAIT_TIME_IN_SECONDS=120 \
-e IMPORT_FORCE=false \
-e JWKS_CONNECT_TIMEOUT=250 \
-e JWKS_READ_TIMEOUT=250 \
-e JWKS_SIZE_LIMIT=51200 \
Expand Down Expand Up @@ -109,6 +110,7 @@ services:
- KEYCLOAK_ADMIN_PASSWORD=<keycloak admin password>
- KEYCLOAK_ENCRYPT_ADMIN_PASSWORD=<true/false>
- WAIT_TIME_IN_SECONDS=120
- IMPORT_FORCE=false
- JWKS_CONNECT_TIMEOUT=250
- JWKS_READ_TIMEOUT=250
- JWKS_SIZE_LIMIT=51200
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public class RealmImport extends RealmRepresentation {

private RolesImport rolesImport = new RolesImport();

private String checksum;

public Optional<CustomImport> getCustomImport() {
return Optional.ofNullable(customImport);
}
Expand Down Expand Up @@ -143,4 +145,14 @@ private List<AuthenticationFlowRepresentation> getNonTopLevelFlows() {
.filter(f -> !f.isTopLevel())
.collect(Collectors.toList());
}

@JsonIgnore
public String getChecksum() {
return checksum;
}

@JsonIgnore
public void setChecksum(String checksum) {
this.checksum = checksum;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import de.adorsys.keycloak.config.model.KeycloakImport;
import de.adorsys.keycloak.config.model.RealmImport;
import de.adorsys.keycloak.config.service.checksum.ChecksumService;
import org.apache.logging.log4j.util.Strings;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.Map;

Expand All @@ -24,16 +26,22 @@ public class KeycloakImportProvider {

private final ObjectMapper objectMapper;

public KeycloakImportProvider(@Qualifier("json") ObjectMapper objectMapper) {
private final ChecksumService checksumService;

public KeycloakImportProvider(
@Qualifier("json") ObjectMapper objectMapper,
ChecksumService checksumService
) {
this.objectMapper = objectMapper;
this.checksumService = checksumService;
}

public KeycloakImport get() {
KeycloakImport keycloakImport;

if(Strings.isNotBlank(importFilePath)) {
if (Strings.isNotBlank(importFilePath)) {
keycloakImport = readFromFile(importFilePath);
} else if(Strings.isNotBlank(importDirectoryPath)) {
} else if (Strings.isNotBlank(importDirectoryPath)) {
keycloakImport = readFromDirectory(importDirectoryPath);
} else {
throw new RuntimeException("Either 'import.path' or 'import.file' has to be defined");
Expand All @@ -45,10 +53,10 @@ public KeycloakImport get() {
private KeycloakImport readFromFile(String filename) {
try {
File configFile = new File(filename);
if(!configFile.exists()) {
if (!configFile.exists()) {
throw new RuntimeException("Is not existing: " + filename);
}
if(configFile.isDirectory()) {
if (configFile.isDirectory()) {
throw new RuntimeException("Is a directory: " + filename);
}

Expand All @@ -61,10 +69,10 @@ private KeycloakImport readFromFile(String filename) {
private KeycloakImport readFromDirectory(String filename) {
try {
File configDirectory = new File(filename);
if(!configDirectory.exists()) {
if (!configDirectory.exists()) {
throw new RuntimeException("Is not existing: " + filename);
}
if(!configDirectory.isDirectory()) {
if (!configDirectory.isDirectory()) {
throw new RuntimeException("Is not a directory: " + filename);
}

Expand All @@ -78,10 +86,10 @@ public KeycloakImport readRealmImportsFromDirectory(File importFilesDirectory) t
Map<String, RealmImport> realmImports = new HashMap<>();

File[] files = importFilesDirectory.listFiles();
if(files != null) {
if (files != null) {
for (File importFile : files) {
if(!importFile.isDirectory()) {
RealmImport realmImport = objectMapper.readValue(importFile, RealmImport.class);
if (!importFile.isDirectory()) {
RealmImport realmImport = readRealmImport(importFile);
realmImports.put(importFile.getName(), realmImport);
}
}
Expand All @@ -93,11 +101,21 @@ public KeycloakImport readRealmImportsFromDirectory(File importFilesDirectory) t
private KeycloakImport readRealmImportFromFile(File importFile) throws IOException {
Map<String, RealmImport> realmImports = new HashMap<>();

if(!importFile.isDirectory()) {
RealmImport realmImport = objectMapper.readValue(importFile, RealmImport.class);
if (!importFile.isDirectory()) {
RealmImport realmImport = readRealmImport(importFile);
realmImports.put(importFile.getName(), realmImport);
}

return new KeycloakImport(realmImports);
}

private RealmImport readRealmImport(File importFile) throws IOException {
byte[] importFileInBytes = Files.readAllBytes(importFile.toPath());
String checksum = checksumService.checksum(importFileInBytes);

RealmImport realmImport = objectMapper.readValue(importFile, RealmImport.class);
realmImport.setChecksum(checksum);

return realmImport;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;

@Service
public class RealmImportService {
private static final Logger logger = LoggerFactory.getLogger(RealmImportService.class);

private static final String REALM_CHECKSUM_ATTRIBUTE_KEY = "de.adorsys.keycloak.config.import-checksum";

private final String[] ignoredPropertiesForCreation = new String[]{
"users",
"browserFlow",
Expand Down Expand Up @@ -63,6 +67,10 @@ public class RealmImportService {
private final RequiredActionsImportService requiredActionsImportService;
private final CustomImportService customImportService;

@Value("${import.force:#{false}}")
private Boolean forceImport;


@Autowired
public RealmImportService(
KeycloakProvider keycloakProvider,
Expand Down Expand Up @@ -90,7 +98,7 @@ public void doImport(RealmImport realmImport) {
boolean realmExists = realmRepository.exists(realmImport.getRealm());

if(realmExists) {
updateRealm(realmImport);
updateRealmIfNecessary(realmImport);
} else {
createRealm(realmImport);
}
Expand All @@ -110,12 +118,26 @@ private void createRealm(RealmImport realmImport) {
setupFlows(realmImport);
importComponents(realmImport);
customImportService.doImport(realmImport);
setupImportChecksum(realmImport);
}

private void importComponents(RealmImport realmImport) {
componentImportService.doImport(realmImport);
}

private void updateRealmIfNecessary(RealmImport realmImport) {
if(forceImport || hasToBeUpdated(realmImport)) {
updateRealm(realmImport);
} else {
if(logger.isDebugEnabled())
logger.debug(
"No need to update realm '{}', import checksum same: '{}'",
realmImport.getRealm(),
realmImport.getChecksum()
);
}
}

private void updateRealm(RealmImport realmImport) {
if(logger.isDebugEnabled()) logger.debug("Updating realm '{}'...", realmImport.getRealm());

Expand All @@ -130,6 +152,7 @@ private void updateRealm(RealmImport realmImport) {
setupFlows(realmImport);
importComponents(realmImport);
customImportService.doImport(realmImport);
setupImportChecksum(realmImport);
}

private void importRequiredActions(RealmImport realmImport) {
Expand All @@ -152,4 +175,23 @@ private void setupFlows(RealmImport realmImport) {

realmRepository.update(realmToUpdate);
}

private boolean hasToBeUpdated(RealmImport realmImport) {
RealmRepresentation existingRealm = realmRepository.get(realmImport.getRealm());
Map<String, String> customAttributes = existingRealm.getAttributes();
String readChecksum = customAttributes.get(REALM_CHECKSUM_ATTRIBUTE_KEY);

return !realmImport.getChecksum().equals(readChecksum);
}

private void setupImportChecksum(RealmImport realmImport) {
RealmRepresentation existingRealm = realmRepository.get(realmImport.getRealm());
Map<String, String> customAttributes = existingRealm.getAttributes();

String importChecksum = realmImport.getChecksum();
customAttributes.put(REALM_CHECKSUM_ATTRIBUTE_KEY, importChecksum);
realmRepository.update(existingRealm);

if(logger.isDebugEnabled()) logger.debug("Updated import checksum of realm '{}' to '{}'", realmImport.getRealm(), importChecksum);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package de.adorsys.keycloak.config.service.checksum;

import org.bouncycastle.jcajce.provider.digest.SHA3;
import org.bouncycastle.util.encoders.Hex;
import org.springframework.stereotype.Service;

import java.security.MessageDigest;

@Service
public class ChecksumService {

private MessageDigest digest = new SHA3.Digest512();

public String checksum(String text) {
if (text == null) {
throw new IllegalArgumentException("Cannot calculate checksum of null");
}

byte[] textInBytes = text.getBytes();
return calculateSha3Checksum(textInBytes);
}

public String checksum(byte[] textInBytes) {
if (textInBytes == null) {
throw new IllegalArgumentException("Cannot calculate checksum of null");
}

return calculateSha3Checksum(textInBytes);
}

private String calculateSha3Checksum(byte[] textInBytes) {
byte[] shaInBytes = this.digest.digest(textInBytes);
return Hex.toHexString(shaInBytes);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public void shouldReadImports() {
@Test
public void integrationTests() throws Exception {
shouldCreateSimpleRealm();
shouldNotUpdateSimpleRealm();
shouldUpdateSimpleRealm();
shouldCreateSimpleRealmWithLoginTheme();
}
Expand All @@ -78,6 +79,24 @@ private void shouldCreateSimpleRealm() throws Exception {
assertThat(createdRealm.getRealm(), is(REALM_NAME));
assertThat(createdRealm.isEnabled(), is(true));
assertThat(createdRealm.getLoginTheme(), is(nullValue()));
assertThat(
createdRealm.getAttributes().get("de.adorsys.keycloak.config.import-checksum"),
is("3796660d3087308ee757d9d86e14dd6e6fe4bfd66cc1435851ff2f5c6fa432c5991b3042f95c4f11238e1dfb81676ae2a00bde0bbad17c1f66ef530841df2e66")
);
}

private void shouldNotUpdateSimpleRealm() throws Exception {
doImport("0.1_update_simple-realm_with_same_config.json");

RealmRepresentation createdRealm = keycloakProvider.get().realm(REALM_NAME).toRepresentation();

assertThat(createdRealm.getRealm(), is(REALM_NAME));
assertThat(createdRealm.isEnabled(), is(true));
assertThat(createdRealm.getLoginTheme(), is(nullValue()));
assertThat(
createdRealm.getAttributes().get("de.adorsys.keycloak.config.import-checksum"),
is("3796660d3087308ee757d9d86e14dd6e6fe4bfd66cc1435851ff2f5c6fa432c5991b3042f95c4f11238e1dfb81676ae2a00bde0bbad17c1f66ef530841df2e66")
);
}

private void shouldUpdateSimpleRealm() throws Exception {
Expand All @@ -88,6 +107,10 @@ private void shouldUpdateSimpleRealm() throws Exception {
assertThat(updatedRealm.getRealm(), is(REALM_NAME));
assertThat(updatedRealm.isEnabled(), is(true));
assertThat(updatedRealm.getLoginTheme(), is("moped"));
assertThat(
updatedRealm.getAttributes().get("de.adorsys.keycloak.config.import-checksum"),
is("d3913c179bf6d1ed1afbc2580207f3d7d78efed3ef13f9e12dea3afd5c28e9b307dd930fecfcc100038e540d1e23dc5b5c74d0321a410c7ba330e9dbf9d4211c")
);
}

private void shouldCreateSimpleRealmWithLoginTheme() throws Exception {
Expand All @@ -98,6 +121,10 @@ private void shouldCreateSimpleRealmWithLoginTheme() throws Exception {
assertThat(createdRealm.getRealm(), is("simpleWithLoginTheme"));
assertThat(createdRealm.isEnabled(), is(true));
assertThat(createdRealm.getLoginTheme(), is("moped"));
assertThat(
createdRealm.getAttributes().get("de.adorsys.keycloak.config.import-checksum"),
is("5d75698bacb06b1779e2b303069266664d63eec9c52038e2e6ae930bfc6e33ec7e7493b067ee0253e73a6b19cdf8905fd75cc6bb394ca333d32c784063aa65c8")
);
}

private void doImport(String realmImport) {
Expand Down
Loading

0 comments on commit eb69e19

Please sign in to comment.