LdapService.java

/*
 * Copyright (c) 2002-2021, City of Paris
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice
 *     and the following disclaimer.
 *
 *  2. Redistributions in binary form must reproduce the above copyright notice
 *     and the following disclaimer in the documentation and/or other materials
 *     provided with the distribution.
 *
 *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
 *     contributors may be used to endorse or promote products derived from
 *     this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * License 1.0
 */
package fr.paris.lutece.plugins.adminauthenticationldap.service;

import fr.paris.lutece.plugins.adminauthenticationldap.AdminLdapAuthentication;
import fr.paris.lutece.plugins.adminauthenticationldap.business.AdminLdapUser;
import fr.paris.lutece.portal.business.user.AdminUser;
import fr.paris.lutece.portal.service.datastore.DatastoreService;
import fr.paris.lutece.portal.service.security.RsaService;
import fr.paris.lutece.portal.service.util.AppLogService;
import fr.paris.lutece.portal.service.util.AppPropertiesService;
import fr.paris.lutece.util.ldap.LdapUtil;
import org.apache.commons.lang3.StringUtils;

import javax.naming.CommunicationException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.*;
import javax.security.auth.login.FailedLoginException;
import java.security.GeneralSecurityException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

public class LdapService
{

    private static final String PROPERTY_BIND_DN = "adminauthenticationldap.ldap.connectionName";
    private static final String PROPERTY_BIND_PASSWORD = "adminauthenticationldap.ldap.connectionPassword";
    private static final String PROPERTY_IS_ENCRYPTED = "adminauthenticationldap.ldap.isEncrypted";
    private static final String PROPERTY_USER_SUBTREE = "adminauthenticationldap.ldap.userSubtree";
    private static final String PROPERTY_USER_DN_SEARCH_BASE = "adminauthenticationldap.ldap.userBase";
    private static final String PROPERTY_ROOT_DN_SEARCH_BASE = "adminauthenticationldap.ldap.rootBase";
    private static final String PROPERTY_INITIAL_CONTEXT_PROVIDER = "adminauthenticationldap.ldap.initialContextProvider";
    private static final String PROPERTY_PROVIDER_URL = "adminauthenticationldap.ldap.connectionUrl";
    private static final String PROPERTY_USER_DN_SEARCH_FILTER_BY_ACCESS_CODE = "adminauthenticationldap.ldap.userSearch.filterAccessCode";
    private static final String PROPERTY_USER_DN_SEARCH_FILTER_BY_CRITERIA = "adminauthenticationldap.ldap.userSearch.filterCriteria";
    private static final String PROPERTY_USER_DN_SEARCH_GROUP_FILTER = "adminauthenticationldap.ldap.userSearch.groupFilter";
    private static final String PROPERTY_USER_ATTRIBUTE_NAME_ACCESS_CODE = "adminauthenticationldap.ldap.dn.attributeName.accessCode";
    private static final String PROPERTY_USER_ATTRIBUTE_NAME_FAMILY_NAME = "adminauthenticationldap.ldap.dn.attributeName.family";
    private static final String PROPERTY_USER_ATTRIBUTE_NAME_GIVEN_NAME = "adminauthenticationldap.ldap.dn.attributeName.given";
    private static final String PROPERTY_USER_ATTRIBUTE_NAME_EMAIL = "adminauthenticationldap.ldap.dn.attributeName.email";
    private static final String PROPERTY_USER_ATTRIBUTE_GROUP = "adminauthenticationldap.ldap.dn.attributeName.groupMemberOf";
    private static final String PROPERTY_USER_ATTRIBUTE_DN = "adminauthenticationldap.ldap.dn.attributeName.distinguishedName";

    private static final String ATTRIBUTE_ACCESS_CODE = AppPropertiesService.getProperty( PROPERTY_USER_ATTRIBUTE_NAME_ACCESS_CODE );
    private static final String ATTRIBUTE_FAMILY_NAME = AppPropertiesService.getProperty( PROPERTY_USER_ATTRIBUTE_NAME_FAMILY_NAME );
    private static final String ATTRIBUTE_GIVEN_NAME = AppPropertiesService.getProperty( PROPERTY_USER_ATTRIBUTE_NAME_GIVEN_NAME );
    private static final String ATTRIBUTE_EMAIL = AppPropertiesService.getProperty( PROPERTY_USER_ATTRIBUTE_NAME_EMAIL );
    private static final String ATTRIBUTE_GROUP = AppPropertiesService.getProperty( PROPERTY_USER_ATTRIBUTE_GROUP );
    private static final String ATTRIBUTE_DN = AppPropertiesService.getProperty( PROPERTY_USER_ATTRIBUTE_DN );

