javaspringspring-dataldapspring-data-ldap

LDAP: error code 21 - XXXActive: value #0 invalid per syntax - spring-data-ldap


I'm using spring-data-ldap with OOM and my own schema containing a boolean property. When I try to store a value in or read a value from openLDAP, I get the exception

javax.naming.directory.InvalidAttributeValueException: [LDAP: error code 21 - XXXActive: value #0 invalid per syntax]

here's my schema (the actual customer prefix has been replaced with "XXX"):

dn: cn=XXX,cn=schema,cn=config
objectClass: olcSchemaConfig
cn: XXX
olcAttributeTypes: ( 1.3.6.1.4.1.42691910.1.1.1.1 NAME 'XXXActive'
                    DESC 'whether the subscriber has been activated (default false, after self-registration)'
                    EQUALITY booleanMatch
                    SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
                    SINGLE-VALUE )
olcAttributeTypes: ( 1.3.6.1.4.1.42691910.1.1.1.2 NAME 'XXXLocale'
                    DESC 'the locale in which he wants to receive emails'
                    EQUALITY caseExactMatch
                    SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcAttributeTypes: ( 1.3.6.1.4.1.42691910.1.1.1.7 NAME 'XXXQueryFreshnessDate'
                    DESC 'freshness date ...'
                    EQUALITY generalizedTimeMatch
                    SYNTAX 1.3.6.1.4.1.1466.115.121.1.24
                    SINGLE-VALUE )
olcObjectClasses: ( 1.3.6.1.4.1.42691910.1.1.2.1 NAME 'XXXSubscriber'
                    DESC 'a subscriber for ...'
                    SUP top
                    STRUCTURAL
                    MUST ( uid $ userPassword )
                    MAY ( XXXActive $ XXXLocale $ XXXQueryFreshnessDate $ telephoneNumber ) )

here's my entity class:

import lombok.*;
import org.springframework.ldap.odm.annotations.*;

import javax.naming.Name;
import java.io.Serializable;
import java.util.Date;


@Getter
@Setter
@Entry(objectClasses = {"XXXSubscriber", "top"})
@EqualsAndHashCode
@Builder
@AllArgsConstructor
@NoArgsConstructor
public final class XXXSubscriber implements Serializable {

  @Id
  private Name dn;

  @Attribute(name = "uid")
  @DnAttribute(value = "uid", index = 3)
  private String email;

  @Transient
  @DnAttribute(value = "dc", index = 0)
  private String env;

  @Transient
  @DnAttribute(value = "dc", index = 1)
  private String application;

  @Transient
  @DnAttribute(value = "ou", index = 2)
  private String orga;

  @Attribute(name = "telephoneNumber")
  private String phone;

  @Attribute(name = "XXXActive", syntax="1.3.6.1.4.1.1466.115.121.1.7") //TODO throws an error due to invalid syntax false/FALSE
  private boolean active;

  @Attribute(name = "XXXQueryFreshnessDate", syntax = "1.3.6.1.4.1.1466.115.121.1.24") //TODO doesn't work either
  private Date queryFreshnessDate;

  @Attribute(name = "XXXLocale")
  private String locale;

  @Attribute(name = "userPassword", type = Attribute.Type.BINARY)
  private byte[] password;
}

And the according Repo class (which is registered via @org.springframework.data.ldap.repository.config.EnableLdapRepositories()):

import org.springframework.data.ldap.repository.LdapRepository;

public interface XXXSubscriberRepo extends LdapRepository<XXXSubscriber> {

  XXXSubscriber findOneByEmail(String email);

  XXXSubscriber findOneByEmailAndActive(String email, boolean active);

}

And here's an example entry from an ldif:

dn: uid=somebody@example.org,ou=subscribers,dc=applications,dc=test,dc=example,dc=org
objectclass: top
objectclass: XXXSubscriber
uid: somebody@example.org
telephoneNumber: 004940123456789
XXXActive: TRUE
XXXLocale: en
userPassword: {SCRYPT}$e0801$9KXJwk7Q0kFzj07LWKef4TgGmPll0sr1hWxL6kMAQzuluW/87EyaQ4lLkWHNdUInF1GMkm7DAefsa+wUOlMGJg==$3aCwqyWYcS70p6Ib1k/Wh7gKsyZwYq/D3ynZpUUvIfM=
XXXQueryFreshnessDate: 20221108164632.123Z

Is there a possibility, to use the org.springframework.data.ldap.repository.LdapRepository, but register the correct converter for this class, so it can handle the boolean values properly? Per default the boolean values are converted to "true" / "false", but LDAP seems to expect "TRUE"/"FALSE" (MATCH booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7).

btw I'm pretty sure that the Date XXXQueryFreshnessDate (MATCH generalizedTimeMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24) will need to be converted accordingly.

The only examples I found in the internet were either with custom Repositories (not an LdapRepository interface with @EnableLdapRepositories) and Converters.

Is it possible? How?

Best regards,
Alexander.

Update I've debugged into it - when debugging I stepped across org.springframework.ldap.odm.core.impl.DefaultObjectDirectoryMapper#populateSingleValueAttribute - which calls org.springframework.ldap.odm.typeconversion.impl.ConversionServiceConverterManager#convert(Object source, String syntax, Class<T> toClass) with

(!) it's trying to convert the given Boolean to a String.

(!) It's completely ignoring the syntax:

    @Override
    public <T> T convert(Object source, String syntax, Class<T> toClass) {
        return conversionService.convert(source, toClass);
    }

(i) conversionService is of type org.springframework.core.convert.support.DefaultConversionService.


Solution

  • I managed to get this thing going by wiring my own ConversionService / ConversionServiceConverterManager into the LdapTemplate like this:

      @Bean
      public DefaultConversionService myObjectDirectoryMapper(LdapTemplate ldapTemplate) {
        DefaultObjectDirectoryMapper objectDirectoryMapper = (DefaultObjectDirectoryMapper) ldapTemplate.getObjectDirectoryMapper();
        DefaultConversionService conversionService = new DefaultConversionService();
    
    // own implementations
        conversionService.addConverter(new BooleanToStringConverter());
        conversionService.addConverter(new StringToBooleanConverter());
    
    // implementations from https://mvnrepository.com/artifact/org.ldaptive/ldaptive-beans/2.1.1
        conversionService.addConverter(new StringToZonedDateTimeConverter());
        conversionService.addConverter(new ZonedDateTimeToStringConverter());
    
        ConversionServiceConverterManager converterManager = new ConversionServiceConverterManager(conversionService);
        objectDirectoryMapper.setConverterManager(converterManager);
    
        return conversionService;
      }
    

    with my converter classes

    class BooleanToStringConverter
      implements org.springframework.core.convert.converter.Converter<Boolean, String> {
    
      @Override
      public String convert(Boolean source) {
        return source.toString().toUpperCase();
      }
    
    }
    

    and

    class StringToBooleanConverter
      implements org.springframework.core.convert.converter.Converter<String, Boolean> {
    
      @Override
      public Boolean convert(String source) {
        return Boolean.parseBoolean(source.toLowerCase());
      }
    
    }