Spring LDAP using Java Configuration

Jane picture Jane · Sep 23, 2015 · Viewed 15.3k times · Source

I was following the samples of Spring LDAP project and was trying to convert xml configuration to Java Configuration.

I am trying to do CRUD operations on the LDAP server.

I was able to figure out the following,

This is the xml configuration for the application, which I am hoping to convert into Java Config.

<context:property-placeholder location="classpath:/ldap.properties"
        system-properties-mode="OVERRIDE" />
    <context:annotation-config />

    <ldap:context-source id="contextSource" password="${sample.ldap.password}"
        url="${sample.ldap.url}" username="${sample.ldap.userDn}" base="${sample.ldap.base}" />

    <ldap:ldap-template id="ldapTemplate"
        context-source-ref="contextSource" />

    <!-- This will scan the org.springframework.ldap.samples.useradmin.domain 
        package for interfaces extending CrudRepository (in our case, LdapRepository), 
        automatically creating repository beans based on these interfaces. -->
    <ldap:repositories base-package="com.cazysystems.appstore.model" />

    <!-- This one will never be referenced directly, but the ldap:repositories 
        tag will make sure it will be 'wired in', because the GroupRepo interface 
        extends from an interface that GroupRepoImpl imlements. -->
    <bean class="com.cazysystems.appstore.model.impli.GroupRepoImpl" />

    <bean class="com.cazysystems.appstore.model.impli.DepartmentRepoImpl" />

    <bean class="com.cazysystems.appstore.service.UserService">
        <property name="directoryType" value="${sample.ldap.directory.type}" />
    </bean>

So I have the following classes that are supposed to do the migration,

@Configuration
@EnableLdapRepositories("com.cazysystems.appstore.model")
public class LdapConfiguration {

    @Autowired
    Environment env;

    @Bean
    public LdapContextSource contextSource() {
        LdapContextSource contextSource = new LdapContextSource();
        contextSource.setUrl(env.getRequiredProperty("sample.ldap.url"));
        contextSource.setBase(env.getRequiredProperty("sample.ldap.base"));
        contextSource.setUserDn(env.getRequiredProperty("sample.ldap.userDn"));
        contextSource.setPassword(env
                .getRequiredProperty("sample.ldap.password"));
        return contextSource;
    }

    @Bean
    public LdapTemplate ldapTemplate() {
        return new LdapTemplate(contextSource());
    }

}

and the

 @Configuration
public class AuthenticationConfiguration extends
        GlobalAuthenticationConfigurerAdapter {

    @Autowired
    Environment env;

    @Override
    public void init(AuthenticationManagerBuilder auth) throws Exception {

        auth.ldapAuthentication()
                // .userDetailsContextMapper(userDetailsContextMapper())
                .userDnPatterns(
                        env.getRequiredProperty("ldap.user_dn_patterns"))
                .groupSearchBase(
                        env.getRequiredProperty("ldap.group_search_base"))
                .contextSource().ldif("classpath:setup_data.ldif")
                .url(env.getRequiredProperty("sample.ldap.url"))
                .managerDn("sample.ldap.userDn")
                .managerPassword("sample.ldap.password").port(10389);
    }

}

But when I run the application,

I get the following error,

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'groupRepo': Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: Not an managed type: class com.cazysystems.appstore.model.Group

But under the com.cazysystems.appstore.modelpackage,

I have,

public interface GroupRepo extends LdapRepository<Group>, GroupRepoExtension {
    public final static String USER_GROUP = "ROLE_USER";

    Group findByName(String groupName);

    @Query("(member={0})")
    Collection<Group> findByMember(Name member);
}

and

@Entry(objectClasses = { "groupOfNames", "top" }, base = "ou=Groups")
public final class Group {
    @Id
    private Name id;

    @Attribute(name = "cn")
    @DnAttribute(value = "cn", index = 1)
    private String name;

