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:
+ *
+ * - 1 byte for the revision, which must be {@code 1}
+ * - 1 byte for the number of sub-authorities, which must not exceed {@code 15}
+ * - 6 bytes for the identifier authority
+ * - 4 bytes for each sub-authority
+ *
+ * @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;
+ }
+
+}