Avoid iterating and updating all group policies when removing groups (#31057)

Closes #31056

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2024-07-08 08:57:20 -03:00 committed by GitHub
parent 07040ea57a
commit cbf7f208fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 60 additions and 108 deletions

View file

@ -59,6 +59,10 @@ public class GroupPolicyProvider implements PolicyProvider {
for (GroupPolicyRepresentation.GroupDefinition definition : policy.getGroups()) { for (GroupPolicyRepresentation.GroupDefinition definition : policy.getGroups()) {
GroupModel allowedGroup = realm.getGroupById(definition.getId()); GroupModel allowedGroup = realm.getGroupById(definition.getId());
if (allowedGroup == null) {
continue;
}
for (int i = 0; i < groupsClaim.size(); i++) { for (int i = 0; i < groupsClaim.size(); i++) {
String group = groupsClaim.asString(i); String group = groupsClaim.asString(i);
@ -84,4 +88,4 @@ public class GroupPolicyProvider implements PolicyProvider {
public void close() { public void close() {
} }
} }

View file

@ -33,10 +33,13 @@ import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.policy.provider.PolicyProvider; import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.authorization.policy.provider.PolicyProviderFactory; import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
import org.keycloak.models.GroupModel; import org.keycloak.models.GroupModel;
import org.keycloak.models.GroupProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation; import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation.GroupDefinition;
import org.keycloak.representations.idm.authorization.PolicyRepresentation; import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
@ -79,7 +82,7 @@ public class GroupPolicyProviderFactory implements PolicyProviderFactory<GroupPo
representation.setGroupsClaim(policy.getConfig().get("groupsClaim")); representation.setGroupsClaim(policy.getConfig().get("groupsClaim"));
try { try {
representation.setGroups(getGroupsDefinition(policy.getConfig())); representation.setGroups(getGroupsDefinition(policy.getConfig(), authorization));
} catch (IOException cause) { } catch (IOException cause) {
throw new RuntimeException("Failed to deserialize groups", cause); throw new RuntimeException("Failed to deserialize groups", cause);
} }
@ -104,7 +107,7 @@ public class GroupPolicyProviderFactory implements PolicyProviderFactory<GroupPo
@Override @Override
public void onImport(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorization) { public void onImport(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorization) {
try { try {
updatePolicy(policy, representation.getConfig().get("groupsClaim"), getGroupsDefinition(representation.getConfig()), authorization); updatePolicy(policy, representation.getConfig().get("groupsClaim"), getGroupsDefinition(representation.getConfig(), authorization), authorization);
} catch (IOException cause) { } catch (IOException cause) {
throw new RuntimeException("Failed to deserialize groups", cause); throw new RuntimeException("Failed to deserialize groups", cause);
} }
@ -118,6 +121,11 @@ public class GroupPolicyProviderFactory implements PolicyProviderFactory<GroupPo
for (GroupPolicyRepresentation.GroupDefinition definition: groups) { for (GroupPolicyRepresentation.GroupDefinition definition: groups) {
GroupModel group = authorization.getRealm().getGroupById(definition.getId()); GroupModel group = authorization.getRealm().getGroupById(definition.getId());
if (group == null) {
continue;
}
definition.setId(null); definition.setId(null);
definition.setPath(ModelToRepresentation.buildGroupPath(group)); definition.setPath(ModelToRepresentation.buildGroupPath(group));
} }
@ -144,8 +152,6 @@ public class GroupPolicyProviderFactory implements PolicyProviderFactory<GroupPo
@Override @Override
public void postInit(KeycloakSessionFactory factory) { public void postInit(KeycloakSessionFactory factory) {
factory.register(event -> {
});
} }
@Override @Override
@ -164,41 +170,11 @@ public class GroupPolicyProviderFactory implements PolicyProviderFactory<GroupPo
config.put("groupsClaim", groupsClaim); config.put("groupsClaim", groupsClaim);
} }
List<GroupModel> topLevelGroups = authorization.getKeycloakSession().groups().getTopLevelGroupsStream(authorization.getRealm()).collect(Collectors.toList());
for (GroupPolicyRepresentation.GroupDefinition definition : groups) { for (GroupPolicyRepresentation.GroupDefinition definition : groups) {
GroupModel group = null; GroupModel group = getGroup(authorization, definition);
if (definition.getId() != null) {
group = authorization.getRealm().getGroupById(definition.getId());
}
String path = definition.getPath();
if (group == null && path != null) {
String canonicalPath = path.startsWith("/") ? path.substring(1, path.length()) : path;
if (canonicalPath != null) {
String[] parts = canonicalPath.split("/");
GroupModel parent = null;
for (String part : parts) {
if (parent == null) {
parent = topLevelGroups.stream().filter(groupModel -> groupModel.getName().equals(part)).findFirst().orElseThrow(() -> new RuntimeException("Top level group with name [" + part + "] not found"));
} else {
group = parent.getSubGroupsStream().filter(groupModel -> groupModel.getName().equals(part)).findFirst().orElseThrow(() -> new RuntimeException("Group with name [" + part + "] not found"));
parent = group;
}
}
if (parts.length == 1) {
group = parent;
}
}
}
if (group == null) { if (group == null) {
throw new RuntimeException("Group with id [" + definition.getId() + "] not found"); continue;
} }
definition.setId(group.getId()); definition.setId(group.getId());
@ -214,7 +190,47 @@ public class GroupPolicyProviderFactory implements PolicyProviderFactory<GroupPo
policy.setConfig(config); policy.setConfig(config);
} }
private Set<GroupPolicyRepresentation.GroupDefinition> getGroupsDefinition(Map<String, String> config) throws IOException { private GroupModel getGroup(AuthorizationProvider authorization, GroupDefinition definition) {
RealmModel realm = authorization.getRealm();
KeycloakSession session = authorization.getKeycloakSession();
GroupProvider groups = session.groups();
if (definition.getId() != null) {
return realm.getGroupById(definition.getId());
}
GroupModel group = null;
String path = definition.getPath();
if (path != null) {
String canonicalPath = path.startsWith("/") ? path.substring(1) : path;
String[] parts = canonicalPath.split("/");
GroupModel parent = null;
for (String part : parts) {
if (parent == null) {
parent = groups.getGroupByName(realm, null, part);
if (parent == null) {
return null;
}
} else {
group = groups.getGroupByName(realm, parent, part);
if (group == null) {
return null;
}
parent = group;
}
}
if (parts.length == 1) {
group = parent;
}
}
return group;
}
private Set<GroupPolicyRepresentation.GroupDefinition> getGroupsDefinition(Map<String, String> config, AuthorizationProvider authorization) throws IOException {
String groups = config.get("groups"); String groups = config.get("groups");
if (groups == null) { if (groups == null) {
@ -222,6 +238,7 @@ public class GroupPolicyProviderFactory implements PolicyProviderFactory<GroupPo
} }
return Arrays.stream(JsonSerialization.readValue(groups, GroupPolicyRepresentation.GroupDefinition[].class)) return Arrays.stream(JsonSerialization.readValue(groups, GroupPolicyRepresentation.GroupDefinition[].class))
.filter(d -> getGroup(authorization, d) != null)
.sorted() .sorted()
.collect(Collectors.toCollection(LinkedHashSet::new)); .collect(Collectors.toCollection(LinkedHashSet::new));
} }

View file

@ -22,11 +22,9 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.keycloak.authorization.store.syncronization.ClientApplicationSynchronizer; import org.keycloak.authorization.store.syncronization.ClientApplicationSynchronizer;
import org.keycloak.authorization.store.syncronization.GroupSynchronizer;
import org.keycloak.authorization.store.syncronization.RealmSynchronizer; import org.keycloak.authorization.store.syncronization.RealmSynchronizer;
import org.keycloak.authorization.store.syncronization.Synchronizer; import org.keycloak.authorization.store.syncronization.Synchronizer;
import org.keycloak.authorization.store.syncronization.UserSynchronizer; import org.keycloak.authorization.store.syncronization.UserSynchronizer;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.ClientModel.ClientRemovedEvent; import org.keycloak.models.ClientModel.ClientRemovedEvent;
import org.keycloak.models.RealmModel.RealmRemovedEvent; import org.keycloak.models.RealmModel.RealmRemovedEvent;
@ -50,7 +48,6 @@ public interface AuthorizationStoreFactory extends ProviderFactory<StoreFactory>
synchronizers.put(ClientRemovedEvent.class, new ClientApplicationSynchronizer()); synchronizers.put(ClientRemovedEvent.class, new ClientApplicationSynchronizer());
synchronizers.put(RealmRemovedEvent.class, new RealmSynchronizer()); synchronizers.put(RealmRemovedEvent.class, new RealmSynchronizer());
synchronizers.put(UserRemovedEvent.class, new UserSynchronizer()); synchronizers.put(UserRemovedEvent.class, new UserSynchronizer());
synchronizers.put(GroupModel.GroupRemovedEvent.class, new GroupSynchronizer());
factory.register(event -> { factory.register(event -> {
try { try {

View file

@ -1,66 +0,0 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.
*/
package org.keycloak.authorization.store.syncronization;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
import org.keycloak.authorization.store.PolicyStore;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class GroupSynchronizer implements Synchronizer<GroupModel.GroupRemovedEvent> {
@Override
public void synchronize(GroupModel.GroupRemovedEvent event, KeycloakSessionFactory factory) {
ProviderFactory<AuthorizationProvider> providerFactory = factory.getProviderFactory(AuthorizationProvider.class);
AuthorizationProvider authorizationProvider = providerFactory.create(event.getKeycloakSession());
StoreFactory storeFactory = authorizationProvider.getStoreFactory();
PolicyStore policyStore = storeFactory.getPolicyStore();
GroupModel group = event.getGroup();
Map<Policy.FilterOption, String[]> attributes = new EnumMap<>(Policy.FilterOption.class);
attributes.put(Policy.FilterOption.TYPE, new String[] {"group"});
attributes.put(Policy.FilterOption.CONFIG, new String[] {"groups", group.getId()});
attributes.put(Policy.FilterOption.ANY_OWNER, Policy.FilterOption.EMPTY_FILTER);
List<Policy> search = policyStore.find(null, attributes, null, null);
for (Policy policy : search) {
PolicyProviderFactory policyFactory = authorizationProvider.getProviderFactory(policy.getType());
GroupPolicyRepresentation representation = GroupPolicyRepresentation.class.cast(policyFactory.toRepresentation(policy, authorizationProvider));
Set<GroupPolicyRepresentation.GroupDefinition> groups = representation.getGroups();
groups.removeIf(groupDefinition -> groupDefinition.getId().equals(group.getId()));
policyFactory.onUpdate(policy, representation, authorizationProvider);
}
}
}

View file

@ -228,7 +228,7 @@ public class GroupPolicyManagementTest extends AbstractPolicyManagementTest {
GroupsResource groups = getRealm().groups(); GroupsResource groups = getRealm().groups();
GroupRepresentation group = groups.groups("Group G", null, null).get(0); GroupRepresentation group = groups.groups("Group G", null, null).get(0);
groups.group(group.getId()).remove(); groups.group(group.getId()).remove();
GroupPolicyRepresentation actual = getClient().authorization().policies().group().findByName(representation.getName()); GroupPolicyRepresentation actual = getClient().authorization().policies().group().findByName(representation.getName());