diff --git a/src/main/java/de/adorsys/keycloak/config/configuration/NormalizationConfiguration.java b/src/main/java/de/adorsys/keycloak/config/configuration/NormalizationConfiguration.java index 855f07eb8..aa66bafc9 100644 --- a/src/main/java/de/adorsys/keycloak/config/configuration/NormalizationConfiguration.java +++ b/src/main/java/de/adorsys/keycloak/config/configuration/NormalizationConfiguration.java @@ -29,6 +29,7 @@ import org.javers.core.metamodel.clazz.EntityDefinition; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientScopeRepresentation; +import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; @@ -106,6 +107,7 @@ private JaversBuilder commonJavers() { List.of("id", "authorizationSettings", "protocolMappers"))) .registerEntity(new EntityDefinition(ProtocolMapperRepresentation.class, "name", List.of("id"))) .registerEntity(new EntityDefinition(ClientScopeRepresentation.class, "name", List.of("id", "protocolMappers"))) - .registerEntity(new EntityDefinition(RoleRepresentation.class, "name", List.of("id", "containerId", "composites"))); + .registerEntity(new EntityDefinition(RoleRepresentation.class, "name", List.of("id", "containerId", "composites"))) + .registerEntity(new EntityDefinition(GroupRepresentation.class, "path", List.of("id", "subGroups", "attributes", "clientRoles"))); } } diff --git a/src/main/java/de/adorsys/keycloak/config/service/normalize/AttributeNormalizationService.java b/src/main/java/de/adorsys/keycloak/config/service/normalize/AttributeNormalizationService.java index 5fff554e7..69e55bd97 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/normalize/AttributeNormalizationService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/normalize/AttributeNormalizationService.java @@ -20,10 +20,12 @@ package de.adorsys.keycloak.config.service.normalize; +import org.javers.core.Javers; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -31,10 +33,16 @@ @ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE") public class AttributeNormalizationService { - public Map normalizeAttributes(Map exportedAttributes, Map baselineAttributes) { - Map exportedOrEmpty = exportedAttributes == null ? Map.of() : exportedAttributes; - Map baselineOrEmpty = baselineAttributes == null ? Map.of() : baselineAttributes; - var normalizedAttributes = new HashMap(); + private final Javers unOrderedJavers; + + public AttributeNormalizationService(Javers unOrderedJavers) { + this.unOrderedJavers = unOrderedJavers; + } + + public Map normalizeStringAttributes(Map exportedAttributes, Map baselineAttributes) { + Map exportedOrEmpty = exportedAttributes == null ? Map.of() : exportedAttributes; + Map baselineOrEmpty = baselineAttributes == null ? Map.of() : baselineAttributes; + var normalizedAttributes = new HashMap(); for (var entry : baselineOrEmpty.entrySet()) { var attributeName = entry.getKey(); var baselineAttribute = entry.getValue(); @@ -47,4 +55,39 @@ public Map normalizeAttributes(Map exportedAttributes, normalizedAttributes.putAll(exportedOrEmpty); return normalizedAttributes.isEmpty() ? null : normalizedAttributes; } + + public Map> normalizeListAttributes(Map> exportedAttributes, Map> baselineAttributes) { + Map> exportedOrEmpty = exportedAttributes == null ? Map.of() : exportedAttributes; + Map> baselineOrEmpty = baselineAttributes == null ? Map.of() : baselineAttributes; + var normalizedAttributes = new HashMap>(); + for (var entry : baselineOrEmpty.entrySet()) { + var attributeName = entry.getKey(); + var baselineAttribute = entry.getValue(); + var exportedAttribute = exportedOrEmpty.remove(attributeName); + + if (unOrderedJavers.compareCollections(baselineAttribute, exportedAttribute, String.class).hasChanges()) { + normalizedAttributes.put(attributeName, exportedAttribute); + } + } + normalizedAttributes.putAll(exportedOrEmpty); + return normalizedAttributes.isEmpty() ? null : normalizedAttributes; + } + + public boolean listAttributesChanged(Map> exportedAttributes, Map> baselineAttributes) { + Map> exportedOrEmpty = exportedAttributes == null ? Map.of() : exportedAttributes; + Map> baselineOrEmpty = baselineAttributes == null ? Map.of() : baselineAttributes; + + if (!Objects.equals(exportedOrEmpty.keySet(), baselineOrEmpty.keySet())) { + return true; + } + + for (var entry : baselineOrEmpty.entrySet()) { + if (unOrderedJavers.compareCollections(entry.getValue(), + exportedOrEmpty.get(entry.getKey()), String.class).hasChanges()) { + return true; + } + } + return false; + } + } diff --git a/src/main/java/de/adorsys/keycloak/config/service/normalize/ClientNormalizationService.java b/src/main/java/de/adorsys/keycloak/config/service/normalize/ClientNormalizationService.java index 2a70078e0..1132b2255 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/normalize/ClientNormalizationService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/normalize/ClientNormalizationService.java @@ -59,14 +59,16 @@ public ClientNormalizationService(Javers javers, } public List normalizeClients(RealmRepresentation exportedRealm, RealmRepresentation baselineRealm) { + List exportedOrEmpty = exportedRealm.getClients() == null ? List.of() : exportedRealm.getClients(); + List baselineOrEmpty = baselineRealm.getClients() == null ? List.of() : baselineRealm.getClients(); var exportedClientMap = new HashMap(); - for (var exportedClient : exportedRealm.getClients()) { + for (var exportedClient : exportedOrEmpty) { exportedClientMap.put(exportedClient.getClientId(), exportedClient); } var baselineClientMap = new HashMap(); var clients = new ArrayList(); - for (var baselineRealmClient : baselineRealm.getClients()) { + for (var baselineRealmClient : baselineOrEmpty) { var clientId = baselineRealmClient.getClientId(); baselineClientMap.put(clientId, baselineRealmClient); var exportedClient = exportedClientMap.get(clientId); diff --git a/src/main/java/de/adorsys/keycloak/config/service/normalize/GroupNormalizationService.java b/src/main/java/de/adorsys/keycloak/config/service/normalize/GroupNormalizationService.java new file mode 100644 index 000000000..520006c1b --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/service/normalize/GroupNormalizationService.java @@ -0,0 +1,150 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2022 adorsys GmbH & Co. KG @ https://adorsys.com + * --- + * 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. + * ---license-end + */ + +package de.adorsys.keycloak.config.service.normalize; + +import de.adorsys.keycloak.config.util.JaversUtil; +import org.javers.core.Javers; +import org.javers.core.diff.changetype.PropertyChange; +import org.keycloak.representations.idm.GroupRepresentation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE") +public class GroupNormalizationService { + + private static final Logger logger = LoggerFactory.getLogger(GroupNormalizationService.class); + + private final Javers unOrderedJavers; + private final JaversUtil javersUtil; + private final AttributeNormalizationService attributeNormalizationService; + + public GroupNormalizationService(Javers unOrderedJavers, + JaversUtil javersUtil, + AttributeNormalizationService attributeNormalizationService) { + this.unOrderedJavers = unOrderedJavers; + this.javersUtil = javersUtil; + this.attributeNormalizationService = attributeNormalizationService; + } + + public List normalizeGroups(List exportedGroups, List baselineGroups) { + List exportedOrEmpty = exportedGroups == null ? List.of() : exportedGroups; + List baselineOrEmpty = baselineGroups == null ? List.of() : baselineGroups; + var exportedGroupsMap = exportedOrEmpty.stream() + .collect(Collectors.toMap(GroupRepresentation::getPath, Function.identity())); + var baselineGroupsMap = baselineOrEmpty.stream() + .collect(Collectors.toMap(GroupRepresentation::getPath, Function.identity())); + + var normalizedGroups = new ArrayList(); + for (var entry : baselineGroupsMap.entrySet()) { + var groupPath = entry.getKey(); + var exportedGroup = exportedGroupsMap.remove(groupPath); + if (exportedGroup == null) { + logger.warn("Default realm group '{}' was deleted in exported realm. It may be reintroduced during import", groupPath); + continue; + } + var baselineGroup = entry.getValue(); + var diff = unOrderedJavers.compare(baselineGroup, exportedGroup); + + if (diff.hasChanges() || subGroupsChanged(exportedGroup, baselineGroup) + || attributeNormalizationService.listAttributesChanged(exportedGroup.getAttributes(), baselineGroup.getAttributes()) + || attributeNormalizationService.listAttributesChanged(exportedGroup.getClientRoles(), baselineGroup.getClientRoles())) { + var normalizedGroup = new GroupRepresentation(); + for (var change : diff.getChangesByType(PropertyChange.class)) { + javersUtil.applyChange(normalizedGroup, change); + } + normalizedGroup.setAttributes(exportedGroup.getAttributes()); + normalizedGroup.setClientRoles(exportedGroup.getClientRoles()); + normalizedGroup.setPath(exportedGroup.getPath()); + normalizedGroups.add(normalizedGroup); + } + } + normalizedGroups.addAll(exportedGroupsMap.values()); + normalizeGroupList(normalizedGroups); + return normalizedGroups.isEmpty() ? null : normalizedGroups; + } + + public boolean subGroupsChanged(GroupRepresentation exportedGroup, GroupRepresentation baselineGroup) { + if (exportedGroup.getSubGroups() == null && baselineGroup.getSubGroups() != null) { + return true; + } + if (exportedGroup.getSubGroups() != null && baselineGroup.getSubGroups() == null) { + return true; + } + if (exportedGroup.getSubGroups() == null && baselineGroup.getSubGroups() == null) { + return false; + } + + Map exportedSubGroups = exportedGroup.getSubGroups().stream() + .collect(Collectors.toMap(GroupRepresentation::getPath, Function.identity())); + Map baselineSubGroups = baselineGroup.getSubGroups().stream() + .collect(Collectors.toMap(GroupRepresentation::getPath, Function.identity())); + + for (var entry : baselineSubGroups.entrySet()) { + var groupPath = entry.getKey(); + var exportedSubGroup = exportedSubGroups.remove(groupPath); + + if (exportedSubGroup == null) { + // There's a subgroup in the baseline that's gone in the export. This counts as a change. + return true; + } + var baselineSubGroup = entry.getValue(); + if (unOrderedJavers.compare(baselineSubGroup, exportedSubGroup).hasChanges()) { + return true; + } + if (subGroupsChanged(exportedSubGroup, baselineSubGroup)) { + return true; + } + } + + // There are subgroups in the export that are not in the baseline. This is a change. + return !exportedSubGroups.isEmpty(); + } + + public void normalizeGroupList(List groups) { + for (var group : groups) { + if (group.getAttributes() != null && group.getAttributes().isEmpty()) { + group.setAttributes(null); + } + if (group.getRealmRoles() != null && group.getRealmRoles().isEmpty()) { + group.setRealmRoles(null); + } + if (group.getClientRoles() != null && group.getClientRoles().isEmpty()) { + group.setClientRoles(null); + } + if (group.getSubGroups() != null) { + if (group.getSubGroups().isEmpty()) { + group.setSubGroups(null); + } else { + normalizeGroupList(group.getSubGroups()); + } + } + } + } +} diff --git a/src/main/java/de/adorsys/keycloak/config/service/normalize/RealmNormalizationService.java b/src/main/java/de/adorsys/keycloak/config/service/normalize/RealmNormalizationService.java index dc8b4d446..bb2681a51 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/normalize/RealmNormalizationService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/normalize/RealmNormalizationService.java @@ -49,6 +49,7 @@ public class RealmNormalizationService { private final ClientScopeNormalizationService clientScopeNormalizationService; private final RoleNormalizationService roleNormalizationService; private final AttributeNormalizationService attributeNormalizationService; + private final GroupNormalizationService groupNormalizationService; private final JaversUtil javersUtil; @Autowired @@ -61,6 +62,7 @@ public RealmNormalizationService(NormalizationKeycloakConfigProperties keycloakC ClientScopeNormalizationService clientScopeNormalizationService, RoleNormalizationService roleNormalizationService, AttributeNormalizationService attributeNormalizationService, + GroupNormalizationService groupNormalizationService, JaversUtil javersUtil) { this.keycloakConfigProperties = keycloakConfigProperties; this.javers = javers; @@ -71,6 +73,7 @@ public RealmNormalizationService(NormalizationKeycloakConfigProperties keycloakC this.clientScopeNormalizationService = clientScopeNormalizationService; this.roleNormalizationService = roleNormalizationService; this.attributeNormalizationService = attributeNormalizationService; + this.groupNormalizationService = groupNormalizationService; this.javersUtil = javersUtil; // TODO allow extra "default" values to be ignored? @@ -123,7 +126,7 @@ public RealmRepresentation normalizeRealm(RealmRepresentation exportedRealm) { minimizedRealm.setClientScopeMappings(clientScopeMappings); } - minimizedRealm.setAttributes(attributeNormalizationService.normalizeAttributes(exportedRealm.getAttributes(), baselineRealm.getAttributes())); + minimizedRealm.setAttributes(attributeNormalizationService.normalizeStringAttributes(exportedRealm.getAttributes(), baselineRealm.getAttributes())); minimizedRealm.setProtocolMappers(protocolMapperNormalizationService.normalizeProtocolMappers(exportedRealm.getProtocolMappers(), baselineRealm.getProtocolMappers())); @@ -132,6 +135,9 @@ public RealmRepresentation normalizeRealm(RealmRepresentation exportedRealm) { baselineRealm.getClientScopes())); minimizedRealm.setRoles(roleNormalizationService.normalizeRoles(exportedRealm.getRoles(), baselineRealm.getRoles())); + + minimizedRealm.setGroups(groupNormalizationService.normalizeGroups(exportedRealm.getGroups(), baselineRealm.getGroups())); + return minimizedRealm; } diff --git a/src/main/java/de/adorsys/keycloak/config/service/normalize/RoleNormalizationService.java b/src/main/java/de/adorsys/keycloak/config/service/normalize/RoleNormalizationService.java index a4dd90585..2372b046c 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/normalize/RoleNormalizationService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/normalize/RoleNormalizationService.java @@ -89,14 +89,14 @@ public List normalizeRealmRoles(List exp } var diff = unOrderedJavers.compare(baselineRole, exportedRole); if (diff.hasChanges() - || attributesChanged(baselineRole.getAttributes(), exportedRole.getAttributes()) + || attributeNormalizationService.listAttributesChanged(exportedRole.getAttributes(), baselineRole.getAttributes()) || compositesChanged(exportedRole.getComposites(), baselineRole.getComposites())) { var normalizedRole = new RoleRepresentation(); normalizedRole.setName(roleName); for (var change : diff.getChangesByType(PropertyChange.class)) { javersUtil.applyChange(normalizedRole, change); } - normalizedRole.setAttributes(attributeNormalizationService.normalizeAttributes(exportedRole.getAttributes(), + normalizedRole.setAttributes(attributeNormalizationService.normalizeListAttributes(exportedRole.getAttributes(), baselineRole.getAttributes())); normalizedRoles.add(normalizedRole); normalizedRole.setComposites(exportedRole.getComposites()); diff --git a/src/main/java/de/adorsys/keycloak/config/service/normalize/ScopeMappingNormalizationService.java b/src/main/java/de/adorsys/keycloak/config/service/normalize/ScopeMappingNormalizationService.java index d72763cba..dc7dccec4 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/normalize/ScopeMappingNormalizationService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/normalize/ScopeMappingNormalizationService.java @@ -83,25 +83,25 @@ public List normalizeScopeMappings(RealmRepresentati public Map> normalizeClientScopeMappings(RealmRepresentation exportedRealm, RealmRepresentation baselineRealm) { - var baselineMappings = baselineRealm.getClientScopeMappings(); - var exportedMappings = exportedRealm.getClientScopeMappings(); + Map> baselineOrEmpty = baselineRealm.getClientScopeMappings() == null ? Map.of() : baselineRealm.getClientScopeMappings(); + Map> exportedOrEmpty = exportedRealm.getClientScopeMappings() == null ? Map.of() : exportedRealm.getClientScopeMappings(); var mappings = new HashMap>(); - for (var e : baselineMappings.entrySet()) { + for (var e : baselineOrEmpty.entrySet()) { var key = e.getKey(); - if (!exportedMappings.containsKey(key)) { + if (!exportedOrEmpty.containsKey(key)) { logger.warn("Default realm clientScopeMapping '{}' was deleted in exported realm. It may be reintroduced during import!", key); continue; } - var scopeMappings = exportedMappings.get(key); + var scopeMappings = exportedOrEmpty.get(key); if (javers.compareCollections(e.getValue(), scopeMappings, ScopeMappingRepresentation.class).hasChanges()) { mappings.put(key, scopeMappings); } } - for (var e : exportedMappings.entrySet()) { + for (var e : exportedOrEmpty.entrySet()) { var key = e.getKey(); - if (!baselineMappings.containsKey(key)) { + if (!baselineOrEmpty.containsKey(key)) { mappings.put(key, e.getValue()); } }