    @Attribute(name = "description")
    private String description;

    @Attribute(name = "member")
    private Set<Name> members = new HashSet<Name>();

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public Set<Name> getMembers() {
        return members;
    }

    public void addMember(Name newMember) {
        members.add(newMember);
    }

    public void removeMember(Name member) {
        members.remove(member);
    }

    public Name getId() {
        return id;
    }

    public void setId(Name id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

But this annotation is supposed to do the trick, But it is not working,

@EnableLdapRepositories("com.cazysystems.appstore.model")

I am using the following dependencies in my pom.

<dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-ldap</artifactId>
    </dependency>


    <dependency>
        <groupId>org.springframework.ldap</groupId>
        <artifactId>spring-ldap-core</artifactId>
        <version>2.0.3.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.ldap</groupId>
        <artifactId>spring-ldap-core-tiger</artifactId>
        <version>2.0.3.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-commons</artifactId>
    </dependency>

I am not sure what I am doing wrong, because the documentation on this are very hard to find. Please help if you know this stuff.

EDIT:

Here is my Controller class,

@Controller
public class GroupController {

    @Autowired
    private GroupRepo groupRepo;

    @Autowired
    private UserService userService;

    @RequestMapping(value = "/groups", method = GET)
    public String listGroups(ModelMap map) {
        map.put("groups", groupRepo.getAllGroupNames());
        return "listGroups";
    }

    @RequestMapping(value = "/newGroup", method = GET)
    public String initNewGroup() {
        return "newGroup";
    }

    @RequestMapping(value = "/groups", method = POST)
    public String newGroup(Group group) {
        groupRepo.create(group);

        return "redirect:groups/" + group.getName();
    }

    @RequestMapping(value = "/groups/{name}", method = GET)
    public String editGroup(@PathVariable String name, ModelMap map) {
        Group foundGroup = groupRepo.findByName(name);
        map.put("group", foundGroup);

        final Set<User> groupMembers = userService.findAllMembers(foundGroup.getMembers());
        map.put("members", groupMembers);

        Iterable<User> otherUsers = Iterables.filter(userService.findAll(), new Predicate<User>() {
            @Override
            public boolean apply(User user) {
                return !groupMembers.contains(user);
            }
        });
        map.put("nonMembers", Lists.newLinkedList(otherUsers));

        return "editGroup";
    }

    @RequestMapping(value = "/groups/{name}/members", method = POST)
    public String addUserToGroup(@PathVariable String name, @RequestParam String userId) {
        Group group = groupRepo.findByName(name);
        group.addMember(userService.toAbsoluteDn(LdapUtils.newLdapName(userId)));

        groupRepo.save(group);

        return "redirect:/groups/" + name;
    }

    @RequestMapping(value = "/groups/{name}/members", method = DELETE)
    public String removeUserFromGroup(@PathVariable String name, @RequestParam String userId) {
        Group group = groupRepo.findByName(name);
        group.removeMember(userService.toAbsoluteDn(LdapUtils.newLdapName(userId)));

        groupRepo.save(group);

        return "redirect:/groups/" + name;
    }
}

EDIT:

The following is the complete stack trace,

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'groupController': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: private com.cazysystems.appstore.domain.GroupRepo com.eazysystems.appstore.controller.GroupController.groupRepo; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'groupRepo': Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: Not an managed type: class com.cazysystems.appstore.domain.Group
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:334)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1210)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:537)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:476)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:303)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:299)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:755)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:757)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:480)
    at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:118)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:686)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:320)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:957)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:946)
    at com.eazysystems.appstore.Application.main(Application.java:20)
Caused by: org.springframework.beans.factory.BeanCreationException: Could not autowire field: private com.cazysystems.appstore.domain.GroupRepo com.eazysystems.appstore.controller.GroupController.groupRepo; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'groupRepo': Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: Not an managed type: class com.eazysystems.appstore.domain.Group
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:561)
    at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88)
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:331)
    ... 16 common frames omitted
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'groupRepo': Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: Not an managed type: class com.eazysystems.appstore.domain.Group
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1574)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:539)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:476)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:303)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:299)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:1120)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1044)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:942)
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:533)
    ... 18 common frames omitted
