<commit_before>/*
 * This file is part of Alpine.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 * Copyright (c) <NAME>. All Rights Reserved.
 */
package alpine.server.auth;

import alpine.Config;
import alpine.common.logging.Logger;
import alpine.common.validation.LdapStringSanitizer;
import alpine.model.LdapUser;
import org.apache.commons.lang3.StringUtils;

import javax.naming.CommunicationException;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.PartialResultException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Hashtable;
import java.util.List;

/**
 * A convenience wrapper for LDAP connections and commons LDAP tasks.
 *
 * @since 1.4.0
 */
public class LdapConnectionWrapper {

    private static final Logger LOGGER = Logger.getLogger(LdapConnectionWrapper.class);

    private static final String BIND_USERNAME = Config.getInstance().getProperty(Config.AlpineKey.LDAP_BIND_USERNAME);
    private static final String BIND_PASSWORD = Config.getInstance().getPropertyOrFile(Config.AlpineKey.LDAP_BIND_PASSWORD);
    private static final String LDAP_SECURITY_AUTH = Config.getInstance().getProperty(Config.AlpineKey.LDAP_SECURITY_AUTH);
    private static final String LDAP_AUTH_USERNAME_FMT = Config.getInstance().getProperty(Config.AlpineKey.LDAP_AUTH_USERNAME_FMT);
    private static final String USER_GROUPS_FILTER = Config.getInstance().getProperty(Config.AlpineKey.LDAP_USER_GROUPS_FILTER);
    private static final String GROUPS_FILTER = Config.getInstance().getProperty(Config.AlpineKey.LDAP_GROUPS_FILTER);
    private static final String GROUPS_SEARCH_FILTER = Config.getInstance().getProperty(Config.AlpineKey.LDAP_GROUPS_SEARCH_FILTER);
    private static final String USERS_SEARCH_FILTER = Config.getInstance().getProperty(Config.AlpineKey.LDAP_USERS_SEARCH_FILTER);

    public static final boolean LDAP_ENABLED = Config.getInstance().getPropertyAsBoolean(Config.AlpineKey.LDAP_ENABLED);
    public static final String LDAP_URL = Config.getInstance().getProperty(Config.AlpineKey.LDAP_SERVER_URL);
    public static final String BASE_DN = Config.getInstance().getProperty(Config.AlpineKey.LDAP_BASEDN);
    public static final String ATTRIBUTE_MAIL = Config.getInstance().getProperty(Config.AlpineKey.LDAP_ATTRIBUTE_MAIL);
    public static final String ATTRIBUTE_NAME = Config.getInstance().getProperty(Config.AlpineKey.LDAP_ATTRIBUTE_NAME);

    public static final boolean USER_PROVISIONING = Config.getInstance().getPropertyAsBoolean(Config.AlpineKey.LDAP_USER_PROVISIONING);
    public static final boolean TEAM_SYNCHRONIZATION = Config.getInstance().getPropertyAsBoolean(Config.AlpineKey.LDAP_TEAM_SYNCHRONIZATION);

    public static final boolean LDAP_CONFIGURED = LDAP_ENABLED && StringUtils.isNotBlank(LDAP_URL);
    private static final boolean IS_LDAP_SSLTLS = StringUtils.isNotBlank(LDAP_URL) && LDAP_URL.startsWith("ldaps:");


