org.alfresco.repo.security.authentication.ldap.LDAPInitialDirContextFactoryImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.repo.security.authentication.ldap.LDAPInitialDirContextFactoryImpl.java

Source

/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.security.authentication.ldap;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.naming.AuthenticationNotSupportedException;
import javax.naming.CommunicationException;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.ldap.Control;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.PagedResultsControl;
import javax.naming.ldap.PagedResultsResponseControl;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.security.authentication.AlfrescoSSLSocketFactory;
import org.alfresco.repo.security.authentication.AuthenticationDiagnostic;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.util.ApplicationContextHelper;
import org.alfresco.util.PropertyCheck;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;

public class LDAPInitialDirContextFactoryImpl implements LDAPInitialDirContextFactory, InitializingBean {
    private static final Log logger = LogFactory.getLog(LDAPInitialDirContextFactoryImpl.class);

    private static Set<Map<String, String>> checkedEnvs = Collections
            .synchronizedSet(new HashSet<Map<String, String>>(11));

    private Map<String, String> defaultEnvironment = Collections.<String, String>emptyMap();
    private Map<String, String> authenticatedEnvironment = Collections.<String, String>emptyMap();
    private Map<String, String> poolSystemProperties = Collections.<String, String>emptyMap();
    private String trustStorePath;
    private String trustStoreType;
    private String trustStorePassPhrase;

    public String getTrustStorePath() {
        return trustStorePath;
    }

    public void setTrustStorePath(String trustStorePath) {
        if (PropertyCheck.isValidPropertyString(trustStorePath)) {
            this.trustStorePath = trustStorePath;
        }
    }

    public String getTrustStoreType() {
        return trustStoreType;
    }

    public void setTrustStoreType(String trustStoreType) {
        if (PropertyCheck.isValidPropertyString(trustStoreType)) {
            this.trustStoreType = trustStoreType;
        }
    }

    public String getTrustStorePassPhrase() {
        return trustStorePassPhrase;
    }

    public void setTrustStorePassPhrase(String trustStorePassPhrase) {
        if (PropertyCheck.isValidPropertyString(trustStorePassPhrase)) {
            this.trustStorePassPhrase = trustStorePassPhrase;
        }
    }

    static {
        System.setProperty("javax.security.auth.useSubjectCredentialsOnly", "false");
    }

    public LDAPInitialDirContextFactoryImpl() {
        super();
    }

    public void setInitialDirContextEnvironment(Map<String, String> initialDirContextEnvironment) {
        this.authenticatedEnvironment = initialDirContextEnvironment;
        this.authenticatedEnvironment.values().removeAll(Collections.singleton(null));
    }

    public Map<String, String> getInitialDirContextEnvironment() {
        return authenticatedEnvironment;
    }

    public void setDefaultIntialDirContextEnvironment(Map<String, String> defaultEnvironment) {
        this.defaultEnvironment = new LinkedHashMap<String, String>(defaultEnvironment.size());
        this.defaultEnvironment.putAll(defaultEnvironment);

        // filter out empty values, as this usually means that property should be omitted.
        for (Entry<String, String> entry : defaultEnvironment.entrySet()) {
            if (entry.getValue() == null || entry.getValue().trim().length() == 0) {
                this.defaultEnvironment.remove(entry.getKey());
            }
        }
        this.defaultEnvironment.values().removeAll(Collections.singleton(null));
    }

    public InitialDirContext getDefaultIntialDirContext() throws AuthenticationException {
        return getDefaultIntialDirContext(0, new AuthenticationDiagnostic());
    }

    public void setPoolSystemProperties(Map<String, String> poolSystemProperties) {
        this.poolSystemProperties = poolSystemProperties;
        for (Entry<String, String> entry : this.poolSystemProperties.entrySet()) {
            if (entry.getValue() != null && entry.getValue().trim().length() > 0) {
                System.setProperty(entry.getKey(), entry.getValue());
            }
        }
    }

    public InitialDirContext getDefaultIntialDirContext(int pageSize) throws AuthenticationException {
        return getDefaultIntialDirContext(pageSize, new AuthenticationDiagnostic());
    }

    @Override
    public InitialDirContext getDefaultIntialDirContext(AuthenticationDiagnostic diagnostic)
            throws AuthenticationException {
        return getDefaultIntialDirContext(0, diagnostic);
    }