Caused by: java.lang.IllegalArgumentException: Not an managed type: class com.cazysystems.appstore.domain.Group
    at org.hibernate.jpa.internal.metamodel.MetamodelImpl.managedType(MetamodelImpl.java:219)
    at org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation.<init>(JpaMetamodelEntityInformation.java:68)
    at org.springframework.data.jpa.repository.support.JpaEntityInformationSupport.getMetadata(JpaEntityInformationSupport.java:67)
    at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getEntityInformation(JpaRepositoryFactory.java:145)
    at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getTargetRepository(JpaRepositoryFactory.java:89)
    at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getTargetRepository(JpaRepositoryFactory.java:69)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport.getRepository(RepositoryFactorySupport.java:173)
    at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.initAndReturn(RepositoryFactoryBeanSupport.java:239)
    at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.afterPropertiesSet(RepositoryFactoryBeanSupport.java:225)
    at org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean.afterPropertiesSet(JpaRepositoryFactoryBean.java:92)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1633)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1570)
    ... 28 common frames omitted

My UserService class as following,

@Component
public class UserService implements BaseLdapNameAware {

    @Autowired
    private UserRepo userRepo;

    @Autowired
    private GroupRepo groupRepo;

    private LdapName baseLdapPath;

    @Autowired
    @Value("${sample.ldap.directory.type}")
    private DirectoryType directoryType;

    /*
     * @Autowired public UserService(UserRepo userRepo, GroupRepo groupRepo) {
     * this.userRepo = userRepo; this.groupRepo = groupRepo; }
     */
    public Group getUserGroup() {
        return groupRepo.findByName(GroupRepo.USER_GROUP);
    }

    public void setDirectoryType(DirectoryType directoryType) {
        this.directoryType = directoryType;
    }

    @Override
    public void setBaseLdapPath(LdapName baseLdapPath) {
        this.baseLdapPath = baseLdapPath;
    }

    public Iterable<User> findAll() {
        return userRepo.findAll();
    }

    public User findUser(String userId) {
        return userRepo.findOne(LdapUtils.newLdapName(userId));
    }

    public User createUser(User user) {
        User savedUser = userRepo.save(user);

        Group userGroup = getUserGroup();

        // The DN the member attribute must be absolute
        userGroup.addMember(toAbsoluteDn(savedUser.getId()));
        groupRepo.save(userGroup);

        return savedUser;
    }

    public LdapName toAbsoluteDn(Name relativeName) {
        return LdapNameBuilder.newInstance(baseLdapPath).add(relativeName)
                .build();
    }

    /**
     * This method expects absolute DNs of group members. In order to find the
     * actual users the DNs need to have the base LDAP path removed.
     *
     * @param absoluteIds
     * @return
     */
    public Set<User> findAllMembers(Iterable<Name> absoluteIds) {
        return Sets.newLinkedHashSet(userRepo
                .findAll(toRelativeIds(absoluteIds)));
    }

    public Iterable<Name> toRelativeIds(Iterable<Name> absoluteIds) {
        return Iterables.transform(absoluteIds, new Function<Name, Name>() {
            @Override
            public Name apply(Name input) {
                return LdapUtils.removeFirst(input, baseLdapPath);
            }
        });
    }

    public User updateUser(String userId, User user) {
        LdapName originalId = LdapUtils.newLdapName(userId);
        User existingUser = userRepo.findOne(originalId);

        existingUser.setFirstName(user.getFirstName());
        existingUser.setLastName(user.getLastName());
        existingUser.setFullName(user.getFullName());
        existingUser.setEmail(user.getEmail());
        existingUser.setPhone(user.getPhone());
        existingUser.setTitle(user.getTitle());
        existingUser.setDepartment(user.getDepartment());
        existingUser.setUnit(user.getUnit());

        if (directoryType == DirectoryType.AD) {
            return updateUserAd(originalId, existingUser);
        } else {
            return updateUserStandard(originalId, existingUser);
        }
    }