    private static final String BIND_DN = AppPropertiesService.getProperty( PROPERTY_BIND_DN );
    private static final String BIND_PASSWORD = AppPropertiesService.getProperty( PROPERTY_BIND_PASSWORD,"" );
    private static final Boolean IS_ENCRYPTED = AppPropertiesService.getPropertyBoolean( PROPERTY_IS_ENCRYPTED,false );
    private static final String SEARCH_SCOPE = AppPropertiesService.getProperty( PROPERTY_USER_SUBTREE, "false" );
    private static final String SEARCH_FILTER_BY_CRITERIA = AppPropertiesService.getProperty( PROPERTY_USER_DN_SEARCH_FILTER_BY_CRITERIA );
    private static final String SEARCH_FILTER_BY_ACCESS_CODE = AppPropertiesService.getProperty( PROPERTY_USER_DN_SEARCH_FILTER_BY_ACCESS_CODE );
    private static final String INITIAL_CONTEXT_PROVIDER = AppPropertiesService.getProperty( PROPERTY_INITIAL_CONTEXT_PROVIDER );
    private static final String PROVIDER_URL = AppPropertiesService.getProperty( PROPERTY_PROVIDER_URL );
    private static final String USER_DN_SEARCH_BASE = AppPropertiesService.getProperty( PROPERTY_USER_DN_SEARCH_BASE, "" );
    private static final String ROOT_DN_SEARCH_BASE = AppPropertiesService.getProperty( PROPERTY_ROOT_DN_SEARCH_BASE );
    private static final String SEARCH_FILTER_GROUP = AppPropertiesService.getProperty( PROPERTY_USER_DN_SEARCH_GROUP_FILTER );

    // Constant
    private static final String CONSTANT_WILDCARD = "*";

    private LdapService( )
    {
    }

    public static DirContext getAdminContext( )
    {
        return getNewContext( BIND_DN, getBindPassword());
    }

    private static String getBindPassword() {
        String strPass = BIND_PASSWORD;

        if ( StringUtils.isEmpty( strPass ) || DatastoreService.existsKey( PROPERTY_BIND_PASSWORD ) )
        {
            if ( DatastoreService.existsKey( PROPERTY_BIND_PASSWORD ) )
            {
                strPass = DatastoreService.getDataValue(PROPERTY_BIND_PASSWORD,"");
            }
        }

        if ( StringUtils.isEmpty( strPass ) )
        {
            AppLogService.error("No password for Ldap.");
            return "";
        }

        if( IS_ENCRYPTED )
        {
            try
            {
                if ( strPass.startsWith("PLAINTEXT:") )
                {
                    strPass = strPass.replace("PLAINTEXT:","");
                    DatastoreService.setDataValue(PROPERTY_BIND_PASSWORD, "RSA:" + RsaService.encryptRsa( strPass ) );
                }
                else
                {
                    strPass = RsaService.decryptRsa(strPass.replace("RSA:",""));
                }
            }
            catch (GeneralSecurityException e)
            {
                AppLogService.error("Error decrypting password.");
                return "";
            }
        }

        return strPass;
    }

    public static DirContext getNewContext( String strDN, String strPassword )
    {
        try
        {
            return LdapUtil.getContext( INITIAL_CONTEXT_PROVIDER, PROVIDER_URL, strDN, strPassword );
        }
        catch( Exception e )
        {
            AppLogService.error( "Unable to open a new connection to LDAP to " + PROVIDER_URL, e );
            return null;
        }
    }

    public static void freeContext( DirContext context )
    {
        try
        {
            if ( context != null )
            {
                LdapUtil.freeContext( context );
            }
        }
        catch( NamingException e )
        {
            AppLogService.error( "Unable to free ldap context ", e );
        }
    }

    private static String getUserBindDN( String strAccessCode )
    {
        StringBuilder sb = new StringBuilder( );
        sb.append( ATTRIBUTE_ACCESS_CODE ).append( "=" );
        sb.append( strAccessCode );
        sb.append( "," );
        sb.append( USER_DN_SEARCH_BASE );
        if ( StringUtils.isNotEmpty( ROOT_DN_SEARCH_BASE ) )
        {
            sb.append( "," ).append( ROOT_DN_SEARCH_BASE );
        }

        return sb.toString( );
    }

    public static SearchResult getUserSearchResult( String strId )
    {
        List<SearchResult> srList = getUserSearchResult( 1, getCompleteFilter( SEARCH_FILTER_BY_ACCESS_CODE ), strId );
        if ( srList.size( ) != 1 )
        {
            return null;
        }
        return srList.get( 0 );
    }

    public static AdminUser getAdminUser( String strId )
    {
        return getUserFromSr( getUserSearchResult( strId ) );
    }

    public static AdminUser getUserFromSr( SearchResult sr )
    {
        AdminUser user = null;
        if ( sr != null )
        {
            String strLastName = getSrAttribute( sr, ATTRIBUTE_FAMILY_NAME );
            String strFirstName = getSrAttribute( sr, ATTRIBUTE_GIVEN_NAME );
            String strEmail = getSrAttribute( sr, ATTRIBUTE_EMAIL );
            String strAccessCode = getSrAttribute( sr, ATTRIBUTE_ACCESS_CODE );

            if ( strAccessCode != null && !"".equals( strAccessCode ) )
            {
                user = new AdminUser( );
                user.setAuthenticationService( AdminLdapAuthentication.AUTH_SERVICE_NAME );
                user.setAccessCode( strAccessCode );
                user.setLastName( strLastName );
                user.setFirstName( strFirstName );
                user.setEmail( strEmail );
            }
        }
        return user;
    }