    public InitialDirContext getDefaultIntialDirContext(int pageSize, AuthenticationDiagnostic diagnostic)
            throws AuthenticationException {
        Hashtable<String, String> env = new Hashtable<String, String>(defaultEnvironment.size());
        env.putAll(defaultEnvironment);
        return buildInitialDirContext(env, pageSize, diagnostic);
    }

    private InitialDirContext buildInitialDirContext(Hashtable<String, String> env, int pageSize,
            AuthenticationDiagnostic diagnostic) throws AuthenticationException {
        String securityPrincipal = env.get(Context.SECURITY_PRINCIPAL);
        String providerURL = env.get(Context.PROVIDER_URL);

        if (isSSLSocketFactoryRequired()) {
            KeyStore trustStore = initTrustStore();
            AlfrescoSSLSocketFactory.initTrustedSSLSocketFactory(trustStore);
            env.put("java.naming.ldap.factory.socket", AlfrescoSSLSocketFactory.class.getName());
        }

        if (diagnostic == null) {
            diagnostic = new AuthenticationDiagnostic();
        }
        try {
            // If a page size has been requested, use LDAP v3 paging
            if (pageSize > 0) {
                InitialLdapContext ctx = new InitialLdapContext(env, null);
                ctx.setRequestControls(new Control[] { new PagedResultsControl(pageSize, Control.CRITICAL) });
                return ctx;
            } else {
                InitialDirContext ret = new InitialDirContext(env);
                Object[] args = { providerURL, securityPrincipal };
                diagnostic.addStep(AuthenticationDiagnostic.STEP_KEY_LDAP_CONNECTED, true, args);
                return ret;
            }
        } catch (javax.naming.AuthenticationException ax) {
            Object[] args1 = { securityPrincipal };
            Object[] args = { providerURL, securityPrincipal };
            diagnostic.addStep(AuthenticationDiagnostic.STEP_KEY_LDAP_CONNECTED, true, args);
            diagnostic.addStep(AuthenticationDiagnostic.STEP_KEY_LDAP_AUTHENTICATION, false, args1);

            // wrong user/password - if we get this far the connection is O.K
            Object[] args2 = { securityPrincipal, ax.getLocalizedMessage() };
            throw new AuthenticationException("authentication.err.authentication", diagnostic, args2, ax);
        } catch (CommunicationException ce) {
            Object[] args1 = { providerURL };
            diagnostic.addStep(AuthenticationDiagnostic.STEP_KEY_LDAP_CONNECTING, false, args1);

            StringBuffer message = new StringBuffer();

            message.append(ce.getClass().getName() + ", " + ce.getMessage());

            Throwable cause = ce.getCause();
            while (cause != null) {
                message.append(", ");
                message.append(cause.getClass().getName() + ", " + cause.getMessage());
                cause = cause.getCause();
            }

            // failed to connect
            Object[] args = { providerURL, message.toString() };
            throw new AuthenticationException("authentication.err.communication", diagnostic, args, cause);
        } catch (NamingException nx) {
            Object[] args = { providerURL };
            diagnostic.addStep(AuthenticationDiagnostic.STEP_KEY_LDAP_CONNECTING, false, args);

            StringBuffer message = new StringBuffer();

            message.append(nx.getClass().getName() + ", " + nx.getMessage());

            Throwable cause = nx.getCause();
            while (cause != null) {
                message.append(", ");
                message.append(cause.getClass().getName() + ", " + cause.getMessage());
                cause = cause.getCause();
            }

            // failed to connect
            Object[] args1 = { providerURL, message.toString() };
            throw new AuthenticationException("authentication.err.connection", diagnostic, args1, nx);
        } catch (IOException e) {
            Object[] args = { providerURL, securityPrincipal };
            diagnostic.addStep(AuthenticationDiagnostic.STEP_KEY_LDAP_CONNECTED, true, args);

            throw new AuthenticationException("Unable to encode LDAP v3 request controls", e);
        }
    }

