Skip to content

Commit

Permalink
Group normalization
Browse files Browse the repository at this point in the history
  • Loading branch information
sonOfRa committed Dec 19, 2022
1 parent 8a31c89 commit a3b0930
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 25 deletions.
Expand Up @@ -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;
Expand Down Expand Up @@ -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")));
}
}
Expand Up @@ -20,21 +20,29 @@

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;

@Service
@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE")
public class AttributeNormalizationService {

public <T> Map<String, T> normalizeAttributes(Map<String, T> exportedAttributes, Map<String, T> baselineAttributes) {
Map<String, T> exportedOrEmpty = exportedAttributes == null ? Map.of() : exportedAttributes;
Map<String, T> baselineOrEmpty = baselineAttributes == null ? Map.of() : baselineAttributes;
var normalizedAttributes = new HashMap<String, T>();
private final Javers unOrderedJavers;

public AttributeNormalizationService(Javers unOrderedJavers) {
this.unOrderedJavers = unOrderedJavers;
}

public Map<String, String> normalizeStringAttributes(Map<String, String> exportedAttributes, Map<String, String> baselineAttributes) {
Map<String, String> exportedOrEmpty = exportedAttributes == null ? Map.of() : exportedAttributes;
Map<String, String> baselineOrEmpty = baselineAttributes == null ? Map.of() : baselineAttributes;
var normalizedAttributes = new HashMap<String, String>();
for (var entry : baselineOrEmpty.entrySet()) {
var attributeName = entry.getKey();
var baselineAttribute = entry.getValue();
Expand All @@ -47,4 +55,40 @@ public <T> Map<String, T> normalizeAttributes(Map<String, T> exportedAttributes,
normalizedAttributes.putAll(exportedOrEmpty);
return normalizedAttributes.isEmpty() ? null : normalizedAttributes;
}

public Map<String, List<String>> normalizeListAttributes(Map<String, List<String>> exportedAttributes,
Map<String, List<String>> baselineAttributes) {
Map<String, List<String>> exportedOrEmpty = exportedAttributes == null ? Map.of() : exportedAttributes;
Map<String, List<String>> baselineOrEmpty = baselineAttributes == null ? Map.of() : baselineAttributes;
var normalizedAttributes = new HashMap<String, List<String>>();
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<String, List<String>> exportedAttributes, Map<String, List<String>> baselineAttributes) {
Map<String, List<String>> exportedOrEmpty = exportedAttributes == null ? Map.of() : exportedAttributes;
Map<String, List<String>> 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;
}

}
Expand Up @@ -59,14 +59,16 @@ public ClientNormalizationService(Javers javers,
}

public List<ClientRepresentation> normalizeClients(RealmRepresentation exportedRealm, RealmRepresentation baselineRealm) {
List<ClientRepresentation> exportedOrEmpty = exportedRealm.getClients() == null ? List.of() : exportedRealm.getClients();
List<ClientRepresentation> baselineOrEmpty = baselineRealm.getClients() == null ? List.of() : baselineRealm.getClients();
var exportedClientMap = new HashMap<String, ClientRepresentation>();
for (var exportedClient : exportedRealm.getClients()) {
for (var exportedClient : exportedOrEmpty) {
exportedClientMap.put(exportedClient.getClientId(), exportedClient);
}

var baselineClientMap = new HashMap<String, ClientRepresentation>();
var clients = new ArrayList<ClientRepresentation>();
for (var baselineRealmClient : baselineRealm.getClients()) {
for (var baselineRealmClient : baselineOrEmpty) {
var clientId = baselineRealmClient.getClientId();
baselineClientMap.put(clientId, baselineRealmClient);
var exportedClient = exportedClientMap.get(clientId);
Expand Down
@@ -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<GroupRepresentation> normalizeGroups(List<GroupRepresentation> exportedGroups, List<GroupRepresentation> baselineGroups) {
List<GroupRepresentation> exportedOrEmpty = exportedGroups == null ? List.of() : exportedGroups;
List<GroupRepresentation> 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<GroupRepresentation>();
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<String, GroupRepresentation> exportedSubGroups = exportedGroup.getSubGroups().stream()
.collect(Collectors.toMap(GroupRepresentation::getPath, Function.identity()));
Map<String, GroupRepresentation> 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<GroupRepresentation> 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());
}
}
}
}
}
Expand Up @@ -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
Expand All @@ -61,6 +62,7 @@ public RealmNormalizationService(NormalizationKeycloakConfigProperties keycloakC
ClientScopeNormalizationService clientScopeNormalizationService,
RoleNormalizationService roleNormalizationService,
AttributeNormalizationService attributeNormalizationService,
GroupNormalizationService groupNormalizationService,
JaversUtil javersUtil) {
this.keycloakConfigProperties = keycloakConfigProperties;
this.javers = javers;
Expand All @@ -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?
Expand Down Expand Up @@ -123,7 +126,8 @@ 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()));
Expand All @@ -132,6 +136,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;
}

Expand Down
Expand Up @@ -35,7 +35,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -89,14 +88,14 @@ public List<RoleRepresentation> normalizeRealmRoles(List<RoleRepresentation> 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());
Expand All @@ -109,13 +108,6 @@ private boolean compositesChanged(RoleRepresentation.Composites exportedComposit
return unOrderedJavers.compare(baselineComposites, exportedComposites).hasChanges();
}

private boolean attributesChanged(Map<String, List<String>> exportedAttributes, Map<String, List<String>> baselineAttributes) {
var exportedOrEmpty = exportedAttributes == null ? Map.of() : exportedAttributes;
var baselineOrEmpty = baselineAttributes == null ? Map.of() : baselineAttributes;

return !Objects.equals(exportedOrEmpty, baselineOrEmpty);
}

public Map<String, List<RoleRepresentation>> normalizeClientRoles(Map<String, List<RoleRepresentation>> exportedClientRoles,
Map<String, List<RoleRepresentation>> baselineClientRoles) {
Map<String, List<RoleRepresentation>> exportedOrEmpty = exportedClientRoles == null ? Map.of() : exportedClientRoles;
Expand Down

0 comments on commit a3b0930

Please sign in to comment.