    public static List<SearchResult> getUserSearchResult( String strParameterLastName, String strParameterFirstName, String strParameterEmail )
    {
        return getUserSearchResult( 0, getCompleteFilter( SEARCH_FILTER_BY_CRITERIA ), checkSyntax( strParameterLastName ),
                checkSyntax( strParameterFirstName ), checkSyntax( strParameterEmail ) );
    }

    public static List<AdminUser> getAdminUserSearchResult( String strParameterLastName, String strParameterFirstName, String strParameterEmail )
    {
        List<AdminUser> userList = new ArrayList<>( );

        for ( SearchResult sr : getUserSearchResult( strParameterLastName, strParameterFirstName, strParameterEmail ) )
        {
            AdminUser user = getUserFromSr( sr );
            if ( user != null )
            {
                userList.add( user );
            }
        }
        return userList;
    }

    public static List<SearchResult> getUserSearchResult( int nLimit, String strLdapSearchFilterTmpl, String... lstSearchParameter )
    {
        List<SearchResult> srList = new ArrayList<>( );

        if ( lstSearchParameter != null && lstSearchParameter.length > 0 )
        {

            String strUserSearchFilter = MessageFormat.format( strLdapSearchFilterTmpl, (Object [ ]) lstSearchParameter );

            SearchControls scUserSearchControls = new SearchControls( );
            scUserSearchControls.setSearchScope( getUserDnSearchScope( ) );
            scUserSearchControls.setReturningObjFlag( true );
            scUserSearchControls.setCountLimit( nLimit );

            NamingEnumeration<SearchResult> userResults;
            DirContext context = getAdminContext( );

            try
            {
                userResults = LdapUtil.searchUsers( context, strUserSearchFilter, USER_DN_SEARCH_BASE + "," + ROOT_DN_SEARCH_BASE, "", scUserSearchControls );
                AppLogService.debug( " Search users params  : " + strUserSearchFilter );

                while ( ( userResults != null ) && userResults.hasMore( ) )
                {
                    SearchResult sr = userResults.next( );
                    srList.add( sr );
                }
            }
            catch( NamingException e )
            {
                AppLogService.error( "Error while searching for users  with search filter : " + getDebugInfo( strUserSearchFilter ), e );
            }
            finally
            {
                freeContext( context );
            }

        }
        return srList;
    }

    public static String getSrAttribute( SearchResult sr, String strAttributeName )
    {
        try
        {
            return sr.getAttributes( ).get( strAttributeName ).get( ).toString( );
        }
        catch( NamingException e )
        {
            AppLogService.error( "Error while getting attribute + '" + strAttributeName + "' from ldap.", e );
        }
        return null;
    }

    private static String getDebugInfo( String strUserSearchFilter )
    {
        StringBuilder sb = new StringBuilder( );
        sb.append( "userBase : " );
        sb.append( USER_DN_SEARCH_BASE );
        sb.append( "\nuserSearch : " );
        sb.append( strUserSearchFilter );

        return sb.toString( );
    }

    private static String checkSyntax( String in )
    {
        return ( ( ( in == null ) || ( in.equals( "" ) ) ) ? CONSTANT_WILDCARD : in + CONSTANT_WILDCARD );
    }

    private static String getCompleteFilter( String strFilter )
    {
        StringBuilder sb = new StringBuilder( );
        sb.append( "(&" );
        sb.append( strFilter );
        if ( StringUtils.isNotEmpty( ATTRIBUTE_GROUP ) && StringUtils.isNotEmpty( SEARCH_FILTER_GROUP ) )
        {
            sb.append( "(" ).append( ATTRIBUTE_GROUP ).append( "=" ).append( SEARCH_FILTER_GROUP );
            if ( StringUtils.isNotEmpty( ROOT_DN_SEARCH_BASE ) )
            {
                sb.append( "," ).append( ROOT_DN_SEARCH_BASE );
            }
            sb.append( ")" );
        }
        sb.append( ")" );

        return sb.toString( );
    }

    private static int getUserDnSearchScope( )
    {
        if ( SEARCH_SCOPE.equalsIgnoreCase( "true" ) )
        {
            return SearchControls.SUBTREE_SCOPE;
        }
        return SearchControls.ONELEVEL_SCOPE;
    }

    public static void login( String strAccessCode, String strUserPassword ) throws FailedLoginException
    {
        DirContext context = null;
        try
        {
            SearchResult sr = getUserSearchResult( strAccessCode );
            if ( sr != null )
            {
                context = LdapUtil.bindUser( INITIAL_CONTEXT_PROVIDER, PROVIDER_URL, getSrAttribute( sr, ATTRIBUTE_DN ), strUserPassword );
            }
            else
            {
                throw new FailedLoginException( );
            }
        }
        catch( NamingException e )
        {
            throw new FailedLoginException( );
        }
        finally
        {
            freeContext( context );
        }
    }

}