From 28c7a4c59136535ad7e357221fcac5a5ecac3234 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Wed, 2 Sep 2026 20:09:20 +0700 Subject: [PATCH] Add `LdapUtils#isLdapSid` for String and byte array Provide `LdapUtils#isLdapSid` methods for checking whether a `String` or `byte[]` value represents a valid LDAP SID. See gh-1620 Signed-off-by: Tran Ngoc Nhan --- .../ldap/support/LdapUtils.java | 59 ++++++++ .../ldap/support/LdapUtilsTests.java | 39 +++++ modules/ROOT/pages/odm.adoc | 31 ++++ .../StringBinaryConversionTests.java | 133 ++++++++++++++++++ .../converter/UnitTestPersonBinaryType.java | 65 +++++++++ 5 files changed, 327 insertions(+) create mode 100644 test/integration-tests/src/test/java/org/springframework/ldap/itest/converter/StringBinaryConversionTests.java create mode 100644 test/integration-tests/src/test/java/org/springframework/ldap/itest/converter/UnitTestPersonBinaryType.java diff --git a/core/src/main/java/org/springframework/ldap/support/LdapUtils.java b/core/src/main/java/org/springframework/ldap/support/LdapUtils.java index fa60cb919..44e63a0eb 100644 --- a/core/src/main/java/org/springframework/ldap/support/LdapUtils.java +++ b/core/src/main/java/org/springframework/ldap/support/LdapUtils.java @@ -40,6 +40,7 @@ import org.springframework.ldap.NamingException; import org.springframework.ldap.NoSuchAttributeException; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Generic utility methods for working with LDAP. Mainly for internal use within the @@ -55,6 +56,8 @@ public final class LdapUtils { private static final int HEX = 16; + private static final String SID_REGEX = "^S-1-(?:(?:0|[1-9][0-9]{0,9})|0x[0-9a-fA-F]{12})(?:-(?:0|[1-9][0-9]{0,9}))*$"; + /** * Not to be instantiated. */ @@ -760,6 +763,62 @@ static String toHexString(final byte[] b) { return sb.toString(); } + /** + * Determines whether the given string represents a valid LDAP Security Identifier + * (SID). + * @param sid the SID string to validate (can be {@code null}) + * @return {@code true} if the SID is non-empty and matches the expected SID format; + * {@code false} otherwise + * @since 4.1 + * @see #isLdapSid(byte[]) + * @see SID + * String Format Syntax + */ + public static boolean isLdapSid(@Nullable String sid) { + return StringUtils.hasText(sid) && sid.matches(SID_REGEX); + } + + /** + * Determines whether the given byte array represents a valid binary LDAP Security + * Identifier (SID). + * + *

+ * The binary SID format consists of: + *

+ * @param sid the binary SID to validate (can be {@code null}) + * @return {@code true} if the byte array represents a valid binary SID, otherwise + * {@code false} + * @since 4.1 + * @see #isLdapSid(String) + * @see SID--Packet + * Representation + */ + public static boolean isLdapSid(byte @Nullable [] sid) { + if (sid == null || sid.length < 8) { + return false; + } + + int revision = sid[0] & 0xFF; + if (revision != 1) { + return false; + } + + int subAuthorityCount = sid[1] & 0xFF; + if (subAuthorityCount > 15) { + return false; + } + + int expectedLength = 8 + (subAuthorityCount * 4); + return sid.length == expectedLength; + } + /** * An {@link AttributeValueCallbackHandler} to collect values in a supplied * collection. diff --git a/core/src/test/java/org/springframework/ldap/support/LdapUtilsTests.java b/core/src/test/java/org/springframework/ldap/support/LdapUtilsTests.java index 9086a2bea..d6f89452b 100644 --- a/core/src/test/java/org/springframework/ldap/support/LdapUtilsTests.java +++ b/core/src/test/java/org/springframework/ldap/support/LdapUtilsTests.java @@ -238,6 +238,45 @@ public void testConvertStringSidToBinary() throws Exception { } } + @Test + public void isLdapSidString() { + + assertThat(LdapUtils.isLdapSid("S-1-5")).isTrue(); + assertThat(LdapUtils.isLdapSid("S-1-1-0")).isTrue(); + assertThat(LdapUtils.isLdapSid("S-1-5-32-573")).isTrue(); + assertThat(LdapUtils.isLdapSid("S-1-5-21-1-2-3-4")).isTrue(); + assertThat(LdapUtils.isLdapSid("S-1-4294967295-21")).isTrue(); + assertThat(LdapUtils.isLdapSid("S-1-5-21-2562418665-3218585558-1813906818-1576")).isTrue(); + + assertThat(LdapUtils.isLdapSid("S-1")).isFalse(); + assertThat(LdapUtils.isLdapSid("S-1-01-21")).isFalse(); + } + + @Test + public void isLdapSidByteArray() { + + assertThat(LdapUtils.isLdapSid(new byte[] { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05 })).isTrue(); + assertThat(LdapUtils + .isLdapSid(new byte[] { 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00 })).isTrue(); + assertThat(LdapUtils.isLdapSid(new byte[] { 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x20, 0x00, 0x00, + 0x00, 0x3D, 0x02, 0x00, 0x00 })) + .isTrue(); + assertThat(LdapUtils.isLdapSid(new byte[] { 0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x15, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00 })) + .isTrue(); + assertThat(LdapUtils.isLdapSid(new byte[] { 0x01, 0x01, 0x00, 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, + (byte) 0xFF, 0x15, 0x00, 0x00, 0x00 })) + .isTrue(); + assertThat(LdapUtils.isLdapSid(new byte[] { 0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x15, 0x00, 0x00, + 0x00, (byte) 0xE9, 0x67, (byte) 0xBB, (byte) 0x98, (byte) 0xD6, (byte) 0xB7, (byte) 0xD7, (byte) 0xBF, + (byte) 0x82, 0x05, 0x1E, 0x6C, 0x28, 0x06, 0x00, 0x00 })) + .isTrue(); + + assertThat(LdapUtils.isLdapSid(new byte[] { 0x01, 0x00 })).isFalse(); + assertThat(LdapUtils.isLdapSid(new byte[] { 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05 })).isFalse(); + assertThat(LdapUtils.isLdapSid(new byte[] { 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05 })).isFalse(); + } + @Test public void testNewLdapNameFromLdapName() throws InvalidNameException { LdapName ldapName = new LdapName(EXPECTED_DN_STRING); diff --git a/modules/ROOT/pages/odm.adoc b/modules/ROOT/pages/odm.adoc index 4170ab944..c50922458 100644 --- a/modules/ROOT/pages/odm.adoc +++ b/modules/ROOT/pages/odm.adoc @@ -19,6 +19,37 @@ The Spring LDAP project offers a similar ability with respect to LDAP directorie `LdapTemplate` constructs a default `ObjectDirectoryMapper` which typically renders additional configuration unnecessary. +[NOTE] +==== +Spring's default conversion does not support converting between `String` and `byte[]` because the encoding to use for the conversion cannot be determined automatically. +However, `LdapUtils#isLdapSid` is provided for both `String` and `byte[]` values if you need to check whether a value is a SID. +Alternatively, you can override Spring's default conversion by providing your own converter. For example: + + +[source,java] +---- +import org.springframework.core.convert.converter.Converter; + +public class StringToBinaryConverter implements Converter { + + @Override + public byte @Nullable [] convert(@Nullable String source) { + + if (source == null) { + return null; + } + + if (LdapUtils.isLdapSid(source)) { + return LdapUtils.convertStringSidToBinary(source); + } + + return source.getBytes(StandardCharsets.UTF_8); + } + +} +---- +==== + === Converters with Boot `ObjectDirectoryMapper` supports `ConversionService` which allows you to specify customer ``Converter``s for mapping between Java and LDAP. diff --git a/test/integration-tests/src/test/java/org/springframework/ldap/itest/converter/StringBinaryConversionTests.java b/test/integration-tests/src/test/java/org/springframework/ldap/itest/converter/StringBinaryConversionTests.java new file mode 100644 index 000000000..a7257c72f --- /dev/null +++ b/test/integration-tests/src/test/java/org/springframework/ldap/itest/converter/StringBinaryConversionTests.java @@ -0,0 +1,133 @@ +/* + * Copyright 2006-present the original author or authors. + * + * 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 + * + * https://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.springframework.ldap.itest.converter; + +import java.nio.charset.StandardCharsets; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.support.ConversionServiceFactoryBean; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterRegistry; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.odm.config.ObjectDirectoryMapperConfiguration; +import org.springframework.ldap.odm.core.impl.DefaultObjectDirectoryMapper; +import org.springframework.ldap.support.LdapUtils; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Ngoc Nhan + */ +@ExtendWith(SpringExtension.class) +public class StringBinaryConversionTests { + + @Autowired + private DefaultObjectDirectoryMapper mapper; + + @Test + public void mapToLdapDataEntryWhenBinaryType() { + + UnitTestPersonBinaryType testPerson = new UnitTestPersonBinaryType(); + testPerson.setTestString("testStringValue"); + DirContextAdapter adapter = new DirContextAdapter(); + this.mapper.mapToLdapDataEntry(testPerson, adapter); + assertThat(adapter.getObjectAttribute("testString")).isNotNull() + .isInstanceOf(byte[].class) + .isEqualTo("testStringValue".getBytes(StandardCharsets.UTF_8)); + } + + @Test + public void mapFromLdapDataEntryWhenBinaryAttribute() { + + DirContextAdapter adapter = new DirContextAdapter(); + adapter.setAttributeValues("objectclass", + new String[] { "inetOrgPerson", "organizationalPerson", "person", "top" }); + adapter.setAttributeValue("testBytes", "testBytesValue"); + UnitTestPersonBinaryType testPerson = this.mapper.mapFromLdapDataEntry(adapter, UnitTestPersonBinaryType.class); + assertThat(testPerson).isNotNull(); + assertThat(testPerson.getTestBytes()).isNotNull() + .isInstanceOf(byte[].class) + .isEqualTo("testBytesValue".getBytes(StandardCharsets.UTF_8)); + } + + @Import(ObjectDirectoryMapperConfiguration.class) + @Configuration + static class EmbeddedLdapConfig { + + @Bean + static ConversionServiceFactoryBean conversionService() { + return new ConversionServiceFactoryBean(); + } + + @Autowired + void setup(ConversionService conversionService) { + + if (conversionService instanceof ConverterRegistry registry) { + registry.addConverter(new StringToBinaryConverter()); + registry.addConverter(new BinaryToStringConverter()); + } + } + + } + + static class StringToBinaryConverter implements Converter { + + @Override + public byte @Nullable [] convert(@Nullable String source) { + + if (source == null) { + return null; + } + + if (LdapUtils.isLdapSid(source)) { + return LdapUtils.convertStringSidToBinary(source); + } + + return source.getBytes(StandardCharsets.UTF_8); + } + + } + + static class BinaryToStringConverter implements Converter { + + @Override + public @Nullable String convert(byte @Nullable [] source) { + + if (source == null) { + return null; + } + + if (LdapUtils.isLdapSid(source)) { + return LdapUtils.convertBinarySidToString(source); + } + + // source to be decoded into characters + return new String(source, StandardCharsets.UTF_8); + } + + } + +} diff --git a/test/integration-tests/src/test/java/org/springframework/ldap/itest/converter/UnitTestPersonBinaryType.java b/test/integration-tests/src/test/java/org/springframework/ldap/itest/converter/UnitTestPersonBinaryType.java new file mode 100644 index 000000000..a912e53fe --- /dev/null +++ b/test/integration-tests/src/test/java/org/springframework/ldap/itest/converter/UnitTestPersonBinaryType.java @@ -0,0 +1,65 @@ +/* + * Copyright 2006-present the original author or authors. + * + * 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 + * + * https://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.springframework.ldap.itest.converter; + +import javax.naming.Name; + +import org.springframework.ldap.odm.annotations.Attribute; +import org.springframework.ldap.odm.annotations.Attribute.Type; +import org.springframework.ldap.odm.annotations.Entry; +import org.springframework.ldap.odm.annotations.Id; + +/** + * @author Ngoc Nhan + */ +@Entry(objectClasses = { "inetOrgPerson", "organizationalPerson", "person", "top" }) +public class UnitTestPersonBinaryType { + + @Id + private Name dn; + + @Attribute(name = "testBytes", type = Type.BINARY) + private byte[] testBytes; + + @Attribute(name = "testString", type = Type.BINARY) + private String testString; + + public Name getDn() { + return this.dn; + } + + public void setDn(Name dn) { + this.dn = dn; + } + + public byte[] getTestBytes() { + return this.testBytes; + } + + public void setTestBytes(byte[] testBytes) { + this.testBytes = testBytes; + } + + public String getTestString() { + return this.testString; + } + + public void setTestString(String testString) { + this.testString = testString; + } + +}