Keep same name on update for LDAP attributes

Closes https://github.com/keycloak/keycloak/issues/23888
This commit is contained in:
rmartinc 2023-10-11 11:53:31 +02:00 committed by Alexander Schwartz
parent 64836680d7
commit 6963364514
2 changed files with 51 additions and 18 deletions

View file

@ -20,6 +20,7 @@ package org.keycloak.storage.ldap.idm.model;
import org.jboss.logging.Logger;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
@ -48,8 +49,8 @@ public class LDAPObject {
private final Map<String, Set<String>> attributes = new HashMap<>();
// Copy of "attributes" containing lower-cased keys
private final Map<String, Set<String>> lowerCasedAttributes = new HashMap<>();
// Copy of "attributes" containing lower-cased keys and original case-sensitive attribute name
private final Map<String, Map.Entry<String, Set<String>>> lowerCasedAttributes = new HashMap<>();
// range attributes are always read from 0 to max so just saving the top value
private final Map<String, Integer> rangedAttributes = new HashMap<>();
@ -72,8 +73,8 @@ public class LDAPObject {
for (String name : mandatoryAttributeNames) {
name = name.toLowerCase();
this.mandatoryAttributeNames.add(name);
Set<String> values = lowerCasedAttributes.get(name);
if (values == null || values.isEmpty()) {
Map.Entry<String, Set<String>> entry = lowerCasedAttributes.get(name);
if (entry == null || entry.getValue().isEmpty()) {
this.mandatoryAttributeNamesRemaining.add(name);
}
}
@ -132,6 +133,7 @@ public class LDAPObject {
/**
* Useful when single value will be used as the "RDN" attribute. Which will be most of the cases
* @param rdnAttributeName The RDN of the ldap object
*/
public void setRdnAttributeName(String rdnAttributeName) {
this.rdnAttributeNames.clear();
@ -156,14 +158,22 @@ public class LDAPObject {
}
public void setAttribute(String attributeName, Set<String> attributeValue) {
attributes.put(attributeName, attributeValue);
attributeName = attributeName.toLowerCase();
lowerCasedAttributes.put(attributeName, attributeValue);
final String attributeNameLowerCase = attributeName.toLowerCase();
final Set<String> valueSet = attributeValue == null? Collections.emptySet() : attributeValue;
Map.Entry<String, Set<String>> entry = lowerCasedAttributes.get(attributeNameLowerCase);
if (entry == null) {
attributes.put(attributeName, valueSet);
lowerCasedAttributes.put(attributeNameLowerCase, Map.entry(attributeName, valueSet));
} else {
// existing entry, maintain previous case for the attribute name
attributes.put(entry.getKey(), valueSet);
lowerCasedAttributes.put(attributeNameLowerCase, Map.entry(entry.getKey(), valueSet));
}
if (consumerOnMandatoryAttributesComplete != null) {
if (!attributeValue.isEmpty()) {
mandatoryAttributeNamesRemaining.remove(attributeName);
} else if (mandatoryAttributeNames.contains(attributeName)) {
mandatoryAttributeNamesRemaining.add(attributeName);
if (!valueSet.isEmpty()) {
mandatoryAttributeNamesRemaining.remove(attributeNameLowerCase);
} else if (mandatoryAttributeNames.contains(attributeNameLowerCase)) {
mandatoryAttributeNamesRemaining.add(attributeNameLowerCase);
}
executeConsumerOnMandatoryAttributesComplete();
}
@ -171,20 +181,20 @@ public class LDAPObject {
// Case-insensitive
public String getAttributeAsString(String name) {
Set<String> attrValue = lowerCasedAttributes.get(name.toLowerCase());
if (attrValue == null || attrValue.size() == 0) {
Map.Entry<String, Set<String>> entry = lowerCasedAttributes.get(name.toLowerCase());
if (entry == null || entry.getValue().isEmpty()) {
return null;
} else if (attrValue.size() > 1) {
logger.warnf("Expected String but attribute '%s' has more values '%s' on object '%s' . Returning just first value", name, attrValue, dn);
} else if (entry.getValue().size() > 1) {
logger.warnf("Expected String but attribute '%s' has more values '%s' on object '%s' . Returning just first value", name, entry.getValue(), dn);
}
return attrValue.iterator().next();
return entry.getValue().iterator().next();
}
// Case-insensitive. Return null if there is not value of attribute with given name or set with all values otherwise
public Set<String> getAttributeAsSet(String name) {
Set<String> values = lowerCasedAttributes.get(name.toLowerCase());
return (values == null) ? null : new LinkedHashSet<>(values);
Map.Entry<String, Set<String>> entry = lowerCasedAttributes.get(name.toLowerCase());
return (entry == null) ? null : new LinkedHashSet<>(entry.getValue());
}
public boolean isRangeComplete(String name) {

View file

@ -25,6 +25,7 @@ import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.Profile;
import org.keycloak.component.ComponentModel;
@ -78,6 +79,7 @@ import org.keycloak.testsuite.util.OAuthClient;
import javax.naming.AuthenticationException;
import jakarta.ws.rs.core.Response;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -702,7 +704,28 @@ public class LDAPProvidersIntegrationTest extends AbstractLDAPTest {
UserModel user = session.users().getUserByUsername(appRealm, "johnzip");
String postalCode = user.getFirstAttribute("postal_code");
Assert.assertEquals("12398", postalCode);
});
// modify postal_code in the user
RealmResource realm = testRealm();
List<UserRepresentation> users = realm.users().search("johnzip", true);
Assert.assertEquals("User not found", 1, users.size());
UserRepresentation user = users.iterator().next();
Assert.assertEquals("Incorrect postal code", Collections.singletonList("12398"), user.getAttributes().get("postal_code"));
UserResource userRes = realm.users().get(user.getId());
user.getAttributes().put("postal_code", Collections.singletonList("9876"));
userRes.update(user);
user = userRes.toRepresentation();
Assert.assertEquals("Incorrect postal code", Collections.singletonList("9876"), user.getAttributes().get("postal_code"));
// ensure the ldap contains the correct value
testingClient.server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
RealmModel appRealm = ctx.getRealm();
LDAPStorageProvider ldapProvider = ctx.getLdapProvider();
LDAPObject ldapUser = ldapProvider.loadLDAPUserByUsername(appRealm, "johnzip");
Assert.assertEquals("Incorrect postal code", "9876", ldapUser.getAttributeAsString("POstalCode"));
});
}