Pull in new LDAP mapping connector

git-svn-id: https://svn.apache.org/repos/asf/manifoldcf/trunk@1900034 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/connectors/ldapmapper/build.xml b/connectors/ldapmapper/build.xml
new file mode 100644
index 0000000..b387e10
--- /dev/null
+++ b/connectors/ldapmapper/build.xml
@@ -0,0 +1,40 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements.  See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You 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.
+-->
+
+<project name="ldapmapper" default="all">
+
+    <property environment="env"/>
+    <condition property="mcf-dist" value="${env.MCFDISTPATH}">
+        <isset property="env.MCFDISTPATH"/>
+    </condition>
+    <property name="abs-dist" location="../../dist"/>
+    <condition property="mcf-dist" value="${abs-dist}">
+        <not>
+            <isset property="env.MCFDISTPATH"/>
+        </not>
+    </condition>
+
+    <import file="${mcf-dist}/connector-build.xml"/>
+
+    <target name="deliver-connector" depends="mcf-connector-build.deliver-connector">
+        <antcall target="general-add-mapping-connector">
+            <param name="connector-label" value="Regular expression mapper"/>
+            <param name="connector-class" value="org.apache.manifoldcf.authorities.mappers.ldap.LDAPMapper"/>
+        </antcall>
+    </target>
+    
+</project>
diff --git a/connectors/ldapmapper/connector/src/main/java/org/apache/manifoldcf/authorities/mappers/ldap/LDAPMapper.java b/connectors/ldapmapper/connector/src/main/java/org/apache/manifoldcf/authorities/mappers/ldap/LDAPMapper.java
new file mode 100755
index 0000000..09f50ec
--- /dev/null
+++ b/connectors/ldapmapper/connector/src/main/java/org/apache/manifoldcf/authorities/mappers/ldap/LDAPMapper.java
@@ -0,0 +1,716 @@
+/* $Id$ */
+
+/**
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You 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.
+*/
+package org.apache.manifoldcf.authorities.mappers.ldap;
+
+import org.apache.manifoldcf.core.interfaces.*;
+import org.apache.manifoldcf.agents.interfaces.*;
+import org.apache.manifoldcf.authorities.interfaces.*;
+import org.apache.manifoldcf.authorities.system.Logging;
+import org.apache.manifoldcf.authorities.system.ManifoldCF;
+import org.apache.manifoldcf.connectorcommon.interfaces.*;
+import javax.naming.*;
+import javax.naming.directory.*;
+import javax.naming.ldap.*;
+import org.apache.manifoldcf.ui.util.Encoder;
+import org.apache.manifoldcf.core.common.LDAPSSLSocketFactory;
+
+
+import java.io.*;
+import java.util.*;
+
+/** This is the ldap mapper implementation, which uses a ldap association to manipulate a user name.
+*/
+public class LDAPMapper extends org.apache.manifoldcf.authorities.mappers.BaseMappingConnector
+{
+  public static final String _rcsid = "@(#)$Id$";
+
+  // Match map for username
+  private LdapContext session = null;
+  private StartTlsResponse tls = null;
+  
+  private long sessionExpirationTime = -1L;
+
+  //private ConfigParams parameters;
+
+  private String bindUser;
+  private String bindPass;
+  private String serverProtocol;
+  private String serverName;
+  private String serverPort;
+  private String serverBase;
+  private String userBase;
+  private String userSearch;
+  private String userNameReplace;
+  private List<String> forcedTokens;
+  private String userNameAttr;
+  private String sslKeystoreData;
+  
+  private IKeystoreManager sslKeystore;
+  
+  private long responseLifetime = 60000L; //60sec
+
+  private int LRUsize = 1000;
+
+
+  /** Constructor.
+  */
+  public LDAPMapper()
+  {
+  }
+/**
+   * Disconnect a session.
+   */
+  protected void disconnectSession() {
+    if (session != null) {
+      try {
+        if (tls != null)
+          tls.close();
+        session.close();
+      } catch (NamingException e) {
+        // Eat this error
+      } catch (IOException e) {
+        // Eat this error
+      }
+      tls = null;
+      session = null;
+      sessionExpirationTime = -1L;
+    }
+  }
+  /** Close the connection.  Call this before discarding the mapping connection.
+  */
+  @Override
+  public void disconnect()
+    throws ManifoldCFException {
+    disconnectSession();
+    super.disconnect();
+    // Zero out all the stuff that we want to be sure we don't use again
+    serverName = null;
+    serverPort = null;
+    serverBase = null;
+    userBase = null;
+    userSearch = null;
+    userNameAttr = null;
+    userNameReplace = null;
+    forcedTokens = null;
+    sslKeystoreData = null;
+    sslKeystore = null;
+  }
+   /**
+   * Retrieves LDAPProtocol from serverProtocol String
+   *
+   * @return LDAPProtocolEnum
+   */
+  private LDAPProtocolEnum retrieveLDAPProtocol() {
+    if (serverProtocol == null || serverProtocol.length() == 0) {
+      return  LDAPProtocolEnum.LDAP;
+    }
+
+    final LDAPProtocolEnum ldapProtocol;
+    switch (serverProtocol.toUpperCase(Locale.ENGLISH)){
+      case "LDAP":
+        ldapProtocol = LDAPProtocolEnum.LDAP;
+        break;
+      case "LDAPS":
+        ldapProtocol = LDAPProtocolEnum.LDAPS;
+        break;
+      case "LDAP+TLS":
+        ldapProtocol = LDAPProtocolEnum.LDAP_TLS;
+        break;
+      case "LDAPS+TLS":
+        ldapProtocol = LDAPProtocolEnum.LDAPS_TLS;
+        break;
+      default:
+        ldapProtocol = LDAPProtocolEnum.LDAP;
+    }
+    return ldapProtocol;
+  }
+  /**
+   * Checks whether TLS is enabled for given LDAP Protocol
+   *
+   * @param ldapProtocol to check
+   * @return whether TLS is enabled or not
+   */
+  private boolean isLDAPTLS (LDAPProtocolEnum ldapProtocol){
+    return LDAPProtocolEnum.LDAP_TLS.equals(ldapProtocol) || LDAPProtocolEnum.LDAPS_TLS.equals(ldapProtocol);
+  }
+
+  /**
+   * Checks whether LDAPS or LDAPS with TLS is enabled for given LDAP Protocol
+   *
+   * @param ldapProtocol to check
+   * @return whether LDAPS or LDAPS with TLS is enabled or not
+   */
+  private boolean isLDAPS (LDAPProtocolEnum ldapProtocol){
+    return LDAPProtocolEnum.LDAPS.equals(ldapProtocol) || LDAPProtocolEnum.LDAPS_TLS.equals(ldapProtocol);
+  }
+/**
+   * Stringifies LDAP Context environment variable
+   * @param env LDAP Context environment variable
+   * @return Stringified LDAP Context environment. Password is masked if set.
+   */
+  private String printLdapContextEnvironment(Hashtable env) {
+    Hashtable copyEnv = new Hashtable<>(env);
+    if (copyEnv.containsKey(Context.SECURITY_CREDENTIALS)){
+      copyEnv.put(Context.SECURITY_CREDENTIALS, "********");
+    }
+    return Arrays.toString(copyEnv.entrySet().toArray());
+  }
+/**
+   * Connect. The configuration parameters are included.
+   *
+   * @param configParams are the configuration parameters for this connection.
+   */
+  @Override
+  public void connect(ConfigParams configParams) {
+    super.connect(configParams);
+    //parameters = configParams;
+
+    // Credentials
+    bindUser = configParams.getParameter("ldapBindUser");
+    bindPass = configParams.getObfuscatedParameter("ldapBindPass");
+
+    // We get the parameters here, so we can check them in case they are missing
+    serverProtocol = configParams.getParameter("ldapProtocol");
+    serverName = configParams.getParameter("ldapServerName");
+    serverPort = configParams.getParameter("ldapServerPort");
+    serverBase = configParams.getParameter("ldapServerBase");
+
+    sslKeystoreData = configParams.getParameter("sslKeystore");
+    
+    userBase = configParams.getParameter("ldapUserBase");
+    userSearch = configParams.getParameter("ldapUserSearch");
+  
+    userNameAttr = configParams.getParameter("ldapUserNameAttr");
+    userNameReplace = configParams.getParameter("ldapUserNameReplace");
+
+    forcedTokens = new ArrayList<String>();
+    int i = 0;
+    while (i < configParams.getChildCount()) {
+      ConfigNode sn = configParams.getChild(i++);
+      if (sn.getType().equals("access")) {
+        String token = "" + sn.getAttributeValue("token");
+        forcedTokens.add(token);
+      }
+    }
+  }
+   // All methods below this line will ONLY be called if a connect() call succeeded
+  // on this instance!
+  /**
+   * Session setup. Anything that might need to throw an exception should go
+   * here.
+   */
+  protected LdapContext getSession()
+    throws ManifoldCFException {
+
+    try {
+      LDAPProtocolEnum ldapProtocol = retrieveLDAPProtocol();
+      if (session == null) {
+        if (serverName == null || serverName.length() == 0) {
+          Logging.mappingConnectors.error("Server name parameter missing but required");
+          throw new ManifoldCFException("Server name parameter missing but required");
+        }
+        if (serverPort == null || serverPort.length() == 0) {
+          Logging.mappingConnectors.error("Server port parameter missing but required");
+          throw new ManifoldCFException("Server port parameter missing but required");
+        }
+        if (serverBase == null) {
+          Logging.mappingConnectors.error("Server base parameter missing but required");
+          throw new ManifoldCFException("Server base parameter missing but required");
+        }
+        if (userBase == null) {
+          Logging.mappingConnectors.error("User base parameter missing but required");
+          throw new ManifoldCFException("User base parameter missing but required");
+        }
+        if (userSearch == null || userSearch.length() == 0) {
+          Logging.mappingConnectors.error("User search expression missing but required");
+          throw new ManifoldCFException("User search expression missing but required");
+        }
+        if (userNameReplace == null || userNameReplace.length() == 0) {
+          Logging.mappingConnectors.error("User name replace attribute missing but required");
+          throw new ManifoldCFException("User name replace attribute missing but required");
+        }
+        if (userNameAttr == null || userNameAttr.length() == 0) {
+          Logging.mappingConnectors.error("User name attribute missing but required");
+          throw new ManifoldCFException("User name attribute missing but required");
+        }
+
+        if (sslKeystoreData != null) {
+          sslKeystore = KeystoreManagerFactory.make("", sslKeystoreData);
+        } else {
+          sslKeystore = KeystoreManagerFactory.make("");
+        }
+
+        final Hashtable env = new Hashtable();
+        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
+        env.put(Context.PROVIDER_URL, "ldap://" + serverName + ":" + serverPort + "/" + serverBase);
+        if (LDAPProtocolEnum.LDAPS.equals(ldapProtocol)) {
+          // Set thread local for keystore stuff
+          LDAPSSLSocketFactory.setSocketFactoryProducer(sslKeystore);
+          env.put(Context.SECURITY_PROTOCOL, "ssl");
+          env.put("java.naming.ldap.factory.socket", "org.apache.manifoldcf.core.common.LDAPSSLSocketFactory");
+        }
+        
+        if (bindUser != null && !bindUser.isEmpty()) {
+          env.put(Context.SECURITY_AUTHENTICATION, "simple");
+          env.put(Context.SECURITY_PRINCIPAL, bindUser);
+          env.put(Context.SECURITY_CREDENTIALS, bindPass);
+        }
+
+        Logging.mappingConnectors.info("LDAP Context environment properties: " + printLdapContextEnvironment(env));
+        session = new InitialLdapContext(env, null);
+        
+        if (isLDAPTLS(ldapProtocol)) {
+          // Start TLS
+          StartTlsResponse tls = (StartTlsResponse) session.extendedOperation(new StartTlsRequest());
+          tls.negotiate(sslKeystore.getSecureSocketFactory());
+        }
+        
+      } else {
+        if (isLDAPS(ldapProtocol)) {
+          // Set thread local for keystore stuff
+          LDAPSSLSocketFactory.setSocketFactoryProducer(sslKeystore);
+        }
+        session.reconnect(null);
+      }
+      sessionExpirationTime = System.currentTimeMillis() + 300000L;
+      return session;
+    } catch (AuthenticationException e) {
+      session = null;
+      sessionExpirationTime = -1L;
+      Logging.mappingConnectors.error("Authentication error: " + e.getMessage() + ", explanation: " + e.getExplanation(), e);
+      throw new ManifoldCFException("Authentication error: " + e.getMessage() + ", explanation: " + e.getExplanation(), e);
+    } catch (CommunicationException e) {
+      session = null;
+      sessionExpirationTime = -1L;
+      Logging.mappingConnectors.error("Communication error: " + e.getMessage(), e);
+      throw new ManifoldCFException("Communication error: " + e.getMessage(), e);
+    } catch (NamingException e) {
+      session = null;
+      sessionExpirationTime = -1L;
+      Logging.mappingConnectors.error("Naming exception: " + e.getMessage(), e);
+      throw new ManifoldCFException("Naming exception: " + e.getMessage(), e);
+    } catch (InterruptedIOException e) {
+      session = null;
+      sessionExpirationTime = -1L;
+      Logging.mappingConnectors.error("Interrupted IO error: " + e.getMessage());
+      throw new ManifoldCFException(e.getMessage(), ManifoldCFException.INTERRUPTED);
+    } catch (IOException e) {
+      session = null;
+      sessionExpirationTime = -1L;
+      Logging.mappingConnectors.error("IO error: " + e.getMessage(), e);
+      throw new ManifoldCFException("IO error: " + e.getMessage(), e);
+    }
+  }  
+ 
+  /**
+   * Obtain the user LDAP record for a given user logon name.
+   *
+   * @param ctx is the ldap context to use.
+   * @param userName (Domain Logon Name) is the user name or identifier.
+   * DC=qa-ad-76,DC=metacarta,DC=com)
+   * @return SearchResult for given domain user logon name. (Should throws an
+   * exception if user is not found.)
+   */
+  protected SearchResult getUserEntry(LdapContext ctx, String userName)
+    throws ManifoldCFException {
+    String searchFilter = userSearch.replaceAll("\\{0\\}", escapeDN(userName));
+    SearchControls searchCtls = new SearchControls();
+    searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
+    String[] attrIds = {userNameAttr, userNameReplace};
+    searchCtls.setReturningAttributes(attrIds);
+
+    try {
+      NamingEnumeration answer = ctx.search(userBase, searchFilter, searchCtls);
+      if (answer.hasMoreElements()) {
+        return (SearchResult) answer.next();
+      }
+      return null;
+    } catch (Exception e) {
+      throw new ManifoldCFException(e.getMessage(), e);
+    }
+  }
+  public static String escapeDN(String name) {
+    StringBuilder sb = new StringBuilder(); // If using JDK >= 1.5 consider using StringBuilder
+    if ((name.length() > 0) && ((name.charAt(0) == ' ') || (name.charAt(0) == '#'))) {
+      sb.append('\\'); // add the leading backslash if needed
+    }
+    for (int i = 0; i < name.length(); i++) {
+      char curChar = name.charAt(i);
+      switch (curChar) {
+        case '\\':
+          sb.append("\\\\");
+          break;
+        case ',':
+          sb.append("\\,");
+          break;
+        case '+':
+          sb.append("\\+");
+          break;
+        case '"':
+          sb.append("\\\"");
+          break;
+        case '<':
+          sb.append("\\<");
+          break;
+        case '>':
+          sb.append("\\>");
+          break;
+        case ';':
+          sb.append("\\;");
+          break;
+        default:
+          sb.append(curChar);
+      }
+    }
+    if ((name.length() > 1) && (name.charAt(name.length() - 1) == ' ')) {
+      sb.insert(sb.length() - 1, '\\'); // add the trailing backslash if needed
+    }
+    return sb.toString();
+  }
+
+  /** Map an input user name to an output name.
+  *@param userName is the name to map
+  *@return the mapped user name
+  */
+  @Override
+  public String mapUser(String userName)
+    throws ManifoldCFException
+  {
+    LdapContext lcontext = getSession();
+
+    try {
+      String searchFilter = userSearch.replaceAll("\\{0\\}", escapeDN(userName));
+      Logging.mappingConnectors.info("SearFilter="+searchFilter);
+
+      SearchResult answer = getUserEntry(lcontext, userName);
+      Logging.mappingConnectors.info("Found answer="+answer.toString());
+      if (Logging.mappingConnectors.isDebugEnabled())
+        Logging.mappingConnectors.debug("Found answer="+answer.toString());
+    
+      Attributes attrs = answer.getAttributes();
+      Attribute attr = attrs.get(userNameReplace);
+      
+      String outputUserName = (String)attr.get(0);
+      Logging.mappingConnectors.info("LDAPMapper: Input user name '"+userName+"'; output user name '"+outputUserName+"'");
+      
+      if (Logging.mappingConnectors.isDebugEnabled())
+        Logging.mappingConnectors.debug("LDAPMapper: Input user name '"+userName+"'; output user name '"+outputUserName+"'");
+      
+      return outputUserName;
+    }
+    catch(Exception e)
+    { 
+      throw new ManifoldCFException("replace error: " + e.getMessage(), e);
+      }
+  }
+
+  // UI support methods.
+  //
+  // These support methods are involved in setting up authority connection configuration information. The configuration methods cannot assume that the
+  // current authority object is connected.  That is why they receive a thread context argument.
+    
+  /** Output the configuration header section.
+  * This method is called in the head section of the connector's configuration page.  Its purpose is to add the required tabs to the list, and to output any
+  * javascript methods that might be needed by the configuration editing HTML.
+  * @param threadContext is the local thread context.
+  * @param out is the output to which any HTML should be sent.
+  * @param parameters are the configuration parameters, as they currently
+  * exist, for this connection being configured.
+  * @param tabsArray is an array of tab names. Add to this array any tab names
+  * that are specific to the connector.
+  */
+   @Override
+   public void outputConfigurationHeader(IThreadContext threadContext, IHTTPOutput out, Locale locale, ConfigParams parameters, List<String> tabsArray)
+     throws ManifoldCFException, IOException {
+     tabsArray.add(Messages.getString(locale, "LDAP.LDAPMapperTab"));
+     Logging.mappingConnectors.info("LDAP.LDAPMapperTab="+Messages.getString(locale, "LDAP.LDAPMapperTab"));
+     tabsArray.add(Messages.getString(locale, "LDAP.ForcedTokens"));
+     final Map<String,Object> paramMap = new HashMap<String,Object>();
+     fillInLDAPTab(paramMap, out, parameters);
+     fillInForcedTokensTab(paramMap, out, parameters);
+     Messages.outputResourceWithVelocity(out, locale, "editConfiguration.js", paramMap);    
+   }
+  private static String getParam(final ConfigParams parameters, final String name, final String def) {
+    String rval = parameters.getParameter(name);
+    return rval != null ? rval : def;
+  }
+
+  /** Fill in ForcedTokens tab */
+  protected static void fillInForcedTokensTab(Map<String,Object> velocityContext, IHTTPOutput out, ConfigParams parameters)
+  {
+    final List<String> forcedTokenList = new ArrayList<String>();
+    for (int i = 0; i < parameters.getChildCount(); i++) {
+      final ConfigNode sn = parameters.getChild(i);
+      if (sn.getType().equals("access")) {
+        forcedTokenList.add(sn.getAttributeValue("token"));
+      }
+    }
+    velocityContext.put("FORCEDTOKENS", forcedTokenList);
+  }
+  /** Fill in LDAP tab */
+  protected static void fillInLDAPTab(Map<String,Object> velocityContext, IHTTPOutput out, ConfigParams parameters)
+  {
+    velocityContext.put("FSERVERPROTOCOL", getParam(parameters, "ldapProtocol", "ldap"));
+    velocityContext.put("FSERVERNAME", getParam(parameters, "ldapServerName", ""));
+    velocityContext.put("FSERVERPORT", getParam(parameters, "ldapServerPort", "389"));
+    velocityContext.put("FSERVERBASE", getParam(parameters, "ldapServerBase", ""));
+    String sslKeystoreData = parameters.getParameter("sslkeystore");
+    if (sslKeystoreData != null)
+      velocityContext.put("SSLKEYSTOREDATA", sslKeystoreData);
+    velocityContext.put("FUSERBASE", getParam(parameters, "ldapUserBase", "ou=People"));
+    velocityContext.put("FUSERSEARCH", getParam(parameters, "ldapUserSearch", "(&(objectClass=inetOrgPerson)(mail={0}))"));
+    velocityContext.put("FUSERNAMEATTR", getParam(parameters, "ldapUserNameAttr", "mail"));
+    velocityContext.put("FUSERNAMEREPLACE", getParam(parameters, "ldapUserNameReplace", "cn"));
+    
+    velocityContext.put("FBINDUSER", getParam(parameters, "ldapBindUser", ""));
+    String fBindPass = parameters.getObfuscatedParameter("ldapBindPass");
+    if (fBindPass == null)
+      fBindPass = "";
+    else
+      fBindPass = out.mapPasswordToKey(fBindPass);
+    velocityContext.put("FBINDPASS", fBindPass);
+    
+    Map<String,String> sslCertificatesMap = null;
+    String message = null;
+
+    try {
+      final IKeystoreManager localSslKeystore;
+      if (sslKeystoreData == null)
+        localSslKeystore = KeystoreManagerFactory.make("");
+      else
+        localSslKeystore = KeystoreManagerFactory.make("",sslKeystoreData);
+
+      // List the individual certificates in the store, with a delete button for each
+      final String[] contents = localSslKeystore.getContents();
+      if (contents.length > 0)
+      {
+        sslCertificatesMap = new HashMap<>();
+        for (final String alias : contents)
+        {
+          String description = localSslKeystore.getDescription(alias);
+          if (description.length() > 128)
+            description = description.substring(0,125) + "...";
+          sslCertificatesMap.put(alias, description);
+        }
+      }
+    } catch (ManifoldCFException e) {
+      message = e.getMessage();
+      Logging.mappingConnectors.warn(e);
+    }
+
+    if(sslCertificatesMap != null)
+      velocityContext.put("SSLCERTIFICATESMAP", sslCertificatesMap);
+    if(message != null)
+      velocityContext.put("MESSAGE", message);
+  }
+  /**
+   * Output the configuration body section. This method is called in the body
+   * section of the authority connector's configuration page. Its purpose is to
+   * present the required form elements for editing. The coder can presume that
+   * the HTML that is output from this configuration will be within appropriate
+   * &lt;html&gt;, &lt;body&gt;, and &lt;form&gt; tags. The name of the form is "editconnection".
+   *
+   * @param threadContext is the local thread context.
+   * @param out is the output to which any HTML should be sent.
+   * @param parameters are the configuration parameters, as they currently
+   * exist, for this connection being configured.
+   * @param tabName is the current tab name.
+   */
+  @Override
+  public void outputConfigurationBody(IThreadContext threadContext, IHTTPOutput out, Locale locale, ConfigParams parameters, String tabName)
+    throws ManifoldCFException, IOException {
+    final Map<String,Object> paramMap = new HashMap<String,Object>();
+    paramMap.put("TabName",tabName);
+    fillInLDAPTab(paramMap, out, parameters);
+    fillInForcedTokensTab(paramMap, out, parameters);
+    Messages.outputResourceWithVelocity(out, locale, "editConfiguration_LDAP.html", paramMap);    
+    Messages.outputResourceWithVelocity(out, locale, "editConfiguration_ForcedTokens.html", paramMap);    
+  
+    
+
+  }
+  private boolean copyParam(IPostParameters variableContext, ConfigParams parameters, String name) {
+    String val = variableContext.getParameter(name);
+    if (val == null) {
+      return false;
+    }
+    parameters.setParameter(name, val);
+    return true;
+  }
+  
+  /**
+   * Process a configuration post. This method is called at the start of the
+   * authority connector's configuration page, whenever there is a possibility
+   * that form data for a connection has been posted. Its purpose is to gather
+   * form information and modify the configuration parameters accordingly. The
+   * name of the posted form is "editconnection".
+   *
+   * @param threadContext is the local thread context.
+   * @param variableContext is the set of variables available from the post,
+   * including binary file post information.
+   * @param parameters are the configuration parameters, as they currently
+   * exist, for this connection being configured.
+   * @return null if all is well, or a string error message if there is an error
+   * that should prevent saving of the connection (and cause a redirection to an
+   * error page).
+   */
+  @Override
+  public String processConfigurationPost(IThreadContext threadContext, IPostParameters variableContext, Locale locale, ConfigParams parameters)
+    throws ManifoldCFException {
+    copyParam(variableContext, parameters, "ldapProtocol");
+    copyParam(variableContext, parameters, "ldapServerName");
+    copyParam(variableContext, parameters, "ldapServerPort");
+    copyParam(variableContext, parameters, "ldapServerBase");
+    copyParam(variableContext, parameters, "ldapUserBase");
+    copyParam(variableContext, parameters, "ldapUserSearch");
+    copyParam(variableContext, parameters, "ldapUserNameAttr");
+    copyParam(variableContext, parameters, "ldapUserNameReplace");
+
+    copyParam(variableContext, parameters, "ldapBindUser");
+    final String bindPass = variableContext.getParameter("ldapBindPass");
+    if (bindPass != null) {
+      parameters.setObfuscatedParameter("ldapBindPass", variableContext.mapKeyToPassword(bindPass));
+    }
+
+    final String xc = variableContext.getParameter("tokencount");
+    if (xc != null) {
+      // Delete all tokens first
+      int i = 0;
+      while (i < parameters.getChildCount()) {
+        ConfigNode sn = parameters.getChild(i);
+        if (sn.getType().equals("access")) {
+          parameters.removeChild(i);
+        } else {
+          i++;
+        }
+      }
+
+      final int accessCount = Integer.parseInt(xc);
+      i = 0;
+      while (i < accessCount) {
+        final String accessDescription = "_" + Integer.toString(i);
+        final String accessOpName = "accessop" + accessDescription;
+        final String command = variableContext.getParameter(accessOpName);
+        if (command != null && command.equals("Delete")) {
+          // Next row
+          i++;
+          continue;
+        }
+        // Get the stuff we need
+        String accessSpec = variableContext.getParameter("spectoken" + accessDescription);
+        ConfigNode node = new ConfigNode("access");
+        node.setAttribute("token", accessSpec);
+        parameters.addChild(parameters.getChildCount(), node);
+        i++;
+      }
+
+      String op = variableContext.getParameter("accessop");
+      if (op != null && op.equals("Add")) {
+        String accessspec = variableContext.getParameter("spectoken");
+        ConfigNode node = new ConfigNode("access");
+        node.setAttribute("token", accessspec);
+        parameters.addChild(parameters.getChildCount(), node);
+      }
+    }
+
+    String sslKeystoreValue = variableContext.getParameter("sslkeystoredata");
+    final String sslConfigOp = variableContext.getParameter("sslconfigop");
+    if (sslConfigOp != null)
+    {
+      if (sslConfigOp.equals("Delete"))
+      {
+        final String alias = variableContext.getParameter("sslkeystorealias");
+        final IKeystoreManager mgr;
+        if (sslKeystoreValue != null)
+          mgr = KeystoreManagerFactory.make("",sslKeystoreValue);
+        else
+          mgr = KeystoreManagerFactory.make("");
+        mgr.remove(alias);
+        sslKeystoreValue = mgr.getString();
+      }
+      else if (sslConfigOp.equals("Add"))
+      {
+        String alias = IDFactory.make(threadContext);
+        byte[] certificateValue = variableContext.getBinaryBytes("sslcertificate");
+        final IKeystoreManager mgr;
+        if (sslKeystoreValue != null)
+          mgr = KeystoreManagerFactory.make("",sslKeystoreValue);
+        else
+          mgr = KeystoreManagerFactory.make("");
+        java.io.InputStream is = new java.io.ByteArrayInputStream(certificateValue);
+        String certError = null;
+        try
+        {
+          mgr.importCertificate(alias,is);
+        }
+        catch (Throwable e)
+        {
+          certError = e.getMessage();
+        }
+        finally
+        {
+          try
+          {
+            is.close();
+          }
+          catch (IOException e)
+          {
+            // Eat this exception
+          }
+        }
+
+        if (certError != null)
+        {
+          return "Illegal certificate: "+certError;
+        }
+        sslKeystoreValue = mgr.getString();
+      }
+    }
+    if (sslKeystoreValue != null)
+      parameters.setParameter("sslkeystore",sslKeystoreValue);
+    
+    return null;
+  }
+  
+  /**
+   * View configuration. This method is called in the body section of the
+   * authority connector's view configuration page. Its purpose is to present
+   * the connection information to the user. The coder can presume that the HTML
+   * that is output from this configuration will be within appropriate &lt;html&gt;
+   * and &lt;body&gt; tags.
+   *
+   * @param threadContext is the local thread context.
+   * @param out is the output to which any HTML should be sent.
+   * @param parameters are the configuration parameters, as they currently
+   * exist, for this connection being configured.
+   */
+  @Override
+  public void viewConfiguration(IThreadContext threadContext, IHTTPOutput out, Locale locale, ConfigParams parameters)
+    throws ManifoldCFException, IOException {
+    final Map<String,Object> paramMap = new HashMap<String,Object>();
+    fillInLDAPTab(paramMap, out, parameters);
+    fillInForcedTokensTab(paramMap, out, parameters);
+    Messages.outputResourceWithVelocity(out, locale, "viewConfiguration.html", paramMap);    
+  }
+
+  
+
+}
+
+
diff --git a/connectors/ldapmapper/connector/src/main/java/org/apache/manifoldcf/authorities/mappers/ldap/LDAPProtocolEnum.java b/connectors/ldapmapper/connector/src/main/java/org/apache/manifoldcf/authorities/mappers/ldap/LDAPProtocolEnum.java
new file mode 100755
index 0000000..8629d48
--- /dev/null
+++ b/connectors/ldapmapper/connector/src/main/java/org/apache/manifoldcf/authorities/mappers/ldap/LDAPProtocolEnum.java
@@ -0,0 +1,8 @@
+package org.apache.manifoldcf.authorities.mappers.ldap;
+
+enum LDAPProtocolEnum {
+    LDAP,
+    LDAPS,
+    LDAP_TLS,
+    LDAPS_TLS
+}
\ No newline at end of file
diff --git a/connectors/ldapmapper/connector/src/main/java/org/apache/manifoldcf/authorities/mappers/ldap/Messages.java b/connectors/ldapmapper/connector/src/main/java/org/apache/manifoldcf/authorities/mappers/ldap/Messages.java
new file mode 100755
index 0000000..8973939
--- /dev/null
+++ b/connectors/ldapmapper/connector/src/main/java/org/apache/manifoldcf/authorities/mappers/ldap/Messages.java
@@ -0,0 +1,141 @@
+/* $Id$ */
+
+/**
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You 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.
+*/
+package org.apache.manifoldcf.authorities.mappers.ldap;
+
+import java.util.Locale;
+import java.util.Map;
+import org.apache.manifoldcf.core.interfaces.ManifoldCFException;
+import org.apache.manifoldcf.core.interfaces.IHTTPOutput;
+
+public class Messages extends org.apache.manifoldcf.ui.i18n.Messages
+{
+  public static final String DEFAULT_BUNDLE_NAME="org.apache.manifoldcf.authorities.mappers.ldap.common";
+  public static final String DEFAULT_PATH_NAME="org.apache.manifoldcf.authorities.mappers.ldap";
+  
+  /** Constructor - do no instantiate
+  */
+  protected Messages()
+  {
+  }
+  
+  public static String getString(Locale locale, String messageKey)
+  {
+    return getString(DEFAULT_BUNDLE_NAME, locale, messageKey, null);
+  }
+
+  public static String getAttributeString(Locale locale, String messageKey)
+  {
+    return getAttributeString(DEFAULT_BUNDLE_NAME, locale, messageKey, null);
+  }
+
+  public static String getBodyString(Locale locale, String messageKey)
+  {
+    return getBodyString(DEFAULT_BUNDLE_NAME, locale, messageKey, null);
+  }
+
+  public static String getAttributeJavascriptString(Locale locale, String messageKey)
+  {
+    return getAttributeJavascriptString(DEFAULT_BUNDLE_NAME, locale, messageKey, null);
+  }
+
+  public static String getBodyJavascriptString(Locale locale, String messageKey)
+  {
+    return getBodyJavascriptString(DEFAULT_BUNDLE_NAME, locale, messageKey, null);
+  }
+
+  public static String getString(Locale locale, String messageKey, Object[] args)
+  {
+    return getString(DEFAULT_BUNDLE_NAME, locale, messageKey, args);
+  }
+
+  public static String getAttributeString(Locale locale, String messageKey, Object[] args)
+  {
+    return getAttributeString(DEFAULT_BUNDLE_NAME, locale, messageKey, args);
+  }
+  
+  public static String getBodyString(Locale locale, String messageKey, Object[] args)
+  {
+    return getBodyString(DEFAULT_BUNDLE_NAME, locale, messageKey, args);
+  }
+
+  public static String getAttributeJavascriptString(Locale locale, String messageKey, Object[] args)
+  {
+    return getAttributeJavascriptString(DEFAULT_BUNDLE_NAME, locale, messageKey, args);
+  }
+
+  public static String getBodyJavascriptString(Locale locale, String messageKey, Object[] args)
+  {
+    return getBodyJavascriptString(DEFAULT_BUNDLE_NAME, locale, messageKey, args);
+  }
+
+  // More general methods which allow bundlenames and class loaders to be specified.
+  
+  public static String getString(String bundleName, Locale locale, String messageKey, Object[] args)
+  {
+    return getString(Messages.class, bundleName, locale, messageKey, args);
+  }
+
+  public static String getAttributeString(String bundleName, Locale locale, String messageKey, Object[] args)
+  {
+    return getAttributeString(Messages.class, bundleName, locale, messageKey, args);
+  }
+
+  public static String getBodyString(String bundleName, Locale locale, String messageKey, Object[] args)
+  {
+    return getBodyString(Messages.class, bundleName, locale, messageKey, args);
+  }
+  
+  public static String getAttributeJavascriptString(String bundleName, Locale locale, String messageKey, Object[] args)
+  {
+    return getAttributeJavascriptString(Messages.class, bundleName, locale, messageKey, args);
+  }
+
+  public static String getBodyJavascriptString(String bundleName, Locale locale, String messageKey, Object[] args)
+  {
+    return getBodyJavascriptString(Messages.class, bundleName, locale, messageKey, args);
+  }
+
+  // Resource output
+  
+  public static void outputResource(IHTTPOutput output, Locale locale, String resourceKey,
+    Map<String,String> substitutionParameters, boolean mapToUpperCase)
+    throws ManifoldCFException
+  {
+    outputResource(output,Messages.class,DEFAULT_PATH_NAME,locale,resourceKey,
+      substitutionParameters,mapToUpperCase);
+  }
+  
+  public static void outputResourceWithVelocity(IHTTPOutput output, Locale locale, String resourceKey,
+    Map<String,String> substitutionParameters, boolean mapToUpperCase)
+    throws ManifoldCFException
+  {
+    outputResourceWithVelocity(output,Messages.class,DEFAULT_BUNDLE_NAME,DEFAULT_PATH_NAME,locale,resourceKey,
+      substitutionParameters,mapToUpperCase);
+  }
+
+  public static void outputResourceWithVelocity(IHTTPOutput output, Locale locale, String resourceKey,
+    Map<String,Object> contextObjects)
+    throws ManifoldCFException
+  {
+    outputResourceWithVelocity(output,Messages.class,DEFAULT_BUNDLE_NAME,DEFAULT_PATH_NAME,locale,resourceKey,
+      contextObjects);
+  }
+  
+}
+
diff --git a/connectors/ldapmapper/connector/src/main/native2ascii/org/apache/manifoldcf/authorities/mappers/ldap/common_en_US.properties b/connectors/ldapmapper/connector/src/main/native2ascii/org/apache/manifoldcf/authorities/mappers/ldap/common_en_US.properties
new file mode 100755
index 0000000..44178bd
--- /dev/null
+++ b/connectors/ldapmapper/connector/src/main/native2ascii/org/apache/manifoldcf/authorities/mappers/ldap/common_en_US.properties
@@ -0,0 +1,64 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+LDAP.LDAPMapperTab=LDAP Mapping
+
+LDAP.LDAP=LDAP
+LDAP.LDAPServerNameColon=LDAP server name:
+LDAP.LDAPServerPortColon=LDAP server port:
+LDAP.LDAPServerBaseColon=LDAP server base (e.g. 'dc=office,dc=com'):
+LDAP.LDAPBindUserColon=Bind to server as user (leave empty if not needed):
+LDAP.LDAPBindPasswordColon=Bind to server with password:
+LDAP.UserSearchBaseColon=User search base:
+LDAP.UserSearchFilterColon=User search filter:
+
+LDAP.GroupSearchBaseColon=Group search base:
+LDAP.GroupSearchFilterColon=Group search filter:
+LDAP.GroupNameAttributeColon=Group name attribute:
+LDAP.AddUserAuthColon=Add user as authorization token:
+LDAP.UserNameAttrColon=User name attribute:
+LDAP.UserNameReplaceColon=User replace attribute:
+LDAP.GroupMemberDnColon=Member attribute is DN:
+
+LDAP.ForcedTokens=Forced tokens
+LDAP.ForcedTokensColon=Forced tokens:
+LDAP.Add=Add
+LDAP.Delete=Delete
+LDAP.AddToken=Add token
+LDAP.TypeInToken=Token cannot be empty
+LDAP.NoTokensSpecified=No tokens specified
+LDAP.NoTokensPresent=No tokens specified
+LDAP.ForcedTokensDisclaimer=Forced tokens are meant to enrich results with common tokens explicitly handled by authorization center, like "Everyone". Use with extreme attention as this mechanism can grant privileges to every user outside authorization directory!
+
+LDAP.ServerNameCannotBeBlank=Server name cannot be blank
+LDAP.ServerPortCannotBeBlank=Server port cannot be blank
+LDAP.UserSearchCannotBeBlank=User search expression cannot be blank
+LDAP.GroupSearchCannotBeBlank=Group search expression cannot be blank
+LDAP.GroupNameAttrCannotBeBlank=Group name attribute cannot be blank
+LDAP.UserSearchMustIncludeSubstitution=User search must include user substitution ({0})
+LDAP.GroupSearchMustIncludeSubstitution=Group search must include user substitution ({0})
+LDAP.ServerPortMustBeAnInteger=Server port must be an integer
+LDAP.ServerNameCannotIncludeSlash=Server name cannot include "/" character
+LDAP.ServerBaseCannotIncludeSlash=Server base cannot include "/" character
+
+LDAP.Yes=Yes
+LDAP.No=No
+LDAP.NoCertificatesPresent=No certificates present
+LDAP.SSLCertificateList=SSL certificate list:
+LDAP.AddCert=Add certificate
+LDAP.Add=Add
+LDAP.Certificate=Certificate:
+LDAP.ChooseACertificateFile=Choose a certificate file
+LDAP.LDAPProtocolColon=LDAP protocol:
diff --git a/connectors/ldapmapper/connector/src/main/native2ascii/org/apache/manifoldcf/authorities/mappers/ldap/common_es_ES.properties b/connectors/ldapmapper/connector/src/main/native2ascii/org/apache/manifoldcf/authorities/mappers/ldap/common_es_ES.properties
new file mode 100644
index 0000000..1e8fc73
--- /dev/null
+++ b/connectors/ldapmapper/connector/src/main/native2ascii/org/apache/manifoldcf/authorities/mappers/ldap/common_es_ES.properties
@@ -0,0 +1,64 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+LDAP.LDAPMapperTab=LDAP Mapping
+
+LDAP.LDAP=LDAP
+LDAP.LDAPServerNameColon=LDAP server name:
+LDAP.LDAPServerPortColon=LDAP server port:
+LDAP.LDAPServerBaseColon=LDAP server base (e.g. 'dc=office,dc=com'):
+LDAP.LDAPBindUserColon=Bind to server as user (leave empty if not needed):
+LDAP.LDAPBindPasswordColon=Bind to server with password:
+LDAP.UserSearchBaseColon=User search base:
+LDAP.UserSearchFilterColon=User search filter:
+
+LDAP.GroupSearchBaseColon=Group search base:
+LDAP.GroupSearchFilterColon=Group search filter:
+LDAP.GroupNameAttributeColon=Group name attribute:
+LDAP.AddUserAuthColon=Add user as authorization token:
+LDAP.UserNameAttrColon=User name attribute:
+LDAP.UserNameReplaceColon=User replace attribute:
+LDAP.GroupMemberDnColon=Member attribute is DN:
+
+LDAP.ForcedTokens=Forced tokens
+LDAP.ForcedTokensColon=Forced tokens:
+LDAP.Add=Add
+LDAP.Delete=Delete
+LDAP.AddToken=Add token
+LDAP.TypeInToken=Token cannot be empty
+LDAP.NoTokensSpecified=No tokens specified
+LDAP.NoTokensPresent=No tokens specified
+LDAP.ForcedTokensDisclaimer=Forced tokens are meant to enrich results with common tokens explicitly handled by authorization center, like "Everyone". Use with extreme attention as this mechanism can grant privileges to every user outside authorization directory!
+
+LDAP.ServerNameCannotBeBlank=Server name cannot be blank
+LDAP.ServerPortCannotBeBlank=Server port cannot be blank
+LDAP.UserSearchCannotBeBlank=User search expression cannot be blank
+LDAP.GroupSearchCannotBeBlank=Group search expression cannot be blank
+LDAP.GroupNameAttrCannotBeBlank=Group name attribute cannot be blank
+LDAP.UserSearchMustIncludeSubstitution=User search must include user substitution ({0})
+LDAP.GroupSearchMustIncludeSubstitution=Group search must include user substitution ({0})
+LDAP.ServerPortMustBeAnInteger=Server port must be an integer
+LDAP.ServerNameCannotIncludeSlash=Server name cannot include "/" character
+LDAP.ServerBaseCannotIncludeSlash=Server base cannot include "/" character
+
+LDAP.Yes=Yes
+LDAP.No=No
+LDAP.NoCertificatesPresent=No certificates present
+LDAP.SSLCertificateList=SSL certificate list:
+LDAP.AddCert=Add certificate
+LDAP.Add=Add
+LDAP.Certificate=Certificate:
+LDAP.ChooseACertificateFile=Choose a certificate file
+LDAP.LDAPProtocolColon=LDAP protocol:
\ No newline at end of file
diff --git a/connectors/ldapmapper/connector/src/main/native2ascii/org/apache/manifoldcf/authorities/mappers/ldap/common_fr_FR.properties b/connectors/ldapmapper/connector/src/main/native2ascii/org/apache/manifoldcf/authorities/mappers/ldap/common_fr_FR.properties
new file mode 100755
index 0000000..f034280
--- /dev/null
+++ b/connectors/ldapmapper/connector/src/main/native2ascii/org/apache/manifoldcf/authorities/mappers/ldap/common_fr_FR.properties
@@ -0,0 +1,52 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+LDAP.LDAPMapperTab=LDAP Mapping
+LDAP.LDAP=LDAP
+LDAP.LDAPServerNameColon=Nom du serveur LDAP:
+LDAP.LDAPServerPortColon=Port du serveur LDAP:
+LDAP.LDAPServerBaseColon=Base du serveur (server base) LDAP (par ex. 'dc=office,dc=com'):
+LDAP.LDAPBindUserColon=Binding au serveur (bind to server) en tant qu'utilisateur (laisser vide si pas nécessaire):
+LDAP.LDAPBindPasswordColon=Binding au serveur (bind to server) avec le mot de passe:
+LDAP.UserSearchBaseColon=Base pour les recherches d'utilisateurs (User search base):
+LDAP.UserSearchFilterColon=Filtre pour les recherches d'utilisateur:
+LDAP.GroupSearchBaseColon=Base pour la recherche de groupe (Group search base):
+LDAP.GroupSearchFilterColon=Filtre pour la recherche de groupe:
+LDAP.GroupNameAttributeColon=Attribut nom de groupe:
+LDAP.AddUserAuthColon=Ajouter utilisateur comme jeton d'autorisation:
+LDAP.UserNameAttrColon=Attribut nom d'utilisateur:
+LDAP.UserNameReplaceColon=Attribut de remplacement pour l'utilisateur:
+LDAP.GroupMemberDnColon=L'attribut membre (member attribute) est DN:
+
+LDAP.ForcedTokens=Jetons forcés
+LDAP.ForcedTokensColon=Jetons forcés:
+LDAP.Add=Ajouter
+LDAP.Delete=Supprimer
+LDAP.AddToken=Ajouter jeton
+LDAP.TypeInToken=Jeton ne peut pas être vide
+LDAP.NoTokensSpecified=Aucun jeton spécifié
+LDAP.NoTokensPresent=Pas de jeton présent
+LDAP.ForcedTokensDisclaimer=Les jetons forcés servent à enrichir les résultats avec des jetons communs explicitement gérés par le centre d'autorisation, comme "Everyone". A n'utiliser qu'avec une extrême précaution car ce mécanisme peut donner des privilèges à tous les utilisateurs hors de l'authorization directory!
+
+LDAP.ServerNameCannotBeBlank=Le nom de serveur ne peut être vide
+LDAP.ServerPortCannotBeBlank=Le port du serveur ne peut être vide
+LDAP.UserSearchCannotBeBlank=L'expression de recherche d'utilisateur ne peut être vide
+LDAP.GroupSearchCannotBeBlank=L'expression de recherche de groupe ne peut être vide
+LDAP.GroupNameAttrCannotBeBlank=L'attribut nom de groupe ne peut être vide
+LDAP.UserSearchMustIncludeSubstitution=La recherche d'utilisateur doit inclure un user substitution ({0})
+LDAP.GroupSearchMustIncludeSubstitution=La recherche de groupe doit inclure un user substitution ({0})
+LDAP.ServerPortMustBeAnInteger=Le port du serveur doit être un entier
+LDAP.ServerNameCannotIncludeSlash=Le nom du serveur ne peut pas inclure le caractère "/"
+LDAP.ServerBaseCannotIncludeSlash=La base du serveur (server base) ne peut pas inclure le caractère "/"
diff --git a/connectors/ldapmapper/connector/src/main/native2ascii/org/apache/manifoldcf/authorities/mappers/ldap/common_ja_JP.properties b/connectors/ldapmapper/connector/src/main/native2ascii/org/apache/manifoldcf/authorities/mappers/ldap/common_ja_JP.properties
new file mode 100644
index 0000000..1e8fc73
--- /dev/null
+++ b/connectors/ldapmapper/connector/src/main/native2ascii/org/apache/manifoldcf/authorities/mappers/ldap/common_ja_JP.properties
@@ -0,0 +1,64 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+LDAP.LDAPMapperTab=LDAP Mapping
+
+LDAP.LDAP=LDAP
+LDAP.LDAPServerNameColon=LDAP server name:
+LDAP.LDAPServerPortColon=LDAP server port:
+LDAP.LDAPServerBaseColon=LDAP server base (e.g. 'dc=office,dc=com'):
+LDAP.LDAPBindUserColon=Bind to server as user (leave empty if not needed):
+LDAP.LDAPBindPasswordColon=Bind to server with password:
+LDAP.UserSearchBaseColon=User search base:
+LDAP.UserSearchFilterColon=User search filter:
+
+LDAP.GroupSearchBaseColon=Group search base:
+LDAP.GroupSearchFilterColon=Group search filter:
+LDAP.GroupNameAttributeColon=Group name attribute:
+LDAP.AddUserAuthColon=Add user as authorization token:
+LDAP.UserNameAttrColon=User name attribute:
+LDAP.UserNameReplaceColon=User replace attribute:
+LDAP.GroupMemberDnColon=Member attribute is DN:
+
+LDAP.ForcedTokens=Forced tokens
+LDAP.ForcedTokensColon=Forced tokens:
+LDAP.Add=Add
+LDAP.Delete=Delete
+LDAP.AddToken=Add token
+LDAP.TypeInToken=Token cannot be empty
+LDAP.NoTokensSpecified=No tokens specified
+LDAP.NoTokensPresent=No tokens specified
+LDAP.ForcedTokensDisclaimer=Forced tokens are meant to enrich results with common tokens explicitly handled by authorization center, like "Everyone". Use with extreme attention as this mechanism can grant privileges to every user outside authorization directory!
+
+LDAP.ServerNameCannotBeBlank=Server name cannot be blank
+LDAP.ServerPortCannotBeBlank=Server port cannot be blank
+LDAP.UserSearchCannotBeBlank=User search expression cannot be blank
+LDAP.GroupSearchCannotBeBlank=Group search expression cannot be blank
+LDAP.GroupNameAttrCannotBeBlank=Group name attribute cannot be blank
+LDAP.UserSearchMustIncludeSubstitution=User search must include user substitution ({0})
+LDAP.GroupSearchMustIncludeSubstitution=Group search must include user substitution ({0})
+LDAP.ServerPortMustBeAnInteger=Server port must be an integer
+LDAP.ServerNameCannotIncludeSlash=Server name cannot include "/" character
+LDAP.ServerBaseCannotIncludeSlash=Server base cannot include "/" character
+
+LDAP.Yes=Yes
+LDAP.No=No
+LDAP.NoCertificatesPresent=No certificates present
+LDAP.SSLCertificateList=SSL certificate list:
+LDAP.AddCert=Add certificate
+LDAP.Add=Add
+LDAP.Certificate=Certificate:
+LDAP.ChooseACertificateFile=Choose a certificate file
+LDAP.LDAPProtocolColon=LDAP protocol:
\ No newline at end of file
diff --git a/connectors/ldapmapper/connector/src/main/native2ascii/org/apache/manifoldcf/authorities/mappers/ldap/common_zh_CN.properties b/connectors/ldapmapper/connector/src/main/native2ascii/org/apache/manifoldcf/authorities/mappers/ldap/common_zh_CN.properties
new file mode 100644
index 0000000..1e8fc73
--- /dev/null
+++ b/connectors/ldapmapper/connector/src/main/native2ascii/org/apache/manifoldcf/authorities/mappers/ldap/common_zh_CN.properties
@@ -0,0 +1,64 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+LDAP.LDAPMapperTab=LDAP Mapping
+
+LDAP.LDAP=LDAP
+LDAP.LDAPServerNameColon=LDAP server name:
+LDAP.LDAPServerPortColon=LDAP server port:
+LDAP.LDAPServerBaseColon=LDAP server base (e.g. 'dc=office,dc=com'):
+LDAP.LDAPBindUserColon=Bind to server as user (leave empty if not needed):
+LDAP.LDAPBindPasswordColon=Bind to server with password:
+LDAP.UserSearchBaseColon=User search base:
+LDAP.UserSearchFilterColon=User search filter:
+
+LDAP.GroupSearchBaseColon=Group search base:
+LDAP.GroupSearchFilterColon=Group search filter:
+LDAP.GroupNameAttributeColon=Group name attribute:
+LDAP.AddUserAuthColon=Add user as authorization token:
+LDAP.UserNameAttrColon=User name attribute:
+LDAP.UserNameReplaceColon=User replace attribute:
+LDAP.GroupMemberDnColon=Member attribute is DN:
+
+LDAP.ForcedTokens=Forced tokens
+LDAP.ForcedTokensColon=Forced tokens:
+LDAP.Add=Add
+LDAP.Delete=Delete
+LDAP.AddToken=Add token
+LDAP.TypeInToken=Token cannot be empty
+LDAP.NoTokensSpecified=No tokens specified
+LDAP.NoTokensPresent=No tokens specified
+LDAP.ForcedTokensDisclaimer=Forced tokens are meant to enrich results with common tokens explicitly handled by authorization center, like "Everyone". Use with extreme attention as this mechanism can grant privileges to every user outside authorization directory!
+
+LDAP.ServerNameCannotBeBlank=Server name cannot be blank
+LDAP.ServerPortCannotBeBlank=Server port cannot be blank
+LDAP.UserSearchCannotBeBlank=User search expression cannot be blank
+LDAP.GroupSearchCannotBeBlank=Group search expression cannot be blank
+LDAP.GroupNameAttrCannotBeBlank=Group name attribute cannot be blank
+LDAP.UserSearchMustIncludeSubstitution=User search must include user substitution ({0})
+LDAP.GroupSearchMustIncludeSubstitution=Group search must include user substitution ({0})
+LDAP.ServerPortMustBeAnInteger=Server port must be an integer
+LDAP.ServerNameCannotIncludeSlash=Server name cannot include "/" character
+LDAP.ServerBaseCannotIncludeSlash=Server base cannot include "/" character
+
+LDAP.Yes=Yes
+LDAP.No=No
+LDAP.NoCertificatesPresent=No certificates present
+LDAP.SSLCertificateList=SSL certificate list:
+LDAP.AddCert=Add certificate
+LDAP.Add=Add
+LDAP.Certificate=Certificate:
+LDAP.ChooseACertificateFile=Choose a certificate file
+LDAP.LDAPProtocolColon=LDAP protocol:
\ No newline at end of file
diff --git a/connectors/ldapmapper/connector/src/main/resources/org/apache/manifoldcf/authorities/mappers/ldap/editConfiguration.js b/connectors/ldapmapper/connector/src/main/resources/org/apache/manifoldcf/authorities/mappers/ldap/editConfiguration.js
new file mode 100755
index 0000000..13726ad
--- /dev/null
+++ b/connectors/ldapmapper/connector/src/main/resources/org/apache/manifoldcf/authorities/mappers/ldap/editConfiguration.js
@@ -0,0 +1,127 @@
+<!DOCTYPE html>
+<!--
+Copyright 2014 The Apache Software Foundation.
+
+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.
+-->
+<script type="text/javascript">
+    <!--
+    function SSLDeleteCertificate(aliasName)
+    {
+      editconnection.sslkeystorealias.value = aliasName;
+      editconnection.sslconfigop.value = "Delete";
+      postForm();
+    }
+
+    function SSLAddCertificate()
+    {
+      if (editconnection.sslcertificate.value == "")
+      {
+        alert("$Encoder.bodyJavascriptEscape($ResourceBundle.getString('LDAP.ChooseACertificateFile'))");
+        editconnection.sslcertificate.focus();
+      }
+      else
+      {
+        editconnection.sslconfigop.value = "Add";
+        postForm();
+      }
+    }
+
+    function checkConfig() {
+      if (editconnection.ldapServerName.value.indexOf("/") != -1) {
+        alert("$Encoder.bodyJavascriptEscape($ResourceBundle.getString('LDAP.ServerNameCannotIncludeSlash'))");
+        editconnection.ldapServerName.focus();
+        return false;
+      }
+      if (editconnection.ldapServerPort.value != "" && !isInteger(editconnection.ldapServerPort.value)) {
+        alert("$Encoder.bodyJavascriptEscape($ResourceBundle.getString('LDAP.ServerPortMustBeAnInteger'))");
+        editconnection.ldapServerPort.focus();
+        return false;
+      }
+      if (editconnection.ldapServerBase.value.indexOf("/") != -1) {
+        alert("$Encoder.bodyJavascriptEscape($ResourceBundle.getString('LDAP.ServerBaseCannotIncludeSlash'))");
+        editconnection.ldapServerBase.focus();
+        return false;
+      }
+      if (editconnection.ldapUserSearch.value != "" && editconnection.ldapUserSearch.value.indexOf("{0}") == -1) {
+        alert("$Encoder.bodyJavascriptEscape($ResourceBundle.getString('LDAP.UserSearchMustIncludeSubstitution'))");
+        editconnection.ldapUserSearch.focus();
+        return false;
+      }
+      
+      return true;
+    }
+
+    function checkConfigForSave() {
+      if (editconnection.ldapServerName.value == "") {
+        alert("$Encoder.bodyJavascriptEscape($ResourceBundle.getString('LDAP.ServerNameCannotBeBlank'))");
+        SelectTab("$Encoder.bodyJavascriptEscape($ResourceBundle.getString('LDAP.LDAPMapperTab'))");
+        editconnection.ldapServerName.focus();
+        return false;
+      }
+      if (editconnection.ldapServerPort.value == "") {
+        alert("$Encoder.bodyJavascriptEscape($ResourceBundle.getString('LDAP.ServerPortCannotBeBlank'))");
+        SelectTab("$Encoder.bodyJavascriptEscape($ResourceBundle.getString('LDAP.LDAPMapperTab'))");
+        editconnection.ldapServerPort.focus();
+        return false;
+      }
+      if (editconnection.ldapUserSearch.value == "") {
+        alert("$Encoder.bodyJavascriptEscape($ResourceBundle.getString('LDAP.UserSearchCannotBeBlank'))");
+        SelectTab("$Encoder.bodyJavascriptEscape($ResourceBundle.getString('LDAP.LDAPMapperTab'))");
+        editconnection.ldapUserSearch.focus();
+        return false;
+      }
+      
+      if (editconnection.ldapUserSearch.value != "" && editconnection.ldapUserSearch.value.indexOf("{0}") == -1) {
+        alert("$Encoder.bodyJavascriptEscape($ResourceBundle.getString('LDAP.UserSearchMustIncludeSubstitution'))");
+        SelectTab("$Encoder.bodyJavascriptEscape($ResourceBundle.getString('LDAP.LDAPMapperTab'))");
+        editconnection.ldapUserSearch.focus();
+        return false;
+      }
+      
+      if (editconnection.ldapServerPort.value != "" && !isInteger(editconnection.ldapServerPort.value)) {
+        alert("$Encoder.bodyJavascriptEscape($ResourceBundle.getString('LDAP.ServerPortMustBeAnInteger'))");
+        SelectTab("$Encoder.bodyJavascriptEscape($ResourceBundle.getString('LDAP.LDAPMapperTab'))");
+        editconnection.ldapServerPort.focus();
+        return false;
+      }
+      if (editconnection.ldapServerName.value.indexOf("/") != -1) {
+        alert("$Encoder.bodyJavascriptEscape($ResourceBundle.getString('LDAP.ServerNameCannotIncludeSlash'))");
+        SelectTab("$Encoder.bodyJavascriptEscape($ResourceBundle.getString('LDAP.LDAPMapperTab'))");
+        editconnection.ldapServerName.focus();
+        return false;
+      }
+      if (editconnection.ldapServerBase.value.indexOf("/") != -1) {
+        alert("$Encoder.bodyJavascriptEscape($ResourceBundle.getString('LDAP.ServerBaseCannotIncludeSlash'))");
+        editconnection.ldapServerBase.focus();
+        return false;
+      }
+      return true;
+    }
+    
+    function SpecOp(n, opValue, anchorvalue) {
+      eval("editconnection."+n+".value = \""+opValue+"\"");
+      postFormSetAnchor(anchorvalue);
+    }
+    
+    function SpecAddToken(anchorvalue) {
+      if (editconnection.spectoken.value == "")
+      {
+        alert("$Encoder.bodyJavascriptEscape($ResourceBundle.getString('LDAP.TypeInToken'))");
+        editconnection.spectoken.focus();
+        return;
+      }
+      SpecOp("accessop","Add",anchorvalue);
+    }
+    //-->
+</script>
diff --git a/connectors/ldapmapper/connector/src/main/resources/org/apache/manifoldcf/authorities/mappers/ldap/editConfiguration_ForcedTokens.html b/connectors/ldapmapper/connector/src/main/resources/org/apache/manifoldcf/authorities/mappers/ldap/editConfiguration_ForcedTokens.html
new file mode 100755
index 0000000..c846409
--- /dev/null
+++ b/connectors/ldapmapper/connector/src/main/resources/org/apache/manifoldcf/authorities/mappers/ldap/editConfiguration_ForcedTokens.html
@@ -0,0 +1,68 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements.  See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You 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.
+-->
+
+#if($TabName == $ResourceBundle.getString('LDAP.ForcedTokens'))
+
+<table class="displaytable">
+  <tr><td class="separator" colspan="2"><hr/></td></tr>
+  <tr><td class="value" colspan="2">$Encoder.bodyEscape($ResourceBundle.getString('LDAP.ForcedTokensDisclaimer'))</td></tr>
+  <tr><td class="separator" colspan="2"><hr/></td></tr>
+  #set($counter = 0)
+  #foreach($forcedtoken in $FORCEDTOKENS)
+  <tr>
+    <td class="description">
+      <input type="hidden" name="accessop_$counter" value=""/>
+      <input type="hidden" name="spectoken_$counter" value="$Encoder.attributeEscape($forcedtoken)"/>
+      <a name="token_$counter">
+        <input type="button" value="$Encoder.attributeEscape($ResourceBundle.getString('LDAP.Delete'))" onClick='Javascript:SpecOp("accessop_$counter","Delete","token_$counter")' alt="$Encoder.attributeEscape($ResourceBundle.getString('LDAP.DeleteToken'))$counter"/>
+      </a>
+    </td>
+    <td class="value">
+      $Encoder.bodyEscape($forcedtoken)
+    </td>
+  </tr>
+    #set($counter = $counter + 1)
+  #end
+  #if ($counter == 0)
+  <tr>
+    <td class="message" colspan="2">$Encoder.bodyEscape($ResourceBundle.getString('LDAP.NoTokensPresent'))</td>
+  </tr>
+  #end
+  <tr><td class="lightseparator" colspan="2"><hr/></td></tr>
+  <tr>
+    <td class="description">
+      <input type="hidden" name="tokencount" value="$counter"/>
+  #set($counterplusone = $counter + 1)
+      <input type="hidden" name="accessop" value=""/>
+      <a name="token_$counter">
+        <input type="button" value="$Encoder.attributeEscape($ResourceBundle.getString('LDAP.Add'))" onClick='Javascript:SpecAddToken("token_$counterplusone")' alt="$Encoder.attributeEscape($ResourceBundle.getString('LDAP.AddToken'))"/>
+      </a>
+    </td>
+    <td class="value">
+      <input type="text" size="30" name="spectoken" value=""/>
+    </td>
+  </tr>
+</table>
+#else
+  #set($counter = 0)
+  #foreach($forcedtoken in $FORCEDTOKENS)
+<input type="hidden" name="spectoken_$counter" value="$Encoder.attributeEscape($forcedtoken)"/>
+    #set($counter = $counter + 1)
+  #end
+<input type="hidden" name="tokencount" value="$counter"/>
+#end
+
diff --git a/connectors/ldapmapper/connector/src/main/resources/org/apache/manifoldcf/authorities/mappers/ldap/editConfiguration_LDAP.html b/connectors/ldapmapper/connector/src/main/resources/org/apache/manifoldcf/authorities/mappers/ldap/editConfiguration_LDAP.html
new file mode 100755
index 0000000..28b8079
--- /dev/null
+++ b/connectors/ldapmapper/connector/src/main/resources/org/apache/manifoldcf/authorities/mappers/ldap/editConfiguration_LDAP.html
@@ -0,0 +1,141 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements.  See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You 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.
+-->
+
+<input name="sslconfigop" type="hidden" value="Continue"/>
+#if($SSLKEYSTOREDATA)
+<input type="hidden" name="sslkeystoredata" value="$Encoder.attributeEscape($SSLKEYSTOREDATA)"/>
+#end
+
+#if($TabName == $ResourceBundle.getString('LDAP.LDAPMapperTab'))
+<table class="displaytable">
+  <tr><td class="separator" colspan="2"><hr/></td></tr>
+  <tr>
+    <td class="description"><nobr>$Encoder.bodyEscape($ResourceBundle.getString('LDAP.LDAPProtocolColon'))</nobr></td>
+    <td class="value">
+      <select name="ldapProtocol" size="4">
+  #if($FSERVERPROTOCOL == '' || $FSERVERPROTOCOL == 'ldap')
+        <option value="ldap" selected="true">LDAP</option>
+  #else
+        <option value="ldap">LDAP</option>
+  #end
+  #if($FSERVERPROTOCOL == 'ldaps')
+        <option value="ldaps" selected="true">LDAPS</option>
+  #else
+        <option value="ldaps">LDAPS</option>
+  #end
+  #if($FSERVERPROTOCOL == 'ldap+tls')
+        <option value="ldap+tls" selected="true">LDAP+TLS</option>
+  #else
+        <option value="ldap+tls">LDAP+TLS</option>
+  #end
+  #if($FSERVERPROTOCOL == 'ldaps+tls')
+        <option value="ldaps+tls" selected="true">LDAPS+TLS</option>
+  #else
+        <option value="ldaps+tls">LDAPS+TLS</option>
+  #end
+      </select>
+    </td>
+  </tr>
+  <tr>
+    <td class="description"><nobr>$Encoder.bodyEscape($ResourceBundle.getString('LDAP.LDAPServerNameColon'))</nobr></td>
+    <td class="value"><input type="text" size="32" name="ldapServerName" value="$Encoder.attributeEscape($FSERVERNAME)"/></td>
+  </tr>
+  <tr>
+    <td class="description"><nobr>$Encoder.bodyEscape($ResourceBundle.getString('LDAP.LDAPServerPortColon'))</nobr></td>
+    <td class="value"><input type="text" size="5" name="ldapServerPort" value="$Encoder.attributeEscape($FSERVERPORT)"/></td>
+  </tr>
+  <tr>
+    <td class="description"><nobr>$Encoder.bodyEscape($ResourceBundle.getString('LDAP.LDAPServerBaseColon'))</nobr></td>
+    <td class="value"><input type="text" size="64" name="ldapServerBase" value="$Encoder.attributeEscape($FSERVERBASE)"/></td>
+  </tr>
+  <tr>
+    <td class="description"><nobr>$Encoder.bodyEscape($ResourceBundle.getString('LDAP.LDAPBindUserColon'))</nobr></td>
+    <td class="value"><input type="text" size="64" name="ldapBindUser" value="$Encoder.attributeEscape($FBINDUSER)"/></td>
+  </tr>
+  <tr>
+    <td class="description"><nobr>$Encoder.bodyEscape($ResourceBundle.getString('LDAP.LDAPBindPasswordColon'))</nobr></td>
+    <td class="value"><input type="password" size="64" name="ldapBindPass" value="$Encoder.attributeEscape($FBINDPASS)"/></td>
+  </tr>
+  <tr><td class="separator" colspan="2"><hr/></td></tr>
+  <tr>
+    <td class="description"><nobr>$Encoder.bodyEscape($ResourceBundle.getString('LDAP.UserSearchBaseColon'))</nobr></td>
+    <td class="value"><input type="text" size="64" name="ldapUserBase" value="$Encoder.attributeEscape($FUSERBASE)"/></td>
+  </tr>
+  <tr>
+    <td class="description"><nobr>$Encoder.bodyEscape($ResourceBundle.getString('LDAP.UserSearchFilterColon'))</nobr></td>
+    <td class="value"><input type="text" size="64" name="ldapUserSearch" value="$Encoder.attributeEscape($FUSERSEARCH)"/></td>
+  </tr>
+  
+  <tr>
+    <td class="description"><nobr>$Encoder.bodyEscape($ResourceBundle.getString('LDAP.UserNameAttrColon'))</nobr></td>
+    <td class="value"><input type="text" size="64" name="ldapUserNameAttr" value="$Encoder.attributeEscape($FUSERNAMEATTR)"/></td>
+  </tr>
+  <tr>
+    <td class="description"><nobr>$Encoder.bodyEscape($ResourceBundle.getString('LDAP.UserNameReplaceColon'))</nobr></td>
+    <td class="value"><input type="text" size="64" name="ldapUserNameReplace" value="$Encoder.attributeEscape($FUSERNAMEREPLACE)"/></td>
+  </tr>
+  <tr><td class="separator" colspan="2"><hr/></td></tr>
+  <tr>
+    <td class="description">
+      <nobr>$Encoder.bodyEscape($ResourceBundle.getString('LDAP.SSLCertificateList'))</nobr>
+    </td>
+    <td class="value">
+      <input type="hidden" name="sslkeystorealias" value=""/>
+      <table class="displaytable">
+        #if($SSLCERTIFICATESMAP)
+          #foreach($cert in $SSLCERTIFICATESMAP.entrySet())
+        <tr>
+          <td class="description">
+            <input type="button" onclick='Javascript:SSLDeleteCertificate($Encoder.attributeJavascriptEscape($cert.Key))'
+                   alt="$Encoder.attributeEscape($ResourceBundle.getString('LDAP.DeleteCert'))$Encoder.attributeEscape($cert.Key)"
+                   value="$Encoder.attributeEscape($ResourceBundle.getString('LDAP.Delete'))"/>
+          </td>
+          <td class="value">
+            $Encoder.bodyEscape($cert.Value)
+          </td>
+        </tr>
+          #end
+        #else
+        <tr>
+          <td class="message" colspan="2">
+            <nobr>$ResourceBundle.getString('LDAP.NoCertificatesPresent')</nobr>
+          </td>
+        </tr>
+        #end
+      </table>
+      <input type="button" onclick='Javascript:SSLAddCertificate()' alt="$Encoder.attributeEscape($ResourceBundle.getString('LDAP.AddCert'))"
+             value="$Encoder.attributeEscape($ResourceBundle.getString('LDAP.Add'))"/>&nbsp;
+      $Encoder.bodyEscape($ResourceBundle.getString('LDAP.Certificate'))
+      <input name="sslcertificate" size="50" type="file"/>
+    </td>
+  </tr>
+
+</table>
+#else
+<input type="hidden" name="ldapProtocol" value="$Encoder.attributeEscape($FSERVERPROTOCOL)"/>
+<input type="hidden" name="ldapServerName" value="$Encoder.attributeEscape($FSERVERNAME)"/>
+<input type="hidden" name="ldapServerPort" value="$Encoder.attributeEscape($FSERVERPORT)"/>
+<input type="hidden" name="ldapServerBase" value="$Encoder.attributeEscape($FSERVERBASE)"/>
+<input type="hidden" name="ldapBindUser" value="$Encoder.attributeEscape($FBINDUSER)"/>
+<input type="hidden" name="ldapBindPass" value="$Encoder.attributeEscape($FBINDPASS)"/>
+<input type="hidden" name="ldapUserBase" value="$Encoder.attributeEscape($FUSERBASE)"/>
+<input type="hidden" name="ldapUserSearch" value="$Encoder.attributeEscape($FUSERSEARCH)"/>
+
+<input type="hidden" name="ldapUserNameAttr" value="$Encoder.attributeEscape($FUSERNAMEATTR)"/>
+<input type="hidden" name="ldapUserNameReplace" value="$Encoder.attributeEscape($FUSERNAMEREPLACE)"/>
+
+#end
\ No newline at end of file
diff --git a/connectors/ldapmapper/connector/src/main/resources/org/apache/manifoldcf/authorities/mappers/ldap/viewConfiguration.html b/connectors/ldapmapper/connector/src/main/resources/org/apache/manifoldcf/authorities/mappers/ldap/viewConfiguration.html
new file mode 100755
index 0000000..51d3268
--- /dev/null
+++ b/connectors/ldapmapper/connector/src/main/resources/org/apache/manifoldcf/authorities/mappers/ldap/viewConfiguration.html
@@ -0,0 +1,102 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements.  See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You 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.
+-->
+
+<table class="displaytable">
+  <tr><td class="separator" colspan="2"><hr/></td></tr>
+  <tr>
+    <td class="description"><nobr>$Encoder.bodyEscape($ResourceBundle.getString('LDAP.LDAPServerNameColon'))</nobr></td>
+    <td class="value">$Encoder.bodyEscape($FSERVERNAME)</td>
+  </tr>
+  <tr>
+    <td class="description"><nobr>$Encoder.bodyEscape($ResourceBundle.getString('LDAP.LDAPServerPortColon'))</nobr></td>
+    <td class="value">$Encoder.bodyEscape($FSERVERPORT)</td>
+  </tr>
+  <tr>
+    <td class="description"><nobr>$Encoder.bodyEscape($ResourceBundle.getString('LDAP.LDAPServerBaseColon'))</nobr></td>
+    <td class="value">$Encoder.bodyEscape($FSERVERBASE)</td>
+  </tr>
+  <tr>
+    <td class="description"><nobr>$Encoder.bodyEscape($ResourceBundle.getString('LDAP.LDAPBindUserColon'))</nobr></td>
+    <td class="value">$Encoder.bodyEscape($FBINDUSER)</td>
+  </tr>
+  <tr>
+    <td class="description"><nobr>$Encoder.bodyEscape($ResourceBundle.getString('LDAP.LDAPBindPasswordColon'))</nobr></td>
+    <td class="value">*******</td>
+  </tr>
+  <tr>
+    <td class="description"><nobr>$Encoder.bodyEscape($ResourceBundle.getString('LDAP.UserSearchBaseColon'))</nobr></td>
+    <td class="value">$Encoder.bodyEscape($FUSERBASE)</td>
+  </tr>
+  <tr>
+    <td class="description"><nobr>$Encoder.bodyEscape($ResourceBundle.getString('LDAP.UserSearchFilterColon'))</nobr></td>
+    <td class="value">$Encoder.bodyEscape($FUSERSEARCH)</td>
+  </tr>
+  <tr>
+    <td class="description"><nobr>$Encoder.bodyEscape($ResourceBundle.getString('LDAP.UserNameAttrColon'))</nobr></td>
+    <td class="value">$Encoder.bodyEscape($FUSERNAMEATTR)</td>
+  </tr>
+  <tr>
+    <td class="description"><nobr>$Encoder.bodyEscape($ResourceBundle.getString('LDAP.UserNameReplaceColon'))</nobr></td>
+    <td class="value">$Encoder.bodyEscape($FUSERNAMEREPLACE)</td>
+  </tr>
+  
+  <tr><td class="separator" colspan="4"><hr/></td></tr>
+
+  <tr>
+    <td class="description">
+      <nobr>$Encoder.bodyEscape($ResourceBundle.getString('LDAP.SSLCertificateList'))</nobr>
+    </td>
+    <td class="value">
+      <table class="displaytable">
+        #if($SSLCERTIFICATESMAP)
+          #foreach($cert in $SSLCERTIFICATESMAP.entrySet())
+        <tr>
+          <td class="value">
+            $Encoder.bodyEscape($cert.Value)
+          </td>
+        </tr>
+          #end
+        #else
+        <tr>
+          <td class="message" colspan="2">
+            <nobr>$ResourceBundle.getString('LDAP.NoCertificatesPresent')</nobr>
+          </td>
+        </tr>
+        #end
+      </table>
+    </td>
+  </tr>
+
+  <tr><td class="separator" colspan="4"><hr/></td></tr>
+  
+#set($seenany = false)
+#foreach($forcedtoken in $FORCEDTOKENS)
+  #if(!$seenany)
+  <tr>
+    <td class="description"><nobr>$Encoder.bodyEscape($ResourceBundle.getString('LDAP.ForcedTokensColon'))</nobr></td>
+    <td class="value">
+  #end
+  #set($seenany = true)
+        $Encoder.bodyEscape($forcedtoken)<br/>
+#end
+#if($seenany)
+    </td>
+  </tr>
+#else
+  <tr><td class="message" colspan="4"><nobr>$Encoder.bodyEscape($ResourceBundle.getString('LDAP.NoTokensSpecified'))</nobr></td></tr>
+#end
+</table>
diff --git a/connectors/ldapmapper/pom.xml b/connectors/ldapmapper/pom.xml
new file mode 100755
index 0000000..a10f239
--- /dev/null
+++ b/connectors/ldapmapper/pom.xml
@@ -0,0 +1,97 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <parent>
+    <groupId>org.apache.manifoldcf</groupId>
+    <artifactId>mcf-connectors</artifactId>
+    <version>2.23-SNAPSHOT</version>
+  </parent>
+  <modelVersion>4.0.0</modelVersion>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+  </properties>
+
+  <artifactId>mcf-ldapmapper-connector</artifactId>
+  <packaging>jar</packaging>
+  <name>ManifoldCF - Connectors - LDAP Mapper</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>mcf-core</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>mcf-connector-common</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>mcf-agents</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>mcf-pull-agent</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>mcf-ui-core</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <defaultGoal>integration-test</defaultGoal>
+    <sourceDirectory>${basedir}/connector/src/main/java</sourceDirectory>
+    <testSourceDirectory>${basedir}/connector/src/test/java</testSourceDirectory>
+    <resources>
+      <resource>
+        <directory>${basedir}/connector/src/main/native2ascii</directory>
+        <includes>
+          <include>**/*.properties</include>
+        </includes>
+      </resource>
+      <resource>
+        <directory>${basedir}/connector/src/main/resources</directory>
+        <includes>
+          <include>**/*.html</include>
+          <include>**/*.js</include>
+        </includes>
+      </resource>
+    </resources>
+    <testResources>
+      <testResource>
+        <directory>${basedir}/connector/src/test/resources</directory>
+      </testResource>
+    </testResources>
+
+    <plugins>
+
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>native2ascii-maven-plugin</artifactId>
+        <version>1.0-beta-1</version>
+        <configuration>
+          <workDir>target/classes</workDir>
+        </configuration>
+        <executions>
+          <execution>
+            <id>native2ascii-utf8</id>
+            <goals>
+              <goal>native2ascii</goal>
+            </goals>
+            <configuration>
+              <encoding>UTF8</encoding>
+              <includes>
+                <include>**/*.properties</include>
+              </includes>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
\ No newline at end of file
diff --git a/connectors/pom.xml b/connectors/pom.xml
index a77bdde..ef63d8b 100644
--- a/connectors/pom.xml
+++ b/connectors/pom.xml
@@ -58,6 +58,7 @@
     <module>googledrive</module>
     <module>jira</module>
     <module>generic</module>
+    <module>ldapmapper</module>
     <module>regexpmapper</module>
     <module>email</module>
     <module>amazoncloudsearch</module>