Fix ReadOnlyAttributeUnchangedValidator to handle omitted read-only attributes

When a read-only attribute exists on a user but is not provided in an
update request (value == null), the isUnchanged() method incorrectly
returned false, causing a 400 error with
'updateReadOnlyAttributesRejectedMessage'.

This adds a symmetric null check: if an attribute exists on the user but
was omitted from the request, treat it as unchanged. The existing logic
only handled the reverse case (existingValue == null && isBlank(value)).

Closes keycloak/keycloak#49272

Signed-off-by: Bruce Arctor <5032356+brucearctor@users.noreply.github.com>
This commit is contained in:
Bruce Arctor 2026-05-24 21:20:54 -07:00
parent 94dcc24a8d
commit 8857c1b665
No known key found for this signature in database
2 changed files with 77 additions and 1 deletions

View file

@ -90,12 +90,18 @@ public class ReadOnlyAttributeUnchangedValidator implements SimpleValidator {
return context;
}
private boolean isUnchanged(String existingValue, String value) {
// package-private for testing
boolean isUnchanged(String existingValue, String value) {
if (existingValue == null && isBlank(value)) {
// if attribute not set to the user and value is blank/null, then pass validation
return true;
}
if (existingValue != null && value == null) {
// if attribute exists on the user but was not provided in the request, treat as unchanged
return true;
}
return ObjectUtil.isEqualOrBothNull(existingValue, value);
}

View file

@ -0,0 +1,70 @@
/*
* Copyright 2024 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.userprofile.validator;
import org.junit.Assert;
import org.junit.Test;
/**
* Tests for {@link ReadOnlyAttributeUnchangedValidator#isUnchanged(String, String)}.
*/
public class ReadOnlyAttributeUnchangedValidatorTest {
private final ReadOnlyAttributeUnchangedValidator validator = new ReadOnlyAttributeUnchangedValidator();
@Test
public void bothNull() {
Assert.assertTrue("both null should be unchanged", validator.isUnchanged(null, null));
}
@Test
public void existingNullValueBlank() {
Assert.assertTrue("existing null and blank value should be unchanged", validator.isUnchanged(null, ""));
}
@Test
public void existingNullValueWhitespace() {
Assert.assertTrue("existing null and whitespace value should be unchanged", validator.isUnchanged(null, " "));
}
@Test
public void existingNullValueNonBlank() {
Assert.assertFalse("existing null and non-blank value should be changed", validator.isUnchanged(null, "newValue"));
}
@Test
public void existingValueAndValueNull_omittedAttribute() {
// This is the bug fix: attribute exists on user but was omitted from the update request
Assert.assertTrue("existing value with null (omitted) value should be unchanged",
validator.isUnchanged("existingValue", null));
}
@Test
public void bothEqual() {
Assert.assertTrue("equal values should be unchanged", validator.isUnchanged("same", "same"));
}
@Test
public void bothDifferent() {
Assert.assertFalse("different values should be changed", validator.isUnchanged("old", "new"));
}
@Test
public void existingValueAndValueBlank() {
Assert.assertFalse("existing value with blank value should be changed", validator.isUnchanged("existingValue", ""));
}
}