org.ligoj.app.plugin.id.ldap.dao.GroupLdapRepository.java Source code

Java tutorial

Introduction

Here is the source code for org.ligoj.app.plugin.id.ldap.dao.GroupLdapRepository.java

Source

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

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import javax.naming.directory.BasicAttribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;

import org.apache.commons.lang3.ArrayUtils;
import org.ligoj.app.api.Normalizer;
import org.ligoj.app.iam.GroupOrg;
import org.ligoj.app.iam.IGroupRepository;
import org.ligoj.app.iam.ResourceOrg;
import org.ligoj.app.iam.UserOrg;
import org.ligoj.app.iam.dao.CacheGroupRepository;
import org.ligoj.app.iam.model.CacheGroup;
import org.ligoj.app.model.ContainerType;
import org.ligoj.app.plugin.id.DnUtils;
import org.ligoj.app.plugin.id.dao.AbstractMemCacheRepository.CacheDataType;
import org.ligoj.bootstrap.core.validation.ValidationJsonException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.filter.AndFilter;
import org.springframework.ldap.filter.EqualsFilter;

import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

/**
 * Group LDAP repository
 */
@Slf4j
public class GroupLdapRepository extends AbstractContainerLdapRepository<GroupOrg, CacheGroup>
        implements IGroupRepository {

    /**
     * Default DN member for new group. This is required for some LDAP implementation where "uniqueMember" attribute is
     * required for "groupOfUniqueNames" class.
     *
     * @see <a href="https://msdn.microsoft.com/en-us/library/ms682261(v=vs.85).aspx">MSDN</a>
     * @see <a href="https://tools.ietf.org/html/rfc4519#page-19">IETF</a>
     */
    public static final String DEFAULT_MEMBER_DN = "uid=none";

    @Setter
    private String groupsBaseDn;

    private static final String DEPARTMENT_ATTRIBUTE = "businessCategory";
    private static final String GROUP_OF_UNIQUE_NAMES = "groupOfUniqueNames";
    private static final String UNIQUE_MEMBER = "uniqueMember";

    @Autowired
    private CacheGroupRepository cacheGroupRepository;

    /**
     * Default constructor for a container of type {@link ContainerType#GROUP}
     */
    public GroupLdapRepository() {
        super(ContainerType.GROUP, GROUP_OF_UNIQUE_NAMES);
    }

    @Override
    public CacheGroupRepository getCacheRepository() {
        return cacheGroupRepository;
    }

    /**
     * Fetch and return all normalized groups. Note the result use cache, so does not reflect the current state of LDAP.
     * LDAP. Cache manager is involved.
     *
     * @return the groups. Key is the normalized name, Value is the corresponding LDAP group containing real CN, DN and
     *         normalized UID members.
     */
    @Override
    @SuppressWarnings("unchecked")
    public Map<String, GroupOrg> findAll() {
        return (Map<String, GroupOrg>) cacheRepository.getData().get(CacheDataType.GROUP);
    }

    /**
     * Fetch and return all normalized groups. Note the result use cache, so does not reflect the current state of LDAP.
     * LDAP.
     *
     * @return the groups. Key is the normalized name, Value is the corresponding LDAP group containing real CN, DN and
     *         normalized UID members.
     */
    @Override
    public Map<String, GroupOrg> findAllNoCache() {
        final Map<String, GroupOrg> groups = new HashMap<>();
        final Map<String, Set<String>> subGroupsDn = new HashMap<>();
        final Map<String, GroupOrg> dnToGroups = new HashMap<>();

        // First pass, collect the groups and dirty relationships
        for (final DirContextAdapter ldap : template.search(groupsBaseDn,
                new EqualsFilter("objectClass", GROUP_OF_UNIQUE_NAMES).encode(),
                (Object ctx) -> (DirContextAdapter) ctx)) {
            final Set<String> members = new HashSet<>();
            final String dn = ldap.getDn().toString().toLowerCase(Locale.ENGLISH);
            final String name = ldap.getStringAttribute("cn");
            final HashSet<String> subGroups = new HashSet<>();
            for (final String memberDN : ArrayUtils.nullToEmpty(ldap.getStringAttributes(UNIQUE_MEMBER))) {
                if (memberDN.startsWith("uid")) {
                    // User membership
                    members.add(memberDN);
                } else {
                    // Group (or whatever) membership
                    subGroups.add(memberDN);
                }
            }
            final GroupOrg group = new GroupOrg(dn, name, members);
            subGroupsDn.put(group.getId(), subGroups);
            groups.put(group.getId(), group);
            dnToGroups.put(dn, group);
        }

        // Second pass to validate the sub-groups and complete the opposite relation
        updateSubGroups(groups, subGroupsDn, dnToGroups);

        return groups;
    }

    /**
     * Complete the sub-groups hierarchy and update the two-ways relationship
     */
    private void updateSubGroups(final Map<String, GroupOrg> groups, final Map<String, Set<String>> subGroupsDn,
            final Map<String, GroupOrg> dnToGroups) {
        for (final GroupOrg group : groups.values()) {
            for (final String subGroupDn : subGroupsDn.get(group.getId())) {
                final GroupOrg subGroup = dnToGroups.get(Normalizer.normalize(subGroupDn));
                if (subGroup == null) {
                    // The unique member previously found does not match to an existing group, report it
                    log.warn("Broken group reference found '{}' --> {}", group.getDn(), subGroupDn);
                } else {
                    // This is a valid sub group, create both sides of this relation. Raw CN are used
                    group.getSubGroups().add(subGroup.getId());
                    subGroup.getGroups().add(group.getId());
                }
            }
        }
    }

    private void removeFromJavaCache(final GroupOrg group) {
        // Remove the sub groups from LDAP
        new ArrayList<>(group.getSubGroups()).stream().map(this::findById).filter(Objects::nonNull)
                .forEach(child -> removeGroup(child, group.getId()));

        // Remove from the parent LDAP groups
        new ArrayList<>(group.getGroups()).forEach(parent -> removeGroup(group, parent));

        // Also, update the raw cache
        findAll().remove(group.getId());
    }

    /**
     * Delete the given group. There is no synchronized block, so error could occur; this is assumed for performance
     * purpose.
     *
     * @param group
     *            the LDAP group.
     */
    @Override
    public void delete(final GroupOrg group) {

        /*
         * Remove from this group, all groups within (sub LDAP DN) this group. This operation is needed since we are not
         * rebuilding the cache from the LDAP. This save a lot of computations.
         */
        findAll().values().stream().filter(g -> DnUtils.equalsOrParentOf(group.getDn(), g.getDn()))
                .collect(Collectors.toList()).forEach(this::removeFromJavaCache);

        // Remove from LDAP the recursively the group. Anything that was not nicely cleaned will be deleted there.
        template.unbind(org.springframework.ldap.support.LdapUtils.newLdapName(group.getDn()), true);

        // Also, update the cache
        cacheRepository.delete(group);
    }

    @Override
    public void empty(final GroupOrg group, final Map<String, UserOrg> users) {
        cacheRepository.empty(group, users);
    }

    @Override
    public GroupOrg create(final String dn, final String cn) {
        return cacheRepository.create(super.create(dn, cn));
    }

    /**
     * Add an "uniqueMember" to given group. Cache is not updated there.
     *
     * @param element
     *            The new member to add.
     * @param group
     *            CN of the group to update. Must be normalized.
     * @return the target {@link GroupOrg}.
     */
    private GroupOrg addMember(final ResourceOrg element, final String group) {
        final GroupOrg groupLdap = findById(group);
        if (!groupLdap.getMembers().contains(element.getId())) {
            // Not useless operation
            addAttributes(groupLdap.getDn(), UNIQUE_MEMBER, Collections.singletonList(element.getDn()));
        }
        return groupLdap;
    }

    /**
     * Update the uniqueMember attribute of the user having changed DN. Cache is not updated since.
     *
     * @param oldUniqueMemberDn
     *            Old DN of the member to update.
     * @param newUniqueMemberDn
     *            New DN of the member to update. UID of the DN should unchanged.
     * @param group
     *            CN of the group to update.
     */
    public void updateMemberDn(final String group, final String oldUniqueMemberDn, final String newUniqueMemberDn) {
        final GroupOrg groupLdap = findById(group);
        final ModificationItem[] mods = new ModificationItem[2];
        mods[0] = new ModificationItem(DirContext.REMOVE_ATTRIBUTE,
                new BasicAttribute(UNIQUE_MEMBER, oldUniqueMemberDn));
        mods[1] = new ModificationItem(DirContext.ADD_ATTRIBUTE,
                new BasicAttribute(UNIQUE_MEMBER, newUniqueMemberDn));
        template.modifyAttributes(org.springframework.ldap.support.LdapUtils.newLdapName(groupLdap.getDn()), mods);
    }

    @Override
    public void addUser(final UserOrg user, final String group) {
        // Add to Java cache and to SQL cache
        cacheRepository.addUserToGroup(user, addMember(user, group));
    }

    @Override
    public void addGroup(final GroupOrg subGroup, final String toGroup) {
        // Add to Java cache and to SQL cache
        cacheRepository.addGroupToGroup(subGroup, addMember(subGroup, toGroup));
    }

    @Override
    public void removeUser(final UserOrg user, final String group) {
        // Remove from Java cache and from SQL cache
        cacheRepository.removeUserFromGroup(user, removeMember(user, group));
    }

    /**
     * Remove a group from another group. Cache is updated. There is no deletion.
     *
     * @param subGroup
     *            {@link GroupOrg} to remove.
     * @param group
     *            CN of the group to update.
     */
    public void removeGroup(final GroupOrg subGroup, final String group) {
        // Remove from Java cache and from SQL cache
        cacheRepository.removeGroupFromGroup(subGroup, removeMember(subGroup, group));
    }

    /**
     * Remove an "uniqueMember" from given group. Cache is not updated there.
     *
     * @param uniqueMember
     *            DN of the member to remove.
     * @param group
     *            CN of the group to update. Must be normalized.
     * @return the {@link GroupOrg} where the member has just been removed from.
     */
    private GroupOrg removeMember(final ResourceOrg uniqueMember, final String group) {
        final GroupOrg groupLdap = findById(group);
        if (groupLdap.getMembers().contains(uniqueMember.getId())
                || groupLdap.getSubGroups().contains(uniqueMember.getId())) {
            // Not useless LDAP operation, avoid LDAP duplicate deletion
            final ModificationItem[] mods = new ModificationItem[1];
            mods[0] = new ModificationItem(DirContext.REMOVE_ATTRIBUTE,
                    new BasicAttribute(UNIQUE_MEMBER, uniqueMember.getDn()));
            try {
                template.modifyAttributes(org.springframework.ldap.support.LdapUtils.newLdapName(groupLdap.getDn()),
                        mods);
            } catch (final org.springframework.ldap.AttributeInUseException aiue) {
                // Even if the membership update failed, the user does not exist anymore. A broken reference can remains
                // in LDAP, but this case is well managed.
                log.info("Unable to remove user {} from the group {} : {}", uniqueMember.getDn(), group, aiue);
            } catch (final org.springframework.ldap.SchemaViolationException sve) { // NOSONAR - Exception is logged
                // Occurs when there is a LDAP schema violation such as as last member removed
                log.warn("Unable to remove user {} from the group {}", uniqueMember.getDn(), group, sve);
                throw new ValidationJsonException("groups", "last-member-of-group", "user", uniqueMember.getId(),
                        "group", group);
            }
        }
        return groupLdap;
    }

    /**
     * Map {@link GroupOrg} to LDAP
     */
    @Override
    protected void mapToContext(final GroupOrg entry, final DirContextOperations context) {
        context.setAttributeValue("cn", entry.getName());
        // Dummy member for initial group, due to LDAP compliance of class "groupOfUniqueNames"
        context.setAttributeValue(UNIQUE_MEMBER, DEFAULT_MEMBER_DN);
    }

    @Override
    protected GroupOrg newContainer(final String dn, final String cn) {
        return new GroupOrg(dn.toLowerCase(Locale.ENGLISH), cn, new HashSet<>());
    }

    @Override
    public void addAttributes(final String dn, final String attribute, final Collection<String> values) {
        if (values.isEmpty()) {
            // Ignore this call
            return;
        }

        // Build the modification operation
        final ModificationItem[] mods = values.stream()
                .map(v -> new ModificationItem(DirContext.ADD_ATTRIBUTE, new BasicAttribute(attribute, v)))
                .toArray(ModificationItem[]::new);
        try {
            // Perform the addition
            template.modifyAttributes(org.springframework.ldap.support.LdapUtils.newLdapName(dn), mods);
        } catch (final org.springframework.ldap.AttributeInUseException aiue) {
            if (!aiue.getMessage()
                    .matches(".*(value #0 already exists|error code 20|ATTRIBUTE_OR_VALUE_EXISTS).*")) {
                throw aiue;
            }
            log.info("{} is already member of {}", values, dn);
        }
    }

    @Override
    public GroupOrg findByDepartment(final String department) {
        final AndFilter filter = new AndFilter().and(new EqualsFilter("objectclass", GROUP_OF_UNIQUE_NAMES))
                .and(new EqualsFilter(DEPARTMENT_ATTRIBUTE, department));
        return template.search(groupsBaseDn, filter.encode(), (Object ctx) -> (DirContextAdapter) ctx).stream()
                .findFirst().map(c -> c.getStringAttribute("cn")).map(Normalizer::normalize).map(this::findById)
                .orElse(null);
    }
}