    /**
     * Update the user and - if its id changed - update all group references to
     * the user.
     *
     * @param originalId
     *            the original id of the user.
     * @param existingUser
     *            the user, populated with new data
     *
     * @return the updated entry
     */
    private User updateUserStandard(LdapName originalId, User existingUser) {
        User savedUser = userRepo.save(existingUser);

        if (!originalId.equals(savedUser.getId())) {
            // The user has moved - we need to update group references.
            LdapName oldMemberDn = toAbsoluteDn(originalId);
            LdapName newMemberDn = toAbsoluteDn(savedUser.getId());

            Collection<Group> groups = groupRepo.findByMember(oldMemberDn);
            updateGroupReferences(groups, oldMemberDn, newMemberDn);
        }
        return savedUser;
    }

    /**
     * Special behaviour in AD forces us to get the group membership before the
     * user is updated, because AD clears group membership for removed entries,
     * which means that once the user is update we've lost track of which groups
     * the user was originally member of, preventing us to update the membership
     * references so that they point to the new DN of the user.
     *
     * This is slightly less efficient, since we need to get the group
     * membership for all updates even though the user may not have been moved.
     * Using our knowledge of which attributes are part of the distinguished
     * name we can do this more efficiently if we are implementing specifically
     * for Active Directory - this approach is just to highlight this quite
     * significant difference.
     *
     * @param originalId
     *            the original id of the user.
     * @param existingUser
     *            the user, populated with new data
     *
     * @return the updated entry
     */
    private User updateUserAd(LdapName originalId, User existingUser) {
        LdapName oldMemberDn = toAbsoluteDn(originalId);
        Collection<Group> groups = groupRepo.findByMember(oldMemberDn);

        User savedUser = userRepo.save(existingUser);
        LdapName newMemberDn = toAbsoluteDn(savedUser.getId());

        if (!originalId.equals(savedUser.getId())) {
            // The user has moved - we need to update group references.
            updateGroupReferences(groups, oldMemberDn, newMemberDn);
        }
        return savedUser;
    }

    private void updateGroupReferences(Collection<Group> groups,
            Name originalId, Name newId) {
        for (Group group : groups) {
            group.removeMember(originalId);
            group.addMember(newId);

            groupRepo.save(group);
        }
    }

    public List<User> searchByNameName(String lastName) {
        return userRepo.findByFullNameContains(lastName);
    }
}

During the trouble shooting,

I noticed that, the application fails only when I used both JPA and LDAP together. When I removed all the JPA dependencies and implementations, the application works as expected.

The issue seems to be caused by the conflict between JPA and LDAP dependencies. But I am not sure how to resolve it.

Answer

jstuartmilne picture jstuartmilne · Sep 23, 2015

You are not stating what you want to achive, create a user, read user, seems to me you are mixing stuff from spring-data with ldap. Ill paste my configuration maybe it helps, if it does i can show you how im adding users with this configurations, again im not sure what you want to achieve, hope it helps

