UserProfileService.java

/*
 * Copyright (C) 2020-2024 by Savoir-faire Linux
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package net.jami.jams.ldap.connector.service;

import lombok.extern.slf4j.Slf4j;

import net.jami.datastore.main.DataStore;
import net.jami.jams.common.objects.user.UserProfile;
import net.jami.jams.ldap.connector.LDAPConnector;
import net.jami.jams.server.core.workflows.RevokeUserFlow;

import org.ldaptive.Connection;
import org.ldaptive.ConnectionFactory;
import org.ldaptive.LdapEntry;
import org.ldaptive.SearchOperation;
import org.ldaptive.SearchRequest;
import org.ldaptive.SearchResponse;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Slf4j
public class UserProfileService {
    private static DataStore dataStore;
    private final ConnectionFactory connectionFactory;

    public UserProfileService(DataStore dataStore, ConnectionFactory connectionFactory) {
        UserProfileService.dataStore = dataStore;
        this.connectionFactory = connectionFactory;
    }

    public List<UserProfile> getUserProfile(
            String queryString, String field, boolean exactMatch, Optional<Integer> page) {
        Connection connection = null;
        try {
            queryString =
                    new String(
                            queryString.getBytes(StandardCharsets.UTF_8),
                            StandardCharsets.ISO_8859_1);
            connection = connectionFactory.getConnection();
            connection.open();
            SearchOperation search = new SearchOperation(connectionFactory);
            SearchResponse res = search.execute(buildRequest(queryString, field, exactMatch));

            Collection<LdapEntry> entries = getEntriesPage(res, page);

            if (entries.isEmpty()) return new ArrayList<>();

            List<UserProfile> profilesFromResponse =
                    entries.stream()
                            .map(UserProfileService::profileFromResponse)
                            .collect(Collectors.toList());
            for (UserProfile p : profilesFromResponse) {
                dataStore.getUserProfileDao().insertIfNotExists(p);
            }

            return profilesFromResponse;
        } catch (Exception e) {
            log.info("Failed to search LDAP directory with error " + e);
            return null;
        } finally {
            connection.close();
        }
    }

    private Collection<LdapEntry> getEntriesPage(SearchResponse res, Optional<Integer> page) {
        int size = res.getEntries().size();

        DataStore.NUM_PAGES = (Integer) size / DataStore.RESULTS_PER_PAGE;
        if (size % DataStore.RESULTS_PER_PAGE != 0) DataStore.NUM_PAGES++;

        if (page.isEmpty() || size == 0) {
            return res.getEntries();
        }

        if (size < DataStore.RESULTS_PER_PAGE) res = res.subResult(0, size);
        else if (page.get() * DataStore.RESULTS_PER_PAGE > size)
            res = res.subResult((page.get() - 1) * DataStore.RESULTS_PER_PAGE, size);
        else
            res =
                    res.subResult(
                            (page.get() - 1) * DataStore.RESULTS_PER_PAGE,
                            (page.get() * DataStore.RESULTS_PER_PAGE));

        return res.getEntries();
    }

    public static SearchRequest buildRequest(String queryString, String field, boolean exactMatch) {

        if (!exactMatch) {
            if (!queryString.startsWith("*")) queryString = "*".concat(queryString);
            if (!queryString.endsWith("*")) queryString = queryString.concat("*");
        }

        if (field.equals("LOGON_NAME")) {
            return SearchRequest.builder()
                    .dn(LDAPConnector.settings.getBaseDN())
                    .filter("(&(uid=" + queryString + "))")
                    .build();
        }
        if (field.equals("FULL_TEXT_NAME")) {
            return SearchRequest.builder()
                    .dn(LDAPConnector.settings.getBaseDN())
                    .filter("(|(givenName=" + queryString + ")(sn=" + queryString + ")")
                    .build();
        }
        return null;
    }

    public static UserProfile profileFromResponse(LdapEntry entry) {
        // Use reflection to remap.
        HashMap<String, String> fieldMap = LDAPConnector.settings.getFieldMappings();

        try {
            UserProfile userProfile = new UserProfile();
            for (String attribute : entry.getAttributeNames()) {
                if (fieldMap.containsKey(attribute)) {
                    UserProfile.exposedMethods
                            .get("set" + fieldMap.get(attribute))
                            .invoke(userProfile, entry.getAttribute(attribute).getStringValue());
                }
            }
            userProfile.setDefaultValues();
            return userProfile;
        } catch (Exception e) {
            log.error("An error occurred while trying to invoke methods: " + e);
            return null;
        }
    }

    public void synchronizeUsersWithLDAP() {
        log.info("Synchronizing LDAP user profiles");
        // Fetcg all users from the LDAP
        List<UserProfile> profilesFromLDAP =
                getUserProfile("*", "LOGON_NAME", false, Optional.empty());
        // Do not revoke users if there is an error, the LDAP server could be down.
        if (profilesFromLDAP != null) {
            // There is a use case where a user is not in the LDAP server but is in the database.
            // When this happens, we need to revoke the user from the database.
            List<UserProfile> profilesFromDatabase =
                    dataStore.getUserProfileDao().getAllUserProfile();
            for (UserProfile p : profilesFromDatabase) {
                if (profilesFromLDAP.stream()
                        .noneMatch(r -> r.getUsername().equals(p.getUsername()))) {
                    log.info("Revoking user " + p.getUsername() + " from the database.");
                    RevokeUserFlow.revokeUser(p.getUsername());
                    // We also remove the user from the local_directory table to avoid duplicate
                    // revocations
                    dataStore.getUserProfileDao().deleteUserProfile(p.getUsername());
                }
            }
        }
    }
}