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", "simple", 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
