From 8857c1b665ddf0bb36d0e7cbfff69a1ac08dcbcf Mon Sep 17 00:00:00 2001 From: Bruce Arctor <5032356+brucearctor@users.noreply.github.com> Date: Sun, 24 May 2026 21:20:54 -0700 Subject: [PATCH] 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> --- .../ReadOnlyAttributeUnchangedValidator.java | 8 ++- ...adOnlyAttributeUnchangedValidatorTest.java | 70 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 services/src/test/java/org/keycloak/userprofile/validator/ReadOnlyAttributeUnchangedValidatorTest.java diff --git a/services/src/main/java/org/keycloak/userprofile/validator/ReadOnlyAttributeUnchangedValidator.java b/services/src/main/java/org/keycloak/userprofile/validator/ReadOnlyAttributeUnchangedValidator.java index 50de940baf8..ecfcbbe2baf 100644 --- a/services/src/main/java/org/keycloak/userprofile/validator/ReadOnlyAttributeUnchangedValidator.java +++ b/services/src/main/java/org/keycloak/userprofile/validator/ReadOnlyAttributeUnchangedValidator.java @@ -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); } diff --git a/services/src/test/java/org/keycloak/userprofile/validator/ReadOnlyAttributeUnchangedValidatorTest.java b/services/src/test/java/org/keycloak/userprofile/validator/ReadOnlyAttributeUnchangedValidatorTest.java new file mode 100644 index 00000000000..802f7e2b728 --- /dev/null +++ b/services/src/test/java/org/keycloak/userprofile/validator/ReadOnlyAttributeUnchangedValidatorTest.java @@ -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", "")); + } +}