    public boolean hasNextPage(DirContext ctx, int pageSize) {
        if (pageSize > 0) {
            try {
                LdapContext ldapContext = (LdapContext) ctx;
                Control[] controls = ldapContext.getResponseControls();

                // Retrieve the paged result cookie if there is one
                if (controls != null) {
                    for (Control control : controls) {
                        if (control instanceof PagedResultsResponseControl) {
                            byte[] cookie = ((PagedResultsResponseControl) control).getCookie();
                            if (cookie != null) {
                                // Prepare for next page
                                ldapContext.setRequestControls(new Control[] {
                                        new PagedResultsControl(pageSize, cookie, Control.CRITICAL) });
                                return true;
                            }
                        }
                    }
                }
            } catch (NamingException nx) {
                throw new AuthenticationException("Unable to connect to LDAP Server; check LDAP configuration", nx);
            } catch (IOException e) {
                throw new AuthenticationException(
                        "Unable to encode LDAP v3 request controls; check LDAP configuration", e);
            }

        }
        return false;
    }

    @Override
    public InitialDirContext getInitialDirContext(String principal, String credentials)
            throws AuthenticationException {
        return getInitialDirContext(principal, credentials, null);
    }

    public InitialDirContext getInitialDirContext(String principal, String credentials,
            AuthenticationDiagnostic diagnostic) throws AuthenticationException {
        if (diagnostic == null) {
            diagnostic = new AuthenticationDiagnostic();
        }

        if (principal == null) {
            // failed before we tried to do anything
            diagnostic.addStep(AuthenticationDiagnostic.STEP_KEY_VALIDATION, false, null);
            throw new AuthenticationException("Null user name provided.", diagnostic);
        }

        if (principal.length() == 0) {
            diagnostic.addStep(AuthenticationDiagnostic.STEP_KEY_VALIDATION, false, null);
            throw new AuthenticationException("Empty user name provided.", diagnostic);
        }

        if (credentials == null) {
            diagnostic.addStep(AuthenticationDiagnostic.STEP_KEY_VALIDATION, false, null);
            throw new AuthenticationException("No credentials provided.", diagnostic);
        }

        if (credentials.length() == 0) {
            diagnostic.addStep(AuthenticationDiagnostic.STEP_KEY_VALIDATION, false, null);
            throw new AuthenticationException("Empty credentials provided.", diagnostic);
        }

        diagnostic.addStep(AuthenticationDiagnostic.STEP_KEY_VALIDATION, true, null);

        Hashtable<String, String> env = new Hashtable<String, String>(authenticatedEnvironment.size());
        env.putAll(authenticatedEnvironment);
        env.put(Context.SECURITY_PRINCIPAL, principal);
        env.put(Context.SECURITY_CREDENTIALS, credentials);

        return buildInitialDirContext(env, 0, diagnostic);
    }