    /**
     * Asserts a users credentials. Returns an LdapContext if assertion is successful
     * or an exception for any other reason.
     *
     * @param userDn the users DN to assert
     * @param password the password to assert
     * @return the LdapContext upon a successful connection
     * @throws NamingException when unable to establish a connection
     * @since 1.4.0
     */
    public LdapContext createLdapContext(final String userDn, final String password) throws NamingException {
        LOGGER.debug("Creating LDAP context for: " +userDn);
        if (StringUtils.isEmpty(userDn) || StringUtils.isEmpty(password)) {
            throw new NamingException("Username or password cannot be empty or null");
        }
        final Hashtable<String, String> env = new Hashtable<>();
        if (StringUtils.isNotBlank(LDAP_SECURITY_AUTH)) {
            env.put(Context.SECURITY_AUTHENTICATION, LDAP_SECURITY_AUTH);
        }
        env.put(Context.SECURITY_PRINCIPAL, userDn);
        env.put(Context.SECURITY_CREDENTIALS, password);
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.PROVIDER_URL, LDAP_URL);
        if (IS_LDAP_SSLTLS) {
            env.put("java.naming.ldap.factory.socket", "alpine.crypto.RelaxedSSLSocketFactory");
        }
        try {
            return new InitialLdapContext(env, null);
        } catch (CommunicationException e) {
            LOGGER.error("Failed to connect to directory server", e);
            throw(e);
        } catch (NamingException e) {
            throw new NamingException("Failed to authenticate user");
        }
    }

    /**
     * Creates a DirContext with the applications configuration settings.
     * @return a DirContext
     * @throws NamingException if an exception is thrown
     * @since 1.4.0
     */
    public DirContext createDirContext() throws NamingException {
        LOGGER.debug("Creating directory service context (DirContext)");
        final Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.SECURITY_PRINCIPAL, BIND_USERNAME);
        env.put(Context.SECURITY_CREDENTIALS, BIND_PASSWORD);
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.PROVIDER_URL, LDAP_URL);
        if (IS_LDAP_SSLTLS) {
            env.put("java.naming.ldap.factory.socket", "alpine.crypto.RelaxedSSLSocketFactory");
        }
        return new InitialDirContext(env);
    }

    /**
     * Retrieves a list of all groups the user is a member of.
     * @param dirContext a DirContext<commit_msg>correct the package name for Relaxed Factory<commit_after>/*
 * This file is part of Alpine.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 * Copyright (c) <NAME>. All Rights Reserved.
 */
package alpine.server.auth;

import alpine.Config;
import alpine.common.logging.Logger;
import alpine.common.validation.LdapStringSanitizer;
import alpine.model.LdapUser;
import org.apache.commons.lang3.StringUtils;

import javax.naming.CommunicationException;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.PartialResultException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Hashtable;
import java.util.List;

/**
 * A convenience wrapper for LDAP connections and commons LDAP tasks.
 *
 * @since 1.4.0
 */
public class LdapConnectionWrapper {

    private static final Logger LOGGER = Logger.getLogger(LdapConnectionWrapper.class);

    private static final String BIND_USERNAME = Config.getInstance().getProperty(Config.AlpineKey.LDAP_BIND_USERNAME);
    private static final String BIND_PASSWORD = Config.getInstance().getPropertyOrFile(Config.AlpineKey.LDAP_BIND_PASSWORD);
    private static final String LDAP_SECURITY_AUTH = Config.getInstance().getProperty(Config.AlpineKey.LDAP_SECURITY_AUTH);
    private static final String LDAP_AUTH_USERNAME_FMT = Config.getInstance().getProperty(Config.AlpineKey.LDAP_AUTH_USERNAME_FMT);
    private static final String USER_GROUPS_FILTER = Config.getInstance().getProperty(Config.AlpineKey.LDAP_USER_GROUPS_FILTER);
    private static final String GROUPS_FILTER = Config.getInstance().getProperty(Config.AlpineKey.LDAP_GROUPS_FILTER);
    private static final String GROUPS_SEARCH_FILTER = Config.getInstance().getProperty(Config.AlpineKey.LDAP_GROUPS_SEARCH_FILTER);
    private static final String USERS_SEARCH_FILTER = Config.getInstance().getProperty(Config.AlpineKey.LDAP_USERS_SEARCH_FILTER);

