org.ligoj.app.plugin.id.ldap.resource.LdapPluginResource.java Source code

Java tutorial

Introduction

Here is the source code for org.ligoj.app.plugin.id.ldap.resource.LdapPluginResource.java

Source

/*
 * Licensed under MIT (https://github.com/ligoj/ligoj/blob/master/LICENSE)
 */
package org.ligoj.app.plugin.id.ldap.resource;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

import javax.cache.annotation.CacheKey;
import javax.cache.annotation.CacheResult;
import javax.transaction.Transactional;
import javax.transaction.Transactional.TxType;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.apache.commons.lang3.StringUtils;
import org.ligoj.app.api.Normalizer;
import org.ligoj.app.api.ServicePlugin;
import org.ligoj.app.api.SubscriptionStatusWithData;
import org.ligoj.app.iam.Activity;
import org.ligoj.app.iam.GroupOrg;
import org.ligoj.app.iam.IamConfiguration;
import org.ligoj.app.iam.IamConfigurationProvider;
import org.ligoj.app.iam.IamProvider;
import org.ligoj.app.iam.UserOrg;
import org.ligoj.app.model.CacheProjectGroup;
import org.ligoj.app.model.ContainerType;
import org.ligoj.app.model.Node;
import org.ligoj.app.model.Project;
import org.ligoj.app.model.Subscription;
import org.ligoj.app.plugin.id.dao.CacheProjectGroupRepository;
import org.ligoj.app.plugin.id.ldap.dao.CompanyLdapRepository;
import org.ligoj.app.plugin.id.ldap.dao.GroupLdapRepository;
import org.ligoj.app.plugin.id.ldap.dao.ProjectCustomerLdapRepository;
import org.ligoj.app.plugin.id.ldap.dao.UserLdapRepository;
import org.ligoj.app.plugin.id.model.ContainerScope;
import org.ligoj.app.plugin.id.resource.CompanyResource;
import org.ligoj.app.plugin.id.resource.ContainerScopeResource;
import org.ligoj.app.plugin.id.resource.ContainerWithScopeVo;
import org.ligoj.app.plugin.id.resource.GroupResource;
import org.ligoj.app.plugin.id.resource.IdentityResource;
import org.ligoj.app.plugin.id.resource.IdentityServicePlugin;
import org.ligoj.app.plugin.id.resource.UserOrgEditionVo;
import org.ligoj.app.plugin.id.resource.UserOrgResource;
import org.ligoj.app.resource.ActivitiesProvider;
import org.ligoj.app.resource.ServicePluginLocator;
import org.ligoj.app.resource.plugin.AbstractToolPluginResource;
import org.ligoj.bootstrap.core.INamableBean;
import org.ligoj.bootstrap.core.NamedBean;
import org.ligoj.bootstrap.core.SpringUtils;
import org.ligoj.bootstrap.core.resource.BusinessException;
import org.ligoj.bootstrap.core.validation.ValidationJsonException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import lombok.extern.slf4j.Slf4j;

/**
 * LDAP resource.
 */