@Configuration
public class LdapManagerConfiguration {

@Autowired
Environment ldapProperties;

@Bean
public LdapContextSource contextSourceTarget() {
    LdapContextSource ldapContextSource = new LdapContextSource();
    ldapContextSource.setUrl(ldapProperties.getProperty("auth.ldap.url"));
    ldapContextSource.setBase(ldapProperties.getProperty("auth.ldap.base"));
    ldapContextSource.setUserDn(ldapProperties
            .getProperty("auth.ldap.userdn"));
    ldapContextSource.setPassword(ldapProperties
            .getProperty("auth.ldap.password"));

    return ldapContextSource;

}

@Bean
public LdapTemplate ldapTemplate() {

    return new LdapTemplate(contextSourceTarget());

}

@Bean
public InetOrgPersonContextMapper inetOrgPersonContextMapper() {
    return new InetOrgPersonContextMapper();
}

@Bean
public DefaultLdapUsernameToDnMapper defaultLdapUsernameToDnMapper() {
    return new DefaultLdapUsernameToDnMapper(
            ldapProperties.getProperty("auth.ldap.groupbase"),
            ldapProperties.getProperty("auth.ldap.attributename.username"));// "uid"
}

@Bean
public LdapUserDetailsManager ldapUserDetailManager() {
    LdapUserDetailsManager userManager = new LdapUserDetailsManager(
            contextSourceTarget());

    userManager.setGroupSearchBase(ldapProperties
            .getProperty("auth.ldap.groupbase"));
    userManager.setUserDetailsMapper(inetOrgPersonContextMapper());
    userManager.setUsernameMapper(defaultLdapUsernameToDnMapper());
    userManager.setGroupRoleAttributeName(ldapProperties
            .getProperty("auth.ldap.attributename.grouprole"));
    userManager.setGroupMemberAttributeName(ldapProperties
            .getProperty("auth.ldap.attributename.groupmemeber"));

    return userManager;

}

}

to add a user then

Logger logger = LoggerFactory.getLogger(LdapUserManagerImpl.class);

@Autowired
LdapTemplate ldapTemplate;

@Autowired
Environment ldapProperties;

final String passwordAttribute = "userPassword";

@Override
public boolean createUser(final String user,
        final String defaultPasswordPolicy)
        throws UserAlreadyExistsAsInactive {

    boolean created = false;

    String inactivePeople = ldapProperties.getProperty(
            "auth.ldap.inactive.groupbase").replace("ou=", "");

    String activePeople = ldapProperties.getProperty("auth.ldap.groupbase")
            .replace("ou=", "");

    if (existsUser(user, inactivePeople)) {
        logger.error("User " + user
                + " already exists in ldap but it's incative");
        throw new UserAlreadyExistsAsInactive(user);

    }
    if (existsUser(user, activePeople)) {
        logger.error("User " + user
                + " already exists in ldap and is Active");
        return false;
    }

    try {

        ldapTemplate.bind(buildDn(user, inactivePeople), null,
                createUserDetails(user, defaultPasswordPolicy));
        created = true;

    } catch (Exception e) {
        logger.error(
                "Unexpected exception when trying to create user in LdapException",
                e);
        return created;
    }
    return created;
}

So That Will Be the create

This would be de read

public UserI findUser(String userName, String group) {

    try {
        String dn = buildDN(userName, group);
        User user = ldapTemplate.lookup(dn, new UserAttributesMapper());
        return user;
    } catch (NameNotFoundException e) {
        return new UserNullObject();
    }

}

This the update

@Override
public void changeUserGroup(String userName, String oldGroup,
        String newGroup) throws ReLocoException {

    UserI user = findUser(userName, oldGroup);
    if (!user.isEmpty()) {

        try {

            Object password = getLdapPassword(userName, oldGroup);

            ldapTemplate.unbind(buildDN(userName, oldGroup));

            String defaultPasswordPolicy = ldapProperties
                    .getProperty("auth.ldap.default.userpollicy");

            ldapTemplate.bind(buildDN(userName, newGroup), null,
                    createUserDetails(userName, defaultPasswordPolicy));

            applyPasswordToUser(userName, password, newGroup);

        } catch (Exception e) {
            logger.error("Error on change user from incative to active people group "
                    + e);
            throw new ConfirmAccountValidationCodeException(userName);
        }
    } else {

        throw new UserNotFoundException(
                "The username provided for the given group '" + oldGroup
                        + "' does not exist");
    }

}