    public static void main(String[] args) {
        // ....build a pyramid selling scheme .....

        // A group has three user members and 2 group members .... and off we go ....
        // We make the people and groups to represent this and stick them into LDAP ...used to populate a test data base for user and groups

        int userMembers = Integer.parseInt(args[3]);

        ApplicationContext applicationContext = ApplicationContextHelper.getApplicationContext();
        LDAPInitialDirContextFactory factory = (LDAPInitialDirContextFactory) applicationContext
                .getBean("ldapInitialDirContextFactory");

        InitialDirContext ctx = null;
        try {
            ctx = factory.getInitialDirContext("cn=" + args[0] + "," + args[2], args[1]);

            /* Values we'll use in creating the entry */
            Attribute objClasses = new BasicAttribute("objectclass");
            objClasses.add("top");
            objClasses.add("person");
            objClasses.add("organizationalPerson");
            objClasses.add("inetOrgPerson");

            for (int i = 0; i < userMembers; i++) {

                Attribute cn = new BasicAttribute("cn", "User" + i + " TestUser");
                Attribute sn = new BasicAttribute("sn", "TestUser");
                Attribute givenNames = new BasicAttribute("givenName", "User" + i);
                Attribute telephoneNumber = new BasicAttribute("telephoneNumber", "123");
                Attribute uid = new BasicAttribute("uid", "User" + i);
                Attribute mail = new BasicAttribute("mail", "woof@woof");
                Attribute o = new BasicAttribute("o", "Alfresco");
                Attribute userPassword = new BasicAttribute("userPassword", "bobbins");
                /* Specify the DN we're adding */
                String dn = "cn=User" + i + " TestUser," + args[2];

                Attributes orig = new BasicAttributes();
                orig.put(objClasses);
                orig.put(cn);
                orig.put(sn);
                orig.put(givenNames);
                orig.put(telephoneNumber);
                orig.put(uid);
                orig.put(mail);
                orig.put(o);
                orig.put(userPassword);

                try {
                    ctx.destroySubcontext(dn);
                } catch (NamingException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

                ctx.createSubcontext(dn, orig);
            }

        } catch (NamingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            if (ctx != null) {
                try {
                    ctx.close();
                } catch (NamingException e) {

                    e.printStackTrace();
                }
            }
        }

    }

    public void afterPropertiesSet() throws Exception {
        logger.debug("after Properties Set");
        // Check Anonymous bind

        Hashtable<String, String> env = new Hashtable<String, String>(authenticatedEnvironment.size());
        env.putAll(authenticatedEnvironment);
        env.remove(Context.SECURITY_PRINCIPAL);
        env.remove(Context.SECURITY_CREDENTIALS);
        if (isSSLSocketFactoryRequired()) {
            KeyStore trustStore = initTrustStore();
            AlfrescoSSLSocketFactory.initTrustedSSLSocketFactory(trustStore);
            env.put("java.naming.ldap.factory.socket", AlfrescoSSLSocketFactory.class.getName());
        }
        try {
            new InitialDirContext(env);

            logger.warn("LDAP server supports anonymous bind " + env.get(Context.PROVIDER_URL));
        } catch (javax.naming.AuthenticationException ax) {

        } catch (AuthenticationNotSupportedException e) {

        } catch (NamingException nx) {
            logger.error("Unable to connect to LDAP Server; check LDAP configuration", nx);
            return;
        }

        // Simple DN and password

        env = new Hashtable<String, String>(authenticatedEnvironment.size());
        env.putAll(authenticatedEnvironment);
        env.put(Context.SECURITY_PRINCIPAL, "daftAsABrush");
        env.put(Context.SECURITY_CREDENTIALS, "daftAsABrush");
        if (isSSLSocketFactoryRequired()) {
            KeyStore trustStore = initTrustStore();
            AlfrescoSSLSocketFactory.initTrustedSSLSocketFactory(trustStore);
            env.put("java.naming.ldap.factory.socket", AlfrescoSSLSocketFactory.class.getName());
        }
        try {

            new InitialDirContext(env);

            throw new AuthenticationException("The ldap server at " + env.get(Context.PROVIDER_URL)
                    + " falls back to use anonymous bind if invalid security credentials are presented. This is not supported.");
        } catch (javax.naming.AuthenticationException ax) {
            logger.info("LDAP server does not fall back to anonymous bind for a string uid and password at "
                    + env.get(Context.PROVIDER_URL));
        } catch (AuthenticationNotSupportedException e) {
            logger.info("LDAP server does not fall back to anonymous bind for a string uid and password at "
                    + env.get(Context.PROVIDER_URL));
        } catch (NamingException nx) {
            logger.info("LDAP server does not support simple string user ids and invalid credentials at "
                    + env.get(Context.PROVIDER_URL));
        }

        // DN and password

        env = new Hashtable<String, String>(authenticatedEnvironment.size());
        env.putAll(authenticatedEnvironment);
        env.put(Context.SECURITY_PRINCIPAL, "cn=daftAsABrush,dc=woof");
        env.put(Context.SECURITY_CREDENTIALS, "daftAsABrush");
        if (isSSLSocketFactoryRequired()) {
            KeyStore trustStore = initTrustStore();
            AlfrescoSSLSocketFactory.initTrustedSSLSocketFactory(trustStore);
            env.put("java.naming.ldap.factory.socket", AlfrescoSSLSocketFactory.class.getName());
        }
        try {

            new InitialDirContext(env);

            throw new AuthenticationException("The ldap server at " + env.get(Context.PROVIDER_URL)
                    + " falls back to use anonymous bind if invalid security credentials are presented. This is not supported.");
        } catch (javax.naming.AuthenticationException ax) {
            logger.info("LDAP server does not fall back to anonymous bind for a simple dn and password at "
                    + env.get(Context.PROVIDER_URL));
        } catch (AuthenticationNotSupportedException e) {
            logger.info("LDAP server does not fall back to anonymous bind for a simple dn and password at "
                    + env.get(Context.PROVIDER_URL));
        } catch (NamingException nx) {
            logger.info("LDAP server does not support simple DN and invalid password at "
                    + env.get(Context.PROVIDER_URL));
        }

        // Check more if we have a real principal we expect to work

        String principal = defaultEnvironment.get(Context.SECURITY_PRINCIPAL);
        if (principal != null) {
            // Correct principal invalid password

            env = new Hashtable<String, String>(authenticatedEnvironment.size());
            env.putAll(authenticatedEnvironment);
            env.put(Context.SECURITY_PRINCIPAL, principal);
            env.put(Context.SECURITY_CREDENTIALS, "sdasdasdasdasd123123123");
            if (isSSLSocketFactoryRequired()) {
                KeyStore trustStore = initTrustStore();
                AlfrescoSSLSocketFactory.initTrustedSSLSocketFactory(trustStore);
                env.put("java.naming.ldap.factory.socket", AlfrescoSSLSocketFactory.class.getName());
            }
            if (!checkedEnvs.contains(env)) {

                try {

                    new InitialDirContext(env);

                    throw new AuthenticationException("The ldap server at " + env.get(Context.PROVIDER_URL)
                            + " falls back to use anonymous bind for a known principal if  invalid security credentials are presented. This is not supported.");
                } catch (javax.naming.AuthenticationException ax) {
                    logger.info(
                            "LDAP server does not fall back to anonymous bind for known principal and invalid credentials at "
                                    + env.get(Context.PROVIDER_URL));
                } catch (AuthenticationNotSupportedException e) {
                    logger.info("LDAP server does not support the required authentication mechanism");
                } catch (NamingException nx) {
                    // already done
                }
                // Record this environment as checked so that we don't check it again on further restarts / other subsystem
                // instances
                checkedEnvs.add(env);
            }
        }
    }

    /**
     * Check if it required to use custom SSL socket factory with custom trustStore.
     * <br>Required for LDAPS configuration. The <code>ldap.authentication.java.naming.security.protocol</code> should be set to "ssl" for LDAPS.
     * <br>The following properties should be set:
     * <ul>
     * <li>ldap.authentication.truststore.path
     * <li>ldap.authentication.truststore.type
     * <li>ldap.authentication.truststore.passphrase
     * <li>ldap.authentication.java.naming.security.protocol
     * </ul>
     *
     * @return <code>true</code> if all the required properties are set
     */
    private boolean isSSLSocketFactoryRequired() {
        boolean result = false;
        // Check for LDAPS config
        String protocol = authenticatedEnvironment.get(Context.SECURITY_PROTOCOL);
        if (protocol != null && protocol.equals("ssl")) {
            if (getTrustStoreType() != null && getTrustStorePath() != null && getTrustStoreType() != null) {
                result = true;
            } else {
                logger.warn("The SSL configuration for LDAPS is not full, the default configuration will be used.");
            }
        }
        return result;
    }

    /**
     * Initialize trustStore with Spring set properties:
     * <ul>
     * <li>ldap.authentication.truststore.path
     * <li>ldap.authentication.truststore.type
     * <li>ldap.authentication.truststore.passphrase
     * </ul>
     *
     * @return {@link KeyStore} with loaded trustStore file
     */
    private KeyStore initTrustStore() {
        KeyStore ks;
        String trustStoreType = getTrustStoreType();
        try {
            ks = KeyStore.getInstance(trustStoreType);
        } catch (KeyStoreException kse) {
            throw new AlfrescoRuntimeException("No provider supports " + trustStoreType, kse);
        }
        try {
            ks.load(new FileInputStream(getTrustStorePath()), getTrustStorePassPhrase().toCharArray());
        } catch (FileNotFoundException fnfe) {
            throw new AlfrescoRuntimeException("The truststore file is not found.", fnfe);
        } catch (IOException ioe) {
            throw new AlfrescoRuntimeException("The truststore file cannot be read.", ioe);
        } catch (NoSuchAlgorithmException nsae) {
            throw new AlfrescoRuntimeException(
                    "Algorithm used to check the integrity of the truststore cannot be found.", nsae);
        } catch (CertificateException ce) {
            throw new AlfrescoRuntimeException("The certificates cannot be loaded from truststore.", ce);
        }
        return ks;
    }
}