    public static final boolean LDAP_ENABLED = Config.getInstance().getPropertyAsBoolean(Config.AlpineKey.LDAP_ENABLED);
    public static final String LDAP_URL = Config.getInstance().getProperty(Config.AlpineKey.LDAP_SERVER_URL);
    public static final String BASE_DN = Config.getInstance().getProperty(Config.AlpineKey.LDAP_BASEDN);
    public static final String ATTRIBUTE_MAIL = Config.getInstance().getProperty(Config.AlpineKey.LDAP_ATTRIBUTE_MAIL);
    public static final String ATTRIBUTE_NAME = Config.getInstance().getProperty(Config.AlpineKey.LDAP_ATTRIBUTE_NAME);

    public static final boolean USER_PROVISIONING = Config.getInstance().getPropertyAsBoolean(Config.AlpineKey.LDAP_USER_PROVISIONING);
    public static final boolean TEAM_SYNCHRONIZATION = Config.getInstance().getPropertyAsBoolean(Config.AlpineKey.LDAP_TEAM_SYNCHRONIZATION);

    public static final boolean LDAP_CONFIGURED = LDAP_ENABLED && StringUtils.isNotBlank(LDAP_URL);
    private static final boolean IS_LDAP_SSLTLS = StringUtils.isNotBlank(LDAP_URL) && LDAP_URL.startsWith("ldaps:");


    /**
     * Asserts a users credentials. Returns an LdapContext if assertion is successful
     * or an exception for any other reason.
     *
     * @param userDn the users DN to assert
     * @param password the password to assert
     * @return the LdapContext upon a successful connection
     * @throws NamingException when unable to establish a connection
     * @since 1.4.0
     */
    public LdapContext createLdapContext(final String userDn, final String password) throws NamingException {
        LOGGER.debug("Creating LDAP context for: " +userDn);
        if (StringUtils.isEmpty(userDn) || StringUtils.isEmpty(password)) {
            throw new NamingException("Username or password cannot be empty or null");
        }
        final Hashtable<String, String> env = new Hashtable<>();
        if (StringUtils.isNotBlank(LDAP_SECURITY_AUTH)) {
            env.put(Context.SECURITY_AUTHENTICATION, LDAP_SECURITY_AUTH);
        }
        env.put(Context.SECURITY_PRINCIPAL, userDn);
        env.put(Context.SECURITY_CREDENTIALS, password);
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.PROVIDER_URL, LDAP_URL);
        if (IS_LDAP_SSLTLS) {
            env.put("java.naming.ldap.factory.socket", "alpine.security.crypto.RelaxedSSLSocketFactory");
        }
        try {
            return new InitialLdapContext(env, null);
        } catch (CommunicationException e) {
            LOGGER.error("Failed to connect to directory server", e);
            throw(e);
        } catch (NamingException e) {
            throw new NamingException("Failed to authenticate user");
        }
    }

    /**
     * Creates a DirContext with the applications configuration settings.
     * @return a DirContext
     * @throws NamingException if an exception is thrown
     * @since 1.4.0
     */
    public DirContext createDirContext() throws NamingException {
        LOGGER.debug("Creating directory service context (DirContext)");
        final Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.SECURITY_PRINCIPAL, BIND_USERNAME);
        env.put(Context.SECURITY_CREDENTIALS, BIND_PASSWORD);
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.PROVIDER_URL, LDAP_URL);
        if (IS_LDAP_SSLTLS) {
            env.put("java.naming.ldap.factory.socket", "alpine.security.crypto.RelaxedSSLSocketFactory");
        }
        return new InitialDirContext(env);
    }

    /**
     * Retrieves a list of all groups the user is a member of.
     * @param dirContext a DirContext
{'fraction_non_alphanumeric': 0.0883367839889579, 'fraction_numerical': 0.0024537995552488304, 'mean_word_length': 4.1774513695911075, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6891138', 'n_tokens_mistral': 3821, 'n_tokens_neox': 3838, 'n_words': 915}