Repository: mina-sshd
Updated Branches:
  refs/heads/master 458c38fa5 -> c66c6d421


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-ldap/src/main/java/org/apache/sshd/common/util/net/LdapNetworkConnector.java
----------------------------------------------------------------------
diff --git 
a/sshd-ldap/src/main/java/org/apache/sshd/common/util/net/LdapNetworkConnector.java
 
b/sshd-ldap/src/main/java/org/apache/sshd/common/util/net/LdapNetworkConnector.java
new file mode 100644
index 0000000..1e4ff23
--- /dev/null
+++ 
b/sshd-ldap/src/main/java/org/apache/sshd/common/util/net/LdapNetworkConnector.java
@@ -0,0 +1,494 @@
+/*
+ * 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.sshd.common.util.net;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+
+import javax.naming.Context;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.InitialDirContext;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+
+/**
+ * Uses the <A 
HREF="http://docs.oracle.com/javase/7/docs/technotes/guides/jndi/jndi-ldap.html";>
+ * LDAP Naming Service Provider for the Java Naming and Directory Interface 
(JNDI)</A>
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+public class LdapNetworkConnector extends NetworkConnector {
+    public static final String DEFAULT_LDAP_PROTOCOL = "ldap";
+    public static final int DEFAULT_LDAP_PORT = 389;
+
+    /**
+     * Property used to override the default LDAP context factory class
+     */
+    public static final String DEFAULT_LDAP_FACTORY_PROPNAME = 
"javax.naming.ldap.factory";
+    /**
+     * Default LDAP context factory class - unless overridden via the {@link 
#DEFAULT_LDAP_FACTORY_PROPNAME} property
+     */
+    public static final String DEFAULT_LDAP_FACTORY_PROPVAL = 
"com.sun.jndi.ldap.LdapCtxFactory";
+    public static final int DEFAULT_LDAP_SEARCH_SCOPE = 
SearchControls.SUBTREE_SCOPE;
+    public static final long DEFAULT_LDAP_TIME_LIMIT = 
TimeUnit.SECONDS.toMillis(15L);
+    public static final String DEFAULT_LDAP_REFERRAL_MODE = "ignore";
+    public static final long DEFAULT_LDAP_COUNT_LIMIT = 1L;
+    public static final boolean DEFAULT_LDAP_DEREF_ENABLED = false;
+    /**
+     * A special value used to indicate that all attributes are required
+     */
+    public static final String ALL_LDAP_ATTRIBUTES = "*";
+    public static final boolean DEFAULT_LDAP_RETURN_OBJVALUE = false;
+    public static final boolean DEFAULT_LDAP_ACCUMULATE_MULTIVALUES = false;
+    public static final String DEFAULT_LDAP_BIND_DN_PATTERN = "{0}";
+    public static final String DEFAULT_LDAP_BIND_PASSWORD_PATTERN = "{1}";
+    /**
+     * A list of known binary attributes
+     * @see <A 
HREF="http://docs.oracle.com/javase/jndi/tutorial/ldap/misc/attrs.html";>LDAP 
Attributes</A>
+     */
+    public static final String DEFAULT_BINARY_ATTRIBUTES =
+             
"photo,personalSignature,audio,jpegPhoto,javaSerializedData,thumbnailPhoto,thumbnailLogo"
+           + 
",userPassword,userCertificate,cACertificate,authorityRevocationList,certificateRevocationList"
+           + ",crossCertificatePair,x500UniqueIdentifier";
+
+    protected final SearchControls searchControls = new SearchControls();
+    protected final Map<String, Object> ldapEnv = new TreeMap<String, 
Object>(String.CASE_INSENSITIVE_ORDER);
+    protected MessageFormat bindDNPattern = new 
MessageFormat(DEFAULT_LDAP_BIND_DN_PATTERN);
+    protected MessageFormat bindPasswordPattern = new 
MessageFormat(DEFAULT_LDAP_BIND_PASSWORD_PATTERN);
+    protected MessageFormat searchFilterPattern;
+    protected MessageFormat baseDNPattern;
+
+    private boolean accumulateMultiValues = 
DEFAULT_LDAP_ACCUMULATE_MULTIVALUES;
+
+    public LdapNetworkConnector() {
+        setProtocol(DEFAULT_LDAP_PROTOCOL);
+        setPort(DEFAULT_LDAP_PORT);
+        setSearchScope(DEFAULT_LDAP_SEARCH_SCOPE);
+        setLdapFactory(DEFAULT_LDAP_FACTORY_PROPVAL);
+        setTimeLimit(DEFAULT_LDAP_TIME_LIMIT);
+        setCountLimit(DEFAULT_LDAP_COUNT_LIMIT);
+        setDerefLink(DEFAULT_LDAP_DEREF_ENABLED);
+        setReturningObjFlag(DEFAULT_LDAP_RETURN_OBJVALUE);
+        setReferralMode(DEFAULT_LDAP_REFERRAL_MODE);
+        setBinaryAttributes(DEFAULT_BINARY_ATTRIBUTES);
+    }
+
+    public String getLdapFactory() {
+        return Objects.toString(ldapEnv.get(Context.INITIAL_CONTEXT_FACTORY), 
null);
+    }
+
+    /**
+     * @param factory The LDAP context factory
+     */
+    public void setLdapFactory(String factory) {
+        ldapEnv.put(Context.INITIAL_CONTEXT_FACTORY, 
ValidateUtils.checkNotNullAndNotEmpty(factory, "No LDAP factory"));
+    }
+
+    public String getBaseDN() {
+        return baseDNPattern.toPattern();
+    }
+
+    /**
+     * @param p The base DN pattern - the arguments to the pattern depend on 
the actual usage
+     * @see MessageFormat#format(String, Object...)
+     */
+    public void setBaseDN(String p) {
+        baseDNPattern = new 
MessageFormat(ValidateUtils.checkNotNullAndNotEmpty(p, "No base DN pattern"));
+    }
+
+    public String getBindDNPattern() {
+        return bindDNPattern.toPattern();
+    }
+
+    public void setBindDNPattern(String p) {
+        bindDNPattern = new 
MessageFormat(ValidateUtils.checkNotNullAndNotEmpty(p, "No bind DN pattern"));
+    }
+
+    public String getBindPasswordPattern() {
+        return bindPasswordPattern.toPattern();
+    }
+
+    public void setBindPasswordPattern(String p) {
+        bindPasswordPattern = new 
MessageFormat(ValidateUtils.checkNotNullAndNotEmpty(p, "No bind password 
pattern"));
+    }
+
+    public String getSearchFilterPattern() {
+        return searchFilterPattern.toPattern();
+    }
+
+    public void setSearchFilterPattern(String p) {
+        searchFilterPattern = new 
MessageFormat(ValidateUtils.checkNotNullAndNotEmpty(p, "No seatch filter 
pattern"));
+    }
+
+    /**
+     * @return The search scope
+     * @see SearchControls#OBJECT_SCOPE
+     * @see SearchControls#ONELEVEL_SCOPE
+     * @see SearchControls#SUBTREE_SCOPE
+     */
+    public int getSearchScope() {
+        return searchControls.getSearchScope();
+    }
+
+    /**
+     * @param scope The search scope
+     * @see SearchControls#OBJECT_SCOPE
+     * @see SearchControls#ONELEVEL_SCOPE
+     * @see SearchControls#SUBTREE_SCOPE
+     */
+    public void setSearchScope(int scope) {
+        searchControls.setSearchScope(scope);
+    }
+
+    /**
+     * @return Time limit (millis) to wait for result - zero means forever
+     */
+    public long getTimeLimit() {
+        return searchControls.getTimeLimit();
+    }
+
+    public void setTimeLimit(long limit) {
+        ValidateUtils.checkTrue(limit >= 0L, "Negative time limit: %d", limit);
+        searchControls.setTimeLimit((int) limit);
+    }
+
+    /**
+     * @return Maximum number of entries to be returned in a query
+     */
+    public long getCountLimit() {
+        return searchControls.getCountLimit();
+    }
+
+    public void setCountLimit(long count) {
+        ValidateUtils.checkTrue(count >= 0L, "Bad count limit: %d", count);
+        searchControls.setCountLimit(count);
+    }
+
+    /**
+     * @return {@code true} whether links should be de-referenced
+     * @see SearchControls#getDerefLinkFlag()
+     */
+    public boolean isDerefLink() {
+        return searchControls.getDerefLinkFlag();
+    }
+
+    public void setDerefLink(boolean enabled) {
+        searchControls.setDerefLinkFlag(enabled);
+    }
+
+    /**
+     * @return Comma separated list of attributes to retrieve
+     */
+    public String getRetrievedAttributes() {
+        String[] attrs = searchControls.getReturningAttributes();
+        if (attrs == null) {
+            return "*";
+        } else if (attrs.length == 0) {
+            return "";
+        } else if (attrs.length == 1) {
+            return attrs[0];
+        } else {
+            return GenericUtils.join(attrs, ',');
+        }
+    }
+
+    /**
+     * @param attrs Comma separated list of attributes to retrieve - if
+     * {@code null}/empty then no attributes are retrieved
+     * @see SearchControls#setReturningAttributes(String[])
+     */
+    public void setRetrievedAttributes(String attrs) {
+        if (GenericUtils.isEmpty(attrs)) {
+            
searchControls.setReturningAttributes(GenericUtils.EMPTY_STRING_ARRAY);
+        } else if (ALL_LDAP_ATTRIBUTES.equals(attrs)) {
+            searchControls.setReturningAttributes(null);
+        } else {
+            searchControls.setReturningAttributes(GenericUtils.split(attrs, 
','));
+        }
+    }
+
+    public boolean isAccumulateMultiValues() {
+        return accumulateMultiValues;
+    }
+
+    public void setAccumulateMultiValues(boolean enabled) {
+        accumulateMultiValues = enabled;
+    }
+
+    /**
+     * @return {@code true} if objects are returned as result of the query
+     * @see SearchControls#getReturningObjFlag()
+     */
+    public boolean isReturningObjFlag() {
+        return searchControls.getReturningObjFlag();
+    }
+
+    public void setReturningObjFlag(boolean enabled) {
+        searchControls.setReturningObjFlag(enabled);
+    }
+
+    /**
+     * @return Authentication mode to use: &qout;none&quot;, 
&quot;simple&quot;, etc.
+     * @see Context#SECURITY_AUTHENTICATION
+     */
+    public String getAuthenticationMode() {
+        return Objects.toString(ldapEnv.get(Context.SECURITY_AUTHENTICATION), 
null);
+    }
+
+    public void setAuthenticationMode(String mode) {
+        ldapEnv.put(Context.SECURITY_AUTHENTICATION, 
ValidateUtils.checkNotNull(mode, "No authentication mode"));
+    }
+
+    /**
+     * @return How referrals encountered by the service provider are to be 
processed
+     * @see Context#REFERRAL
+     */
+    public String getReferralMode() {
+        return Objects.toString(ldapEnv.get(Context.REFERRAL), null);
+    }
+
+    public void setReferralMode(String mode) {
+        ldapEnv.put(Context.REFERRAL, 
ValidateUtils.checkNotNullAndNotEmpty(mode, "No referral mode"));
+    }
+
+    /**
+     * @return The specified protocol version - non-positive if default 
provider version used
+     */
+    public int getProtocolVersion() {
+        Object value = ldapEnv.get("java.naming.ldap.version");
+        return (value != null) ? ((Number) value).intValue() : -1;
+    }
+
+    public void setProtocolVersion(int value) {
+        ValidateUtils.checkTrue(value > 0, "Non-positive protocol value: %d", 
value);
+        ldapEnv.put("java.naming.ldap.version", value);
+    }
+
+    /**
+     * @return Comma separated list of attributes known to be binary
+     * so that they are returned as {@code byte[]} value rather than strings
+     */
+    public String getBinaryAttributes() {
+        return 
Objects.toString(ldapEnv.get("java.naming.ldap.attributes.binary"), 
"").replace(' ', ',');
+    }
+
+    /**
+     * @param value Comma separated list of attributes known to be binary
+     * so that they are returned as {@code byte[]} value rather than strings
+     * @see <A 
HREF="http://docs.oracle.com/javase/jndi/tutorial/ldap/misc/attrs.html";>LDAP 
Attributes</A>
+     */
+    public void setBinaryAttributes(String value) {
+        value = ValidateUtils.checkNotNullAndNotEmpty(value, "No 
attributes").replace(',', ' ');
+        ldapEnv.put("java.naming.ldap.attributes.binary", value);
+    }
+
+    /**
+     * @param username Username to be used either to access the LDAP or 
retrieve the user's attributes -
+     *                 may be {@code null}/empty if not required for the 
specific query
+     * @param password Password Password to be used if necessary - may be 
{@code null}/empty if not
+     *                 required for the specific query
+     * @param queryContext User specific query context - relevant only for 
derived classes that want
+     *                 to override some of query processing methods
+     * @return A {@link Map} of the retrieved attributes - <B>Note:</B> if 
{@link #isAccumulateMultiValues()}
+     * is {@code true} and multiple values are encountered for an attribute 
then a {@link List} of them is
+     * mapped as its value
+     * @throws NamingException If failed to executed the LDAP query
+     */
+    public Map<String, Object> resolveAttributes(String username, String 
password, Object queryContext) throws NamingException {
+        DirContext context = initializeDirContext(queryContext, ldapEnv, 
username, password);
+        try {
+            Map<?, ?> ldapConfig = context.getEnvironment();
+            String baseDN = resolveBaseDN(queryContext, ldapConfig, username, 
password);
+            String filter = resolveSearchFilter(queryContext, ldapConfig, 
username, password);
+            NamingEnumeration<? extends SearchResult> result =
+                    
context.search(ValidateUtils.checkNotNullAndNotEmpty(baseDN, "No base DN"),
+                                   
ValidateUtils.checkNotNullAndNotEmpty(filter, "No filter"),
+                                   searchControls);
+            try {
+                Map<String, Object> attrsMap = new TreeMap<String, 
Object>(String.CASE_INSENSITIVE_ORDER);
+                String referralMode = 
Objects.toString(ldapConfig.get(Context.REFERRAL), null);
+                for (int index = 0;; index++) {
+                    if (!result.hasMore()) {
+                        break;
+                    }
+
+                    processSearchResult(queryContext, ldapConfig, attrsMap, 
index, result.next());
+
+                    // if not following referrals stop at the 1st result 
regardless if there are others
+                    if ("ignore".equals(referralMode)) {
+                        break;
+                    }
+                }
+
+                return attrsMap;
+            } finally {
+                result.close();
+            }
+        } finally {
+            context.close();
+        }
+    }
+
+    protected DirContext initializeDirContext(Object queryContext, Map<String, 
?> ldapConfig, String username, String password) throws NamingException {
+        Map<String, Object> env;
+        synchronized (ldapConfig) { // create a copy so we can change it
+            env = new HashMap<String, Object>(ldapConfig);
+        }
+
+        if (!env.containsKey(Context.PROVIDER_URL)) {
+            int port = getPort();
+            ValidateUtils.checkTrue(port > 0, "No port configured");
+            String url = ValidateUtils.checkNotNullAndNotEmpty(getProtocol(), 
"No protocol")
+                       + "://" + 
ValidateUtils.checkNotNullAndNotEmpty(getHost(), "No host")
+                       + ":" + port;
+            env.put(Context.PROVIDER_URL, url);
+        }
+
+        String mode = 
Objects.toString(env.get(Context.SECURITY_AUTHENTICATION), null);
+        boolean anonymous = GenericUtils.isEmpty(mode) || 
"none".equalsIgnoreCase(mode);
+        if (!anonymous) {
+            Object[] bindParams = {username, password};
+            if (!env.containsKey(Context.SECURITY_PRINCIPAL)) {
+                String bindDN = ValidateUtils.checkNotNull(bindDNPattern, "No 
bind DN pattern").format(bindParams);
+                env.put(Context.SECURITY_PRINCIPAL, 
ValidateUtils.checkNotNullAndNotEmpty(bindDN, "No bind DN"));
+            }
+
+            if (!env.containsKey(Context.SECURITY_CREDENTIALS)) {
+                String bindPassword = 
ValidateUtils.checkNotNull(bindPasswordPattern, "No bind password 
pattern").format(bindParams);
+                env.put(Context.SECURITY_CREDENTIALS, 
ValidateUtils.checkNotNullAndNotEmpty(bindPassword, "No bind password"));
+            }
+        }
+
+        return new InitialDirContext(new Hashtable<String, Object>(env));
+    }
+
+    protected String resolveBaseDN(Object queryContext, Map<?, ?> ldapConfig, 
String username, String password) throws NamingException {
+        Object[] bindParams = {username, password};
+        return ValidateUtils.checkNotNull(baseDNPattern, "No base DN 
pattern").format(bindParams);
+    }
+
+    protected String resolveSearchFilter(Object queryContext, Map<?, ?> 
ldapConfig, String username, String password) throws NamingException {
+        Object[] bindParams = {username, password};
+        return ValidateUtils.checkNotNull(searchFilterPattern, "No search 
filter pattern").format(bindParams);
+    }
+
+    protected void processSearchResult(Object queryContext, Map<?, ?> 
ldapConfig, Map<String, Object> attrsMap,
+            int resultIndex, SearchResult result)
+                    throws NamingException {
+        String dn = result.getName();
+        accumulateAttributeValue(queryContext, attrsMap, 
Context.AUTHORITATIVE, dn);
+
+        Attributes attrs = result.getAttributes();
+        NamingEnumeration<? extends Attribute>  attrVals = attrs.getAll();
+        try {
+            while (attrVals.hasMore()) {
+                processResultAttributeValue(queryContext, ldapConfig, dn, 
resultIndex, attrsMap, attrVals.next());
+            }
+        } finally {
+            attrVals.close();
+        }
+    }
+
+    // returns the most up-to-date value mapped for the attribute
+    protected Object processResultAttributeValue(Object queryContext, Map<?, 
?> ldapConfig,
+            String dn, int resultIndex, Map<String, Object> attrsMap, 
Attribute a)
+                    throws NamingException {
+        String attrID = a.getID();
+        int numValues = a.size();
+        for (int index = 0; index < numValues; index++) {
+            Object attrVal = a.get(index);
+
+            if (attrVal != null) {
+                Object  prev = accumulateAttributeValue(queryContext, 
attrsMap, attrID, attrVal);
+                if (log.isTraceEnabled()) {
+                    if (prev != null) {
+                        log.trace("processResultAttributeValue({})[{}] 
multiple values: {} / {}",
+                                  dn, attrID, toString(prev), 
toString(attrVal));
+                    } else {
+                        log.trace("processResultAttributeValue({}) {} = {}", 
dn, attrID, toString(attrVal));
+                    }
+                }
+            } else {
+                if (log.isTraceEnabled()) {
+                    log.trace("processResultAttributeValue({}) skip null 
attribute: {}", dn, attrID);
+                }
+            }
+
+            if ((numValues > 1) && (!isAccumulateMultiValues())) {
+                if (log.isTraceEnabled()) {
+                    log.trace("processResultAttributeValue({})[{}] skip 
remaining {} values",
+                              dn, attrID, numValues - 1);
+                }
+
+                break;
+            }
+        }
+
+        return attrsMap.get(attrID);
+    }
+
+    @SuppressWarnings("unchecked")
+    protected Object accumulateAttributeValue(Object queryContext, Map<String, 
Object> attrsMap, String attrID, Object attrVal) {
+        Object prev = attrsMap.put(attrID, attrVal);
+        if (prev == null) {
+            return null;    // debug breakpoint
+        }
+
+        List<Object> values = null;
+        if (prev instanceof List<?>) {
+            values = (List<Object>) prev;
+        } else {
+            values = new ArrayList<Object>();
+            values.add(prev);
+            attrsMap.put(attrID, values);
+        }
+
+        values.add(attrVal);
+        return values.get(values.size() - 2);
+    }
+
+    public static String toString(Object attrVal) {
+        if (attrVal == null) {
+            return null;
+        }
+
+        Class<?> attrType = attrVal.getClass();
+        if (attrType.isArray()) {
+            return (attrVal instanceof byte[]) ? BufferUtils.printHex((byte[]) 
attrVal) : Arrays.toString((Object[]) attrVal);
+        }
+
+        return attrVal.toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-ldap/src/main/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticator.java
----------------------------------------------------------------------
diff --git 
a/sshd-ldap/src/main/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticator.java
 
b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticator.java
new file mode 100644
index 0000000..251a3a8
--- /dev/null
+++ 
b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticator.java
@@ -0,0 +1,71 @@
+/*
+ * 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.sshd.server.auth.password;
+
+import java.util.Map;
+
+import javax.naming.NamingException;
+
+import org.apache.sshd.common.util.net.LdapNetworkConnector;
+import org.apache.sshd.server.session.ServerSession;
+
+/**
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+public class LdapPasswordAuthenticator extends LdapNetworkConnector implements 
PasswordAuthenticator {
+    public static final String DEFAULT_USERNAME_ATTR_NAME = "uid";
+    public static final String DEFAULT_PASSWORD_ATTR_NAME = "userPassword";
+
+    public static final String DEFAULT_SEARCH_FILTER_PATTERN =
+            "(&(" + DEFAULT_USERNAME_ATTR_NAME + "={0})(" + 
DEFAULT_PASSWORD_ATTR_NAME + "={1}))";
+    public static final String DEFAULT_AUTHENTICATION_MODE = "none";
+
+    public LdapPasswordAuthenticator() {
+        setRetrievedAttributes(null);
+        setAuthenticationMode(DEFAULT_AUTHENTICATION_MODE);
+        setSearchFilterPattern(DEFAULT_SEARCH_FILTER_PATTERN);
+    }
+
+    @Override
+    public boolean authenticate(String username, String password, 
ServerSession session) throws PasswordChangeRequiredException {
+        try {
+            Map<String, ?> attrs = resolveAttributes(username, password, 
session);
+            return authenticate(username, password, session, attrs);
+        } catch (NamingException | RuntimeException e) {
+            log.warn("authenticate({}@{}) failed ({}) to query: {}",
+                      username, session, e.getClass().getSimpleName(), 
e.getMessage());
+
+            if (log.isDebugEnabled()) {
+                log.debug("authenticate(" + username + "@" + session + ") 
query failure details", e);
+            }
+
+            return false;
+        }
+    }
+
+    protected boolean authenticate(String username, String password, 
ServerSession session, Map<String, ?> attrs) {
+        /*
+         * By default we assume that the user + password are the same for
+         * accessing the LDAP as the user's account, so the very LDAP query
+         * success is enough
+         */
+        return true;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-ldap/src/main/resources/.gitignore
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/main/resources/.gitignore 
b/sshd-ldap/src/main/resources/.gitignore
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-ldap/src/test/java/org/apache/sshd/server/auth/BaseAuthenticatorTest.java
----------------------------------------------------------------------
diff --git 
a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/BaseAuthenticatorTest.java
 
b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/BaseAuthenticatorTest.java
new file mode 100644
index 0000000..8c0fa47
--- /dev/null
+++ 
b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/BaseAuthenticatorTest.java
@@ -0,0 +1,233 @@
+/*
+ * 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.sshd.server.auth;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.directory.server.constants.ServerDNConstants;
+import org.apache.directory.server.core.CoreSession;
+import org.apache.directory.server.core.DefaultDirectoryService;
+import org.apache.directory.server.core.DirectoryService;
+import 
org.apache.directory.server.core.partition.impl.btree.jdbm.JdbmPartition;
+import org.apache.directory.server.core.partition.ldif.LdifPartition;
+import org.apache.directory.server.core.schema.SchemaPartition;
+import org.apache.directory.server.core.schema.SchemaService;
+import org.apache.directory.server.ldap.LdapServer;
+import org.apache.directory.server.protocol.shared.transport.TcpTransport;
+import org.apache.directory.server.protocol.shared.transport.Transport;
+import org.apache.directory.shared.ldap.entry.Entry;
+import org.apache.directory.shared.ldap.entry.EntryAttribute;
+import org.apache.directory.shared.ldap.ldif.ChangeType;
+import org.apache.directory.shared.ldap.ldif.LdifEntry;
+import org.apache.directory.shared.ldap.ldif.LdifReader;
+import org.apache.directory.shared.ldap.message.AddRequestImpl;
+import org.apache.directory.shared.ldap.message.internal.InternalAddRequest;
+import org.apache.directory.shared.ldap.schema.SchemaManager;
+import 
org.apache.directory.shared.ldap.schema.ldif.extractor.SchemaLdifExtractor;
+import 
org.apache.directory.shared.ldap.schema.ldif.extractor.impl.DefaultSchemaLdifExtractor;
+import org.apache.directory.shared.ldap.schema.loader.ldif.LdifSchemaLoader;
+import 
org.apache.directory.shared.ldap.schema.manager.impl.DefaultSchemaManager;
+import org.apache.directory.shared.ldap.schema.registries.SchemaLoader;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.Pair;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.apache.sshd.util.test.Utils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+public abstract class BaseAuthenticatorTest extends BaseTestSupport {
+    public static final int PORT = 
Integer.parseInt(System.getProperty("org.apache.sshd.test.ldap.port", "11389"));
+    public static String BASE_DN_TEST = "ou=People,dc=sshd,dc=apache,dc=org";
+
+    protected BaseAuthenticatorTest() {
+        super();
+    }
+
+    public static int getPort(Pair<LdapServer, DirectoryService> context) {
+        return getPort((context == null) ? null : context.getFirst());
+    }
+
+    public static int getPort(LdapServer ldapServer) {
+        return getPort((ldapServer == null) ? null : 
ldapServer.getTransports());
+    }
+
+    public static int getPort(Transport ... transports) {
+        return GenericUtils.isEmpty(transports) ? -1 : transports[0].getPort();
+    }
+
+    // see http://javlog.cacek.cz/2014/09/speed-up-apacheds-ldap-server.html
+    // see 
https://cwiki.apache.org/confluence/display/DIRxSRVx11/4.1.+Embedding+ApacheDS+into+an+application
+    // see 
http://stackoverflow.com/questions/1560230/running-apache-ds-embedded-in-my-application
+    public static Pair<LdapServer, DirectoryService> startApacheDs(Class<?> 
anchor) throws Exception {
+        Logger log = LoggerFactory.getLogger(anchor);
+        File targetFolder = 
ValidateUtils.checkNotNull(Utils.detectTargetFolder(anchor), "Failed to detect 
target folder");
+        File workingDirectory = 
assertHierarchyTargetFolderExists(Utils.deleteRecursive(Utils.resolve(targetFolder,
 anchor.getSimpleName(), "apacheds-work")));
+
+        DirectoryService directoryService = new DefaultDirectoryService();
+        directoryService.setWorkingDirectory(workingDirectory);
+
+        SchemaService schemaService = directoryService.getSchemaService();
+        SchemaPartition schemaPartition = schemaService.getSchemaPartition();
+        LdifPartition ldifPartition = new LdifPartition();
+        // see DefaultSchemaLdifExtractor#SCHEMA...
+        File schemaRepository = assertHierarchyTargetFolderExists(new 
File(workingDirectory, "schema"));
+        ldifPartition.setWorkingDirectory(schemaRepository.getAbsolutePath());
+
+        SchemaLdifExtractor extractor = new 
DefaultSchemaLdifExtractor(workingDirectory);
+        extractor.extractOrCopy(true);
+        schemaPartition.setWrappedPartition(ldifPartition);
+
+        SchemaLoader loader = new LdifSchemaLoader(schemaRepository);
+        SchemaManager schemaManager = new DefaultSchemaManager(loader);
+        directoryService.setSchemaManager(schemaManager);
+
+        schemaManager.loadAllEnabled();
+
+        schemaPartition.setSchemaManager(schemaManager);
+
+        List<Throwable> errors = schemaManager.getErrors();
+        if (GenericUtils.size(errors) > 0) {
+            log.error("Schema management loading errors found");
+            for (Throwable t : errors) {
+                log.error(t.getClass().getSimpleName() + ": " + 
t.getMessage(), t);
+            }
+            throw new Exception("Schema load failed");
+        }
+
+        {
+            JdbmPartition systemPartition = new JdbmPartition();
+            systemPartition.setId("system");
+            
systemPartition.setPartitionDir(assertHierarchyTargetFolderExists(Utils.deleteRecursive(new
 File(workingDirectory, systemPartition.getId()))));
+            systemPartition.setSuffix(ServerDNConstants.SYSTEM_DN);
+            systemPartition.setSchemaManager(schemaManager);
+            directoryService.setSystemPartition(systemPartition);
+        }
+
+        // Create a new partition for the users
+        {
+            JdbmPartition partition = new JdbmPartition();
+            partition.setId("users");
+            partition.setSuffix(BASE_DN_TEST);
+            
partition.setPartitionDir(assertHierarchyTargetFolderExists(Utils.deleteRecursive(new
 File(workingDirectory, partition.getId()))));
+            directoryService.addPartition(partition);
+        }
+
+        directoryService.setShutdownHookEnabled(true);
+        directoryService.getChangeLog().setEnabled(false);
+
+        LdapServer ldapServer = new LdapServer();
+        ldapServer.setTransports(new TcpTransport(PORT));
+        ldapServer.setDirectoryService(directoryService);
+
+        log.info("Starting directory service ...");
+        directoryService.startup();
+        log.info("Directory service started");
+
+        log.info("Starting LDAP server on port=" + getPort(ldapServer) + " 
...");
+        try {
+            ldapServer.start();
+            log.info("LDAP server started");
+        } catch(Exception e) {
+            log.error("Failed (" + e.getClass().getSimpleName() + ") to start 
LDAP server: " + e.getMessage(), e);
+            e.printStackTrace(System.err);
+            stopApacheDs(directoryService);
+            throw e;
+        }
+
+        return new Pair<LdapServer, DirectoryService>(ldapServer, 
directoryService);
+    }
+
+    // see 
http://users.directory.apache.narkive.com/GkyqAkot/how-to-import-ldif-file-programmatically
+    public static Map<String, String> populateUsers(DirectoryService service, 
Class<?> anchor, String credentialName) throws Exception {
+        Logger log = LoggerFactory.getLogger(anchor);
+        CoreSession session = 
ValidateUtils.checkNotNull(service.getAdminSession(), "No core session");
+        Map<String, String> usersMap = new HashMap<>();
+        try (LdifReader reader = new 
LdifReader(ValidateUtils.checkNotNull(anchor.getResourceAsStream("/auth-users.ldif"),
 "No users ldif"))) {
+            int id = 1;
+            for (LdifEntry entry : reader) {
+                if (log.isDebugEnabled()) {
+                    log.debug("Add LDIF entry={}", entry);
+                }
+
+                ChangeType changeType = entry.getChangeType();
+                assertEquals("Mismatched change type in users ldif entry=" + 
entry, ChangeType.Add, changeType);
+
+                Entry data = entry.getEntry();
+                EntryAttribute userAttr = data.get("uid");
+                EntryAttribute passAttr = data.get(credentialName);
+                if ((userAttr != null) && (passAttr != null)) {
+                    String username = userAttr.getString();
+                    ValidateUtils.checkTrue(usersMap.put(username, 
passAttr.getString()) == null, "Multiple entries for user=%s", username);
+                }
+
+                InternalAddRequest addRequest = new AddRequestImpl(id++);
+                addRequest.setEntry(data);
+                try {
+                    session.add(addRequest);
+                } catch (Exception e) {
+                    log.error("Failed (" + e.getClass().getSimpleName() + ") 
to add entry=" + entry + ": " + e.getMessage(), e);
+                    throw e;
+                }
+            }
+        }
+
+        return usersMap;
+    }
+
+    public static void stopApacheDs(Pair<LdapServer, DirectoryService> 
context) throws Exception {
+        stopApacheDs((context == null) ? null : context.getFirst());
+        stopApacheDs((context == null) ? null : context.getSecond());
+    }
+
+    public static void stopApacheDs(LdapServer ldapServer) throws Exception {
+        if ((ldapServer == null) || (!ldapServer.isStarted())) {
+            return;
+        }
+
+        Logger log = LoggerFactory.getLogger(BaseAuthenticatorTest.class);
+        log.info("Stopping LDAP server...");
+        ldapServer.stop();
+        log.info("LDAP server stopped");
+    }
+
+    public static void stopApacheDs(DirectoryService directoryService) throws 
Exception {
+        if ((directoryService == null) || (!directoryService.isStarted())) {
+            return;
+        }
+
+        Logger log = LoggerFactory.getLogger(BaseAuthenticatorTest.class);
+        File workDir = directoryService.getWorkingDirectory();
+
+        log.info("Shutdown directory service ...");
+        directoryService.shutdown();
+        log.info("Directory service shut down");
+
+        log.info("Deleting " + workDir.getAbsolutePath());
+        Utils.deleteRecursive(workDir);
+        log.info(workDir.getAbsolutePath() + " deleted");
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-ldap/src/test/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticatorTest.java
----------------------------------------------------------------------
diff --git 
a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticatorTest.java
 
b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticatorTest.java
new file mode 100644
index 0000000..2a88342
--- /dev/null
+++ 
b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticatorTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.sshd.server.auth.password;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.directory.server.core.DirectoryService;
+import org.apache.directory.server.ldap.LdapServer;
+import org.apache.sshd.common.util.Pair;
+import org.apache.sshd.server.auth.BaseAuthenticatorTest;
+import org.apache.sshd.server.session.ServerSession;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+import org.mockito.Mockito;
+
+/**
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class LdapPasswordAuthenticatorTest extends BaseAuthenticatorTest {
+    private static final AtomicReference<Pair<LdapServer, DirectoryService>> 
ldapContextHolder = new AtomicReference<>();
+    private static Map<String, String> usersMap;
+
+    public LdapPasswordAuthenticatorTest() {
+        super();
+    }
+
+    @BeforeClass
+    public static void startApacheDs() throws Exception {
+        
ldapContextHolder.set(startApacheDs(LdapPasswordAuthenticatorTest.class));
+        usersMap = populateUsers(ldapContextHolder.get().getSecond(), 
LdapPasswordAuthenticatorTest.class, 
LdapPasswordAuthenticator.DEFAULT_PASSWORD_ATTR_NAME);
+    }
+
+    @AfterClass
+    public static void stopApacheDs() throws Exception {
+        stopApacheDs(ldapContextHolder.getAndSet(null));
+    }
+
+    @Test   // the user's password is compared with the LDAP stored one
+    public void testPasswordComparison() throws Exception {
+        LdapPasswordAuthenticator auth = new LdapPasswordAuthenticator();
+        auth.setBaseDN(BASE_DN_TEST);
+        auth.setPort(getPort(ldapContextHolder.get()));
+
+        ServerSession session = Mockito.mock(ServerSession.class);
+        for (Map.Entry<String, String> ue : usersMap.entrySet()) {
+            String username = ue.getKey();
+            String password = ue.getValue();
+            outputDebugMessage("Authenticate: user=%s, password=%s", username, 
password);
+            assertTrue("Failed to authenticate " + username, 
auth.authenticate(username, password, session));
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-ldap/src/test/resources/auth-users.ldif
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/test/resources/auth-users.ldif 
b/sshd-ldap/src/test/resources/auth-users.ldif
new file mode 100644
index 0000000..4792099
--- /dev/null
+++ b/sshd-ldap/src/test/resources/auth-users.ldif
@@ -0,0 +1,31 @@
+version: 1
+
+dn: ou=People,dc=sshd,dc=apache,dc=org
+ou: People
+objectClass: top
+objectClass: organizationalUnit
+description: Parent object of all users accounts
+
+dn: cn=Guillaume Nodet,ou=People,dc=sshd,dc=apache,dc=org
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectclass: inetOrgPerson
+cn: Guillaume Nodet
+givenName: Guillaume
+sn: Nodet
+uid: gnodet
+userpassword: gnodet
+mail: [email protected]
+
+dn: cn=Lyor Goldstein,ou=People,dc=sshd,dc=apache,dc=org
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectclass: inetOrgPerson
+cn: Lyor Goldstein
+givenName: Lyor
+sn: Goldstein
+uid: lgoldstein
+userpassword: lgoldstein
+mail: [email protected]

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-ldap/src/test/resources/hostkey.pem
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/test/resources/hostkey.pem 
b/sshd-ldap/src/test/resources/hostkey.pem
new file mode 100644
index 0000000..18d68ac
--- /dev/null
+++ b/sshd-ldap/src/test/resources/hostkey.pem
@@ -0,0 +1,30 @@
+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.
+
+-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQDdfIWeSV4o68dRrKSzFd/Bk51E65UTmmSrmW0O1ohtzi6HzsDP
+jXgCtlTt3FqTcfFfI92IlTr4JWqC9UK1QT1ZTeng0MkPQmv68hDANHbt5CpETZHj
+W5q4OOgWhVvj5IyOC2NZHtKlJBkdsMAa15ouOOJLzBvAvbqOR/yUROsEiQIDAQAB
+AoGBANG3JDW6NoP8rF/zXoeLgLCj+tfVUPSczhGFVrQkAk4mWfyRkhN0WlwHFOec
+K89MpkV1ij/XPVzU4MNbQ2yod1KiDylzvweYv+EaEhASCmYNs6LS03punml42SL9
+97tOmWfVJXxlQoLiY6jHPU97vTc65k8gL+gmmrpchsW0aqmZAkEA/c8zfmKvY37T
+cxcLLwzwsqqH7g2KZGTf9aRmx2ebdW+QKviJJhbdluDgl1TNNFj5vCLznFDRHiqJ
+wq0wkZ39cwJBAN9l5v3kdXj21UrurNPdlV0n2GZBt2vblooQC37XHF97r2zM7Ou+
+Lg6MyfJClyguhWL9dxnGbf3btQ0l3KDstxMCQCRaiEqjAfIjWVATzeNIXDWLHXso
+b1kf5cA+cwY+vdKdTy4IeUR+Y/DXdvPWDqpf0C11aCVMohdLCn5a5ikFUycCQDhV
+K/BuAallJNfmY7JxN87r00fF3ojWMJnT/fIYMFFrkQrwifXQWTDWE76BSDibsosJ
+u1TGksnm8zrDh2UVC/0CQFrHTiSl/3DHvWAbOJawGKg46cnlDcAhSyV8Frs8/dlP
+7YGG3eqkw++lsghqmFO6mRUTKsBmiiB2wgLGhL5pyYY=
+-----END RSA PRIVATE KEY-----

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-ldap/src/test/resources/log4j.properties
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/test/resources/log4j.properties 
b/sshd-ldap/src/test/resources/log4j.properties
new file mode 100644
index 0000000..590c257
--- /dev/null
+++ b/sshd-ldap/src/test/resources/log4j.properties
@@ -0,0 +1,38 @@
+#
+# 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.
+#
+#
+
+#
+# The logging properties used during tests..
+#
+log4j.rootLogger=INFO, stdout, logfile
+#log4j.logger.org.apache.sshd=TRACE
+#log4j.logger.org.apache.sshd.common.channel.Window=DEBUG
+
+# CONSOLE appender
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+log4j.appender.stdout.layout.ConversionPattern=%d [%-15.15t] %-5p %-30.30c{1} 
- %m%n
+
+# File appender
+log4j.appender.logfile=org.apache.log4j.FileAppender
+log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
+log4j.appender.logfile.layout.ConversionPattern=%d [%-15.15t] %-5p %-30.30c{1} 
- %m%n
+log4j.appender.logfile.file=target/sshd-ldap-tests.log
+log4j.appender.logfile.append=true

Reply via email to