Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions core/src/main/java/org/springframework/ldap/support/LdapUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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 <a href=
* "https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/c92a27b1-c772-4fa7-a432-15df5f1b66a1">SID
* String Format Syntax</a>
*/
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).
*
* <p>
* The binary SID format consists of:
* <ul>
* <li>1 byte for the revision, which must be {@code 1}</li>
* <li>1 byte for the number of sub-authorities, which must not exceed {@code 15}</li>
* <li>6 bytes for the identifier authority</li>
* <li>4 bytes for each sub-authority</li>
* </ul>
* @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 <a href=
* "https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/f992ad60-0fe4-4b87-9fed-beb478836861">SID--Packet
* Representation</a>
*/
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
31 changes: 31 additions & 0 deletions modules/ROOT/pages/odm.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, byte @Nullable []> {

@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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, byte @Nullable []> {

@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<byte[], String> {

@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);
}

}

}
Original file line number Diff line number Diff line change
@@ -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;
}

}