@Path(LdapPluginResource.URL)
@Service
@Transactional
@Produces(MediaType.APPLICATION_JSON)
@Slf4j
public class LdapPluginResource extends AbstractToolPluginResource
        implements IdentityServicePlugin, IamConfigurationProvider {

    private static final String PATTERN_PROPERTY = "pattern";

    private static final String LDAP_VERSION = "3";

    /**
     * Plug-in key.
     */
    public static final String URL = IdentityResource.SERVICE_URL + "/ldap";

    /**
     * Plug-in key.
     */
    public static final String KEY = URL.replace('/', ':').substring(1);

    /**
     * Full URL like "ldap/myhost:389/"
     */
    public static final String PARAMETER_URL = KEY + ":url";

    /**
     * DN of administrative user that can fetch the repository
     */
    public static final String PARAMETER_USER = KEY + ":user-dn";

    /**
     * Referral option as "follow"
     */
    public static final String PARAMETER_REFERRAL = KEY + ":referral";

    /**
     * Password of administrative user
     */
    public static final String PARAMETER_PASSWORD = KEY + ":password";

    /**
     * Base DN where people, groups and companies are located
     */
    public static final String PARAMETER_BASE_BN = KEY + ":base-dn";

    /**
     * LDAP schema attribute name of login.
     */
    public static final String PARAMETER_UID_ATTRIBUTE = KEY + ":uid-attribute";

    /**
     * DN of location of users can login
     */
    public static final String PARAMETER_PEOPLE_DN = KEY + ":people-dn";

    /**
     * LDAP schema attribute name of department.
     */
    public static final String PARAMETER_DEPARTMENT_ATTRIBUTE = KEY + ":department-attribute";
    /**
     * LDAP schema attribute name of internal id of login. May not be unique.
     */
    public static final String PARAMETER_LOCAL_ID_ATTRIBUTE = KEY + ":local-id-attribute";

    /**
     * DN of location where isolated users are moved to.
     */
    public static final String PARAMETER_QUARANTINE_DN = KEY + ":quarantine-dn";

    /**
     * LDAP schema attribute holding the locked state of a user.
     */
    public static final String PARAMETER_LOCKED_ATTRIBUTE = KEY + ":locked-attribute";

    /**
     * Value used as flag for a locked user inside the locked attribute
     */
    public static final String PARAMETER_LOCKED_VALUE = KEY + ":locked-value";

    /**
     * Object Class of people : organizationalPerson, inetOrgPerson
     */
    public static final String PARAMETER_PEOPLE_CLASS = KEY + ":people-class";

    /**
     * Pattern capturing the company from the DN of the user. May be a row string for constant.
     */
    public static final String PARAMETER_COMPANY_PATTERN = KEY + ":company-pattern";

    /**
     * DN of location of groups
     */
    public static final String PARAMETER_GROUPS_DN = KEY + ":groups-dn";

    /**
     * DN of location of companies
     */
    public static final String PARAMETER_COMPANIES_DN = KEY + ":companies-dn";

    /**
     * DN of location of people considered as internal. May be the same than people
     */
    public static final String PARAMETER_PEOPLE_INTERNAL_DN = KEY + ":people-internal-dn";

    /**
     * Value used as flag to hash or not the password
     */
    public static final String PARAMETER_CLEAR_PASSWORD = KEY + ":clear-password";

    /**
     * Lock object used to synchronize the creation.
     */
    private static final Object USER_LOCK = new Object();

    @Autowired
    protected ProjectCustomerLdapRepository projectCustomerLdapRepository;

    @Autowired
    protected CompanyResource companyResource;

    @Autowired
    protected UserOrgResource userResource;

    @Autowired
    protected GroupResource groupLdapResource;

    @Autowired
    private ContainerScopeResource containerScopeResource;

    @Autowired
    private CacheProjectGroupRepository cacheProjectGroupRepository;

    @Autowired
    private IamProvider[] iamProvider;

    @Autowired
    protected ServicePluginLocator servicePluginLocator;

    @Autowired
    protected LdapPluginResource self;

    /**
     * Available node configurations. Key is the node identifier.
     */
    private Map<String, IamConfiguration> nodeConfigurations = new HashMap<>();

    /**
     * Build a user LDAP repository from the given node.
     *
     * @param node
     *            The node, also used as cache key.
     * @return The {@link UserLdapRepository} instance. Cache is involved.
     */
    private UserLdapRepository getUserLdapRepository(final String node) {
        log.info("Build ldap template for node {}", node);
        final Map<String, String> parameters = pvResource.getNodeParameters(node);
        final LdapContextSource contextSource = new LdapContextSource();
        contextSource.setReferral(parameters.get(PARAMETER_REFERRAL));
        contextSource.setPassword(parameters.get(PARAMETER_PASSWORD));
        contextSource.setUrl(parameters.get(PARAMETER_URL));
        contextSource.setUserDn(parameters.get(PARAMETER_USER));
        contextSource.setBase(parameters.get(PARAMETER_BASE_BN));
        contextSource.afterPropertiesSet();
        final LdapTemplate template = new LdapTemplate();
        template.setContextSource(contextSource);
        template.setIgnorePartialResultException(true);

        // A new repository instance
        final UserLdapRepository repository = new UserLdapRepository();
        repository.setTemplate(template);
        repository.setPeopleBaseDn(StringUtils.trimToEmpty(parameters.get(PARAMETER_PEOPLE_DN)));
        repository.setPeopleInternalBaseDn(parameters.get(PARAMETER_PEOPLE_INTERNAL_DN));
        repository.setQuarantineBaseDn(StringUtils.trimToEmpty(parameters.get(PARAMETER_QUARANTINE_DN)));
        repository.setDepartmentAttribute(parameters.get(PARAMETER_DEPARTMENT_ATTRIBUTE));
        repository.setLocalIdAttribute(parameters.get(PARAMETER_LOCAL_ID_ATTRIBUTE));
        repository.setUidAttribute(parameters.get(PARAMETER_UID_ATTRIBUTE));
        repository.setLockedAttribute(parameters.get(PARAMETER_LOCKED_ATTRIBUTE));
        repository.setLockedValue(parameters.get(PARAMETER_LOCKED_VALUE));
        repository.setPeopleClass(parameters.get(PARAMETER_PEOPLE_CLASS));
        repository.setCompanyPattern(StringUtils.trimToEmpty(parameters.get(PARAMETER_COMPANY_PATTERN)));
        repository.setClearPassword(Boolean.parseBoolean(parameters.get(PARAMETER_CLEAR_PASSWORD)));

        // Complete the bean
        SpringUtils.getApplicationContext().getAutowireCapableBeanFactory().autowireBean(repository);

        return repository;
    }

    /**
     * Build a group LDAP repository from the given node.
     *
     * @param node
     *            The node, also used as cache key.
     * @param template
     *            The {@link LdapTemplate} used to query the repository.
     * @return The {@link UserLdapRepository} instance. Cache is involved.
     */
    public GroupLdapRepository newGroupLdapRepository(final String node, final LdapTemplate template) {
        final Map<String, String> parameters = pvResource.getNodeParameters(node);

        // A new repository instance
        final GroupLdapRepository repository = new GroupLdapRepository();
        repository.setTemplate(template);
        repository.setGroupsBaseDn(StringUtils.trimToEmpty(parameters.get(PARAMETER_GROUPS_DN)));

        // Complete the bean
        SpringUtils.getApplicationContext().getAutowireCapableBeanFactory().autowireBean(repository);
        return repository;
    }

    /**
     * Build a group LDAP repository from the given node.
     *
     * @param node
     *            The node, also used as cache key.
     * @param template
     *            The {@link LdapTemplate} used to query the repository.
     * @return The {@link UserLdapRepository} instance. Cache is involved.
     */
    public CompanyLdapRepository newCompanyLdapRepository(final String node, final LdapTemplate template) {
        final Map<String, String> parameters = pvResource.getNodeParameters(node);

        // A new repository instance
        final CompanyLdapRepository repository = new CompanyLdapRepository();
        repository.setTemplate(template);
        repository.setCompanyBaseDn(parameters.get(PARAMETER_COMPANIES_DN));
        repository.setQuarantineBaseDn(parameters.get(PARAMETER_QUARANTINE_DN));

        // Complete the bean
        SpringUtils.getApplicationContext().getAutowireCapableBeanFactory().autowireBean(repository);
        return repository;
    }

    @Override
    public boolean accept(final Authentication authentication, final String node) {
        final Map<String, String> parameters = pvResource.getNodeParameters(node);
        return !parameters.isEmpty() && authentication.getName()
                .matches(StringUtils.defaultString(parameters.get(IdentityResource.PARAMETER_UID_PATTERN), ".*"));
    }

    @Override
    public void create(final int subscription) {
        final Map<String, String> parameters = subscriptionResource.getParameters(subscription);
        final String group = parameters.get(IdentityResource.PARAMETER_GROUP);
        final String parentGroup = parameters.get(IdentityResource.PARAMETER_PARENT_GROUP);
        final String ou = parameters.get(IdentityResource.PARAMETER_OU);
        final Project project = subscriptionRepository.findOne(subscription).getProject();
        final String pkey = project.getPkey();

        // Check the relationship between group, OU and project
        validateGroup(group, ou, pkey);

        // Check the relationship between group, and parent
        final String parentDn = validateAndCreateParent(group, parentGroup, ou, pkey);

        // Create the group inside the parent (OU or parent CN)
        final String groupDn = "cn=" + group + "," + parentDn;
        log.info("New Group CN would be created {} project {} and subscription {}", group, pkey);
        final GroupLdapRepository repository = getGroup();
        final GroupOrg groupLdap = repository.create(groupDn, group);

        // Complete as needed the relationship between parent and this new group
        if (StringUtils.isNotBlank(parentGroup)) {
            // This group will be added as "uniqueMember" of its parent
            repository.addGroup(groupLdap, parentGroup);
        }

        // Associate the project to this group in the cache
        final CacheProjectGroup projectGroup = new CacheProjectGroup();
        projectGroup.setProject(project);
        projectGroup.setGroup(repository.getCacheRepository().findOneExpected(groupLdap.getId()));
        cacheProjectGroupRepository.saveAndFlush(projectGroup);
    }

    /**
     * Validate the group against the OU and the linked project.
     */
    private void validateGroup(final String group, final String ou, final String pkey) {
        // Check the group does not exists
        if (groupLdapResource.findById(group) != null) {
            // This group already exists
            throw new ValidationJsonException(IdentityResource.PARAMETER_GROUP, "already-exist", "0",
                    GroupResource.GROUP_ATTRIBUTE, "1", group);
        }

        // Compare the project's key with the OU, and the name of the group

        // The group must start with the target OU
        if (!startsWithAndDifferent(group, ou + "-")) {
            // This group has not a correct form
            throw new ValidationJsonException(IdentityResource.PARAMETER_GROUP, PATTERN_PROPERTY, ou + "-.+");
        }

        // The name of the group must start with the PKEY of project
        if (!group.equals(pkey) && !startsWithAndDifferent(group, pkey + "-")) {
            // This group has not a correct form
            throw new ValidationJsonException(IdentityResource.PARAMETER_GROUP, PATTERN_PROPERTY, pkey + "(-.+)?");
        }
    }

    private boolean startsWithAndDifferent(final String provided, final String expected) {
        return provided.startsWith(expected) && !provided.equals(expected);
    }

    /**
     * Validate the parent and return its DN. OU must be normalized.
     */
    private String validateAndCreateParent(final String group, final String parentGroup, final String ou,
            final String pkey) {
        // Check the creation mode
        if (StringUtils.isBlank(parentGroup)) {
            // Parent as not been defined, so will be the specified OU. that
            // would be created if it does not exist
            return validateAndCreateParentOu(group, ou, pkey);
        }

        // Parent has been specified, so will be another group we need to check
        return validateParentGroup(group, parentGroup);
    }

    /**
     * Validate the group against its direct parent (a normalized OU) and return its DN.
     */
    private String validateAndCreateParentOu(final String group, final String ou, final String pkey) {
        final ContainerScope groupTypeLdap = containerScopeResource.findByName(ContainerScope.TYPE_PROJECT);
        final String parentDn = groupTypeLdap.getDn();

        // Build the complete normalized DN from the OU and new Group
        final String ouDn = "ou=" + ou + "," + parentDn;

        // Check the target OU exists or not and create the OU as needed
        if (projectCustomerLdapRepository.findById(parentDn, ou) == null) {
            // Create the OU in LDAP
            log.info("New OU would be created {} for group {}, project {} and subscription {}", ou, group, pkey);
            projectCustomerLdapRepository.create(parentDn, ou, ouDn);
        }

        // Parent will be an organizationalUnit (OU)
        return ouDn;
    }

    /**
     * Validate the group against its parent and return the corresponding DN.
     */
    private String validateParentGroup(final String group, final String parentGroup) {
        final GroupOrg parentGroupLdap = groupLdapResource.findById(parentGroup);
        if (parentGroupLdap == null) {
            // The parent group does not exists
            throw new ValidationJsonException(IdentityResource.PARAMETER_PARENT_GROUP,
                    BusinessException.KEY_UNKNOW_ID, parentGroup);
        }

        // Compare the group and its parent
        if (!group.startsWith(parentGroup + "-")) {
            // This sub-group has not a correct form
            throw new ValidationJsonException(IdentityResource.PARAMETER_GROUP, PATTERN_PROPERTY,
                    parentGroup + "-.*");
        }

        // Parent will be another group, return its DN
        return parentGroupLdap.getDn();
    }

    @Override
    public void link(final int subscription) {
        final Map<String, String> parameters = subscriptionResource.getParameters(subscription);

        // Validate the job settings
        validateGroup(parameters);

        // There is no additional step since the group is already created in
        // LDAP
    }

    @Override
    public String getVersion(final Map<String, String> parameters) {
        // LDAP version is fixed
        return LDAP_VERSION;
    }

    /**
     * Return activities of all users in the group of this subscription as CSV input stream.
     *
     * @param subscription
     *            The subscription identifier.
     * @param file
     *            The target file name.
     * @return the stream ready to be read during the serialization.
     * @throws Exception
     *             When any technical error occurs. Caught at upper level for the right mapping.
     */
    @GET
    @Path("activity/{subscription:\\d+}/{file:group-.*.csv}")
    @Produces(MediaType.APPLICATION_OCTET_STREAM)
    public Response getGroupActivitiesCsv(@PathParam("subscription") final int subscription,
            @PathParam("file") final String file) throws Exception {
        log.info("Group activities report requested by '{}' for subscription '{}'",
                SecurityContextHolder.getContext().getAuthentication().getName(), subscription);
        return download(new CsvStreamingOutput(getActivities(subscription, false)), file).build();
    }

    /**
     * Return activities of all users in any group subscribed by the same project of this subscription as CSV input
     * stream.
     *
     * @param subscription
     *            The subscription identifier.
     * @param file
     *            The target file name.
     * @return the stream ready to be read during the serialization.
     * @throws Exception
     *             When any technical error occurs. Caught at upper level for the right mapping.
     */
    @GET
    @Path("activity/{subscription:\\d+}/{file:project-.*.csv}")
    @Produces(MediaType.APPLICATION_OCTET_STREAM)
    public Response getProjectActivitiesCsv(@PathParam("subscription") final int subscription,
            @PathParam("file") final String file) throws Exception {
        log.info("Project activities report requested by '{}' for subscription '{}'",
                SecurityContextHolder.getContext().getAuthentication().getName(), subscription);
        return download(new CsvStreamingOutput(getActivities(subscription, true)), file).build();
    }

    /**
     * Return activities associates by given a subscription.
     */
    private ActivitiesComputations getActivities(final int subscription, final boolean global) throws Exception {
        // Get users from other LDAP subscriptions
        final Subscription main = subscriptionResource.checkVisible(subscription);
        final List<Subscription> subscriptions = subscriptionRepository.findAllOnSameProject(subscription);
        final Set<UserOrg> users = global ? getMembersOfAllSubscriptions(subscriptions)
                : getMembersOfSubscription(main);

        // Get the activities from each subscription of the same project,
        final ActivitiesComputations result = new ActivitiesComputations();
        result.setUsers(users);
        final List<String> userLogins = users.stream().map(UserOrg::getId).collect(Collectors.toList());
        final Map<String, Map<String, Activity>> activities = new HashMap<>();
        final Set<INamableBean<String>> nodes = new LinkedHashSet<>();
        for (final Subscription projectSubscription : subscriptions) {
            final ServicePlugin resource = servicePluginLocator.getResource(projectSubscription.getNode().getId());
            addSubscriptionActivities(activities, userLogins, projectSubscription, resource, nodes);
        }
        result.setNodes(nodes);
        result.setActivities(activities);
        return result;
    }

    /**
     * Return members of all LDAP subscriptions
     */
    private Set<UserOrg> getMembersOfAllSubscriptions(final Collection<Subscription> projectSubscriptions) {
        return projectSubscriptions.stream().flatMap(s -> getMembersOfSubscription(s).stream())
                .collect(Collectors.toSet());
    }

    /**
     * Return members of given subscription.
     */
    private Set<UserOrg> getMembersOfSubscription(final Subscription subscription) {
        final Set<UserOrg> users = new HashSet<>();
        final ServicePlugin plugin = servicePluginLocator.getResource(subscription.getNode().getId());
        if (plugin instanceof LdapPluginResource) {
            users.addAll(((LdapPluginResource) plugin).getMembers(subscription.getId()));
        }
        return users;
    }

    /**
     * Return users member of associated subscription.
     *
     * @param subscription
     *            The subscription identifier used to get the related group and members.
     * @return The members of related groups of the subscription.
     */
    public Collection<UserOrg> getMembers(final int subscription) {
        // Get current subscription parameters
        final Map<String, String> parameters = subscriptionResource.getParameters(subscription);
        final String group = parameters.get(IdentityResource.PARAMETER_GROUP);
        return userResource.findAllNotSecure(null, group);
    }

    /**
     * Add activities related to given subscription.
     *
     * @param activities
     *            The collected activities.
     * @param users
     *            The implied users.
     * @param subscription
     *            The related subscription of theses activities.
     * @param plugin
     *            The plug-in associated to this subscription.
     * @param nodes
     *            The nodes that have already been processed. This set will be updated by this function.
     * @throws Exception
     *             When any technical error occurs. Caught at upper level for the right mapping.
     */
    protected void addSubscriptionActivities(final Map<String, Map<String, Activity>> activities,
            final Collection<String> users, final Subscription subscription, final ServicePlugin plugin,
            final Set<INamableBean<String>> nodes) throws Exception {

        // Collect activities of each subscription of unique node
        if (plugin instanceof ActivitiesProvider && nodes.add(subscription.getNode())) {
            final Map<String, Activity> subscriptionActivities = ((ActivitiesProvider) plugin)
                    .getActivities(subscription.getId(), users);
            for (final Entry<String, Activity> userActivity : subscriptionActivities.entrySet()) {
                addUserActivities(activities, subscription.getNode(), userActivity);
            }
        }
    }

    /**
     * Add activities related to a single node.
     */
    private void addUserActivities(final Map<String, Map<String, Activity>> activities, final Node node,
            final Entry<String, Activity> userActivity) {
        final String user = userActivity.getKey();
        activities.computeIfAbsent(user, k -> new HashMap<>()).put(node.getId(), userActivity.getValue());
    }

    /**
     * Validate the group settings.
     *
     * @param parameters
     *            the administration parameters.
     * @return real group name.
     */
    protected INamableBean<String> validateGroup(final Map<String, String> parameters) {
        // Get group configuration
        final String group = parameters.get(IdentityResource.PARAMETER_GROUP);
        final ContainerWithScopeVo groupLdap = groupLdapResource.findByName(group);

        // Check the group exists
        if (groupLdap == null) {
            throw new ValidationJsonException(IdentityResource.PARAMETER_GROUP, BusinessException.KEY_UNKNOW_ID,
                    group);
        }

        // Check the group has type TYPE_PROJECT
        if (!ContainerScope.TYPE_PROJECT.equals(groupLdap.getScope())) {
            // Invalid type
            throw new ValidationJsonException(IdentityResource.PARAMETER_GROUP, "group-type", group);
        }

        // Return the nice name
        final INamableBean<String> result = new NamedBean<>();
        result.setName(groupLdap.getName());
        result.setId(group);
        return result;
    }

    /**
     * Search the LDAP Groups matching to the given criteria and for type "Project". Node identifier is ignored for now.
     *
     * @param criteria
     *            the search criteria.
     * @return LDAP Groups matching the criteria.
     * @see ContainerScope#TYPE_PROJECT
     */
    @GET
    @Path("group/{node}/{criteria}")
    @Consumes(MediaType.APPLICATION_JSON)
    public List<INamableBean<String>> findGroupsByName(@PathParam("criteria") final String criteria) {
        final List<INamableBean<String>> result = new ArrayList<>();
        final String criteriaClean = Normalizer.normalize(criteria);
        final Set<GroupOrg> visibleGroups = groupLdapResource.getContainers();
        final List<ContainerScope> types = containerScopeResource.findAllDescOrder(ContainerType.GROUP);
        for (final GroupOrg group : visibleGroups) {
            final ContainerScope scope = groupLdapResource.toScope(types, group);

            // Check type and criteria
            if (scope != null && ContainerScope.TYPE_PROJECT.equals(scope.getName())
                    && group.getId().contains(criteriaClean)) {
                // Return the nice name
                final INamableBean<String> bean = new NamedBean<>();
                NamedBean.copy(group, bean);
                result.add(bean);
            }
        }

        return result;
    }

    /**
     * Search the LDAP Customers matching to the given criteria and for type "Project". Node identifier is ignored for
     * now. Node is ignored.
     *
     * @param criteria
     *            the search criteria.
     * @return LDAP Customers matching the criteria.
     * @see ContainerScope#TYPE_PROJECT
     */
    @GET
    @Path("customer/{node}/{criteria}")
    @Consumes(MediaType.APPLICATION_JSON)
    public Collection<INamableBean<String>> findCustomersByName(@PathParam("criteria") final String criteria) {
        final Set<INamableBean<String>> result = new TreeSet<>();
        final String criteriaClean = Normalizer.normalize(criteria);
        final ContainerScope findByName = containerScopeResource.findByName(ContainerScope.TYPE_PROJECT);
        final Collection<String> allCustomers = projectCustomerLdapRepository.findAll(findByName.getDn());

        // Check type and criteria
        allCustomers.stream().filter(customer -> customer.contains(criteriaClean)).forEach(customer -> {
            // Return the nice name
            final INamableBean<String> bean = new NamedBean<>();
            // Return the nice name
            bean.setName(customer);
            bean.setId(customer);
            result.add(bean);
        });
        return result;
    }

    @Override
    public void delete(final int subscription, final boolean deleteRemoteData) {
        if (deleteRemoteData) {
            // Data are removed from the LDAP
            final Map<String, String> parameters = subscriptionResource.getParameters(subscription);
            final String group = parameters.get(IdentityResource.PARAMETER_GROUP);

            // Check the group exists, but is not required to continue the
            // process
            final GroupLdapRepository repository = getGroup();
            final GroupOrg groupLdap = repository.findById(group);
            if (groupLdap != null) {
                // Perform the deletion
                repository.delete(groupLdap);
            }
        }
    }

    @Override
    @Transactional(value = TxType.NOT_SUPPORTED)
    public String getKey() {
        return KEY;
    }

    @Override
    @Transactional(value = TxType.NOT_SUPPORTED)
    public String getLastVersion() {
        return LDAP_VERSION;
    }

    @Override
    public boolean checkStatus(final String node, final Map<String, String> parameters) {
        // Query the LDAP, the user is not important, we expect no error, that's all
        self.getConfiguration(node).getUserRepository().findByIdNoCache("-any-");
        return true;
    }

    @Override
    public SubscriptionStatusWithData checkSubscriptionStatus(final Map<String, String> parameters) {
        final GroupOrg groupLdap = getGroup().findById(parameters.get(IdentityResource.PARAMETER_GROUP));
        if (groupLdap == null) {
            return new SubscriptionStatusWithData(false);
        }

        // Non empty group, return amount of members
        final SubscriptionStatusWithData result = new SubscriptionStatusWithData(true);
        result.put("members", groupLdap.getMembers().size());
        return result;
    }

    @Override
    public IamConfiguration getConfiguration(final String node) {
        self.ensureCachedConfiguration(node);
        return nodeConfigurations.computeIfAbsent(node, this::refreshConfiguration);
    }

    @CacheResult(cacheName = "id-ldap-configuration")
    public boolean ensureCachedConfiguration(@CacheKey final String node) {
        refreshConfiguration(node);
        return true;
    }

    private IamConfiguration refreshConfiguration(final String node) {
        return nodeConfigurations.compute(node, (n, m) -> {
            final IamConfiguration configuration = new IamConfiguration();
            final UserLdapRepository repository = getUserLdapRepository(node);
            configuration.setUserRepository(repository);
            configuration.setCompanyRepository(newCompanyLdapRepository(node, repository.getTemplate()));
            configuration.setGroupRepository(newGroupLdapRepository(node, repository.getTemplate()));
            repository.setCompanyRepository((CompanyLdapRepository) configuration.getCompanyRepository());
            repository.setGroupLdapRepository((GroupLdapRepository) configuration.getGroupRepository());
            return configuration;
        });
    }

    /**
     * Group repository provider.
     *
     * @return Group repository provider.
     */
    private GroupLdapRepository getGroup() {
        return (GroupLdapRepository) iamProvider[0].getConfiguration().getGroupRepository();
    }

    @Override
    public Authentication authenticate(final Authentication authentication, final String node,
            final boolean primary) {
        final UserLdapRepository repository = (UserLdapRepository) self.getConfiguration(node).getUserRepository();

        // Authenticate the user
        if (repository.authenticate(authentication.getName(), (String) authentication.getCredentials())) {
            // Return a new authentication based on resolved application user
            return primary ? authentication
                    : new UsernamePasswordAuthenticationToken(toApplicationUser(repository, authentication), null);
        }
        throw new BadCredentialsException("");
    }

    /**
     * Check the authentication, then create or get the application user matching to the given account.
     *
     * @param repository
     *            Repository used to authenticate the user, and also to use to fetch the user attributes.
     * @param authentication
     *            The current authentication.
     * @return A not <code>null</code> application user.
     */
    protected String toApplicationUser(final UserLdapRepository repository, final Authentication authentication) {
        // Check the authentication
        final UserOrg account = repository.findOneBy(repository.getAuthenticateProperty(authentication.getName()),
                authentication.getName());

        // Check at least one mail is present
        if (account.getMails().isEmpty()) {
            // Mails are required to proceed the authentication
            log.info("Account '{} [{} {}]' has no mail", account.getId(), account.getFirstName(),
                    account.getLastName());
            throw new NotAuthorizedException("ambiguous-account-no-mail");
        }

        // Find the right application user
        return toApplicationUser(account);
    }

    /**
     * Create or get the application user matching to the given account.
     *
     * @param account
     *            The account from the authentication.
     * @return A not <code>null</code> application user.
     */
    protected String toApplicationUser(final UserOrg account) {
        // Find the user by the mail in the primary repository
        final List<UserOrg> usersByMail = userResource.findAllBy("mail", account.getMails().get(0));
        if (usersByMail.isEmpty()) {
            // No more try, account can be created in the application repository
            // with a free login
            return newApplicationUser(account);
        }
        if (usersByMail.size() == 1) {
            // Everything is checked, account can be merged into the existing application user
            userResource.mergeUser(usersByMail.get(0), account);
            return usersByMail.get(0).getId();
        }

        // Too many matching mail
        log.info("Account '{} [{} {}]' has too many mails ({}), expected one", account.getId(),
                account.getFirstName(), account.getLastName(), usersByMail.size());
        throw new NotAuthorizedException("ambiguous-account-too-many-mails");

    }

    /**
     * Create the application user from the actual account.
     *
     * @param account
     *            The account from the authentication.
     * @return The new application user.
     */
    protected String newApplicationUser(final UserOrg account) {
        synchronized (USER_LOCK) {

            // Copy the data from the authenticated account to the application
            // account
            final UserOrgEditionVo userLdapEdition = new UserOrgEditionVo();
            account.copy(userLdapEdition);
            userLdapEdition.setGroups(Collections.emptyList());
            userLdapEdition.setMail(account.getMails().get(0));

            // Assign a free login
            userLdapEdition.setName(nextFreeLogin(toLogin(account)));

            // This user can be created in the primary repository
            userResource.saveOrUpdate(userLdapEdition);

            return userLdapEdition.getId();
        }
    }

    /**
     * Find a free application login from a base login. Primary repository is checked to reclaim a free login.
     *
     * @param login
     *            The base login name.
     * @return a free login inside the primary repository.
     */
    protected String nextFreeLogin(final String login) {
        int suffix = 0;
        UserOrg userLdap;
        String nextLogin;
        do {
            nextLogin = login + (suffix == 0 ? "" : suffix);
            userLdap = userResource.findByIdNoCache(nextLogin);
            suffix++;
        } while (userLdap != null);

        // No user found for this login
        return nextLogin;
    }

    /**
     * Generate a application login from an account.
     *
     * @param account
     *            The current authenticated account in this security provider.
     * @return a corresponding application login candidate from an account.
     */
    protected String toLogin(final UserOrg account) {
        final String trimFirstName = normalize(account.getFirstName());
        final String trimLastName = normalize(account.getLastName());
        if (trimFirstName.length() * trimLastName.length() == 0) {
            // Unable to build a valid login from these attributes
            throw new NotAuthorizedException("cannot-build-application-login");
        }

        return trimFirstName.substring(0, 1) + trimLastName;
    }

    private String normalize(final String string) {
        return StringUtils.trimToEmpty(Normalizer.normalize(string).replace("[^\\w\\d]", " ").replace("  ", " "));
    }
}