[SSHD-608] Provide PublicKeyAuthenticator that uses LDAP
Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/e6991a73 Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/e6991a73 Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/e6991a73 Branch: refs/heads/master Commit: e6991a73bd7829c327f1d678b3f2e403d673a418 Parents: f374e72 Author: Lyor Goldstein <[email protected]> Authored: Tue Jan 19 10:56:55 2016 +0200 Committer: Lyor Goldstein <[email protected]> Committed: Tue Jan 19 10:56:55 2016 +0200 ---------------------------------------------------------------------- sshd-ldap/pom.xml | 4 +- .../auth/pubkey/LdapPublickeyAuthenticator.java | 95 ++++++++++++++++++++ .../sshd/server/auth/BaseAuthenticatorTest.java | 29 ++++-- .../password/LdapPasswordAuthenticatorTest.java | 2 + .../pubkey/LdapPublickeyAuthenticatorTest.java | 95 ++++++++++++++++++++ sshd-ldap/src/test/resources/auth-users.ldif | 8 +- 6 files changed, 222 insertions(+), 11 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e6991a73/sshd-ldap/pom.xml ---------------------------------------------------------------------- diff --git a/sshd-ldap/pom.xml b/sshd-ldap/pom.xml index b692860..5921ccb 100644 --- a/sshd-ldap/pom.xml +++ b/sshd-ldap/pom.xml @@ -28,9 +28,9 @@ </parent> <artifactId>sshd-ldap</artifactId> - <name>Apache Mina SSHD :: Git</name> + <name>Apache Mina SSHD :: LDAP</name> <packaging>jar</packaging> - <inceptionYear>2008</inceptionYear> + <inceptionYear>2016</inceptionYear> <properties> <projectRoot>${basedir}/..</projectRoot> http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e6991a73/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java ---------------------------------------------------------------------- diff --git a/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java new file mode 100644 index 0000000..fa3e929 --- /dev/null +++ b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java @@ -0,0 +1,95 @@ +/* + * 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.pubkey; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.PublicKey; +import java.util.Map; +import java.util.Objects; + +import javax.naming.NamingException; + +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.PublicKeyEntryResolver; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.net.LdapNetworkConnector; +import org.apache.sshd.server.config.keys.AuthorizedKeyEntry; +import org.apache.sshd.server.session.ServerSession; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class LdapPublickeyAuthenticator extends LdapNetworkConnector implements PublickeyAuthenticator { + public static final String DEFAULT_USERNAME_ATTR_NAME = "uid"; + public static final String DEFAULT_AUTHENTICATION_MODE = "none"; + public static final String DEFAULT_SEARCH_FILTER_PATTERN = DEFAULT_USERNAME_ATTR_NAME + "={0}"; + public static final String DEFAULT_PUBKEY_ATTR_NAME = "sshPublicKey"; + + private String keyAttributeName = DEFAULT_PUBKEY_ATTR_NAME; + + public LdapPublickeyAuthenticator() { + setAuthenticationMode(DEFAULT_AUTHENTICATION_MODE); + setSearchFilterPattern(DEFAULT_SEARCH_FILTER_PATTERN); + setRetrievedAttributes(DEFAULT_PUBKEY_ATTR_NAME); + } + + /** + * @return The LDAP attribute name containing the public key in {@code OpenSSH} format + */ + public String getKeyAttributeName() { + return keyAttributeName; + } + + public void setKeyAttributeName(String keyAttributeName) { + this.keyAttributeName = ValidateUtils.checkNotNullAndNotEmpty(keyAttributeName, "No attribute name"); + } + + @Override + public boolean authenticate(String username, PublicKey key, ServerSession session) { + try { + Map<String, ?> attrs = resolveAttributes(username, null, session); + return authenticate(username, key, session, attrs); + } catch (NamingException | GeneralSecurityException | IOException | 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, PublicKey expected, ServerSession session, Map<String, ?> attrs) + throws GeneralSecurityException, IOException { + String attrName = getKeyAttributeName(); + Object keyData = ValidateUtils.checkNotNull(attrs.get(attrName), "No data for attribute=%s", attrName); + PublicKey actual = recoverPublicKey(username, expected, session, keyData); + return KeyUtils.compareKeys(expected, actual); + } + + protected PublicKey recoverPublicKey(String username, PublicKey expected, ServerSession session, Object keyData) + throws GeneralSecurityException, IOException { + AuthorizedKeyEntry entry = AuthorizedKeyEntry.parseAuthorizedKeyEntry(Objects.toString(keyData, null)); + return ValidateUtils.checkNotNull(entry, "No key extracted").resolvePublicKey(PublicKeyEntryResolver.FAILING); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e6991a73/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 index 8c0fa47..2b60fb6 100644 --- 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 @@ -127,6 +127,15 @@ public abstract class BaseAuthenticatorTest extends BaseTestSupport { directoryService.setSystemPartition(systemPartition); } + // Create a new partition for the special extra attributes + { + JdbmPartition partition = new JdbmPartition(); + partition.setId("openssh-lpk"); + partition.setSuffix("cn=openssh-lpk,cn=schema,cn=config"); + partition.setPartitionDir(assertHierarchyTargetFolderExists(Utils.deleteRecursive(new File(workingDirectory, partition.getId())))); + directoryService.addPartition(partition); + } + // Create a new partition for the users { JdbmPartition partition = new JdbmPartition(); @@ -170,12 +179,9 @@ public abstract class BaseAuthenticatorTest extends BaseTestSupport { int id = 1; for (LdifEntry entry : reader) { if (log.isDebugEnabled()) { - log.debug("Add LDIF entry={}", entry); + log.debug("Process 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); @@ -184,10 +190,19 @@ public abstract class BaseAuthenticatorTest extends BaseTestSupport { ValidateUtils.checkTrue(usersMap.put(username, passAttr.getString()) == null, "Multiple entries for user=%s", username); } - InternalAddRequest addRequest = new AddRequestImpl(id++); - addRequest.setEntry(data); + ChangeType changeType = entry.getChangeType(); try { - session.add(addRequest); + switch (changeType) { + case Add: { + InternalAddRequest addRequest = new AddRequestImpl(id++); + addRequest.setEntry(data); + session.add(addRequest); + break; + } + + default: + throw new UnsupportedOperationException("Unsupported change type (" + changeType + ") for entry=" + entry); + } } catch (Exception e) { log.error("Failed (" + e.getClass().getSimpleName() + ") to add entry=" + entry + ": " + e.getMessage(), e); throw e; http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e6991a73/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 index 2a88342..aa862ca 100644 --- 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 @@ -24,6 +24,7 @@ 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.GenericUtils; import org.apache.sshd.common.util.Pair; import org.apache.sshd.server.auth.BaseAuthenticatorTest; import org.apache.sshd.server.session.ServerSession; @@ -50,6 +51,7 @@ public class LdapPasswordAuthenticatorTest extends BaseAuthenticatorTest { public static void startApacheDs() throws Exception { ldapContextHolder.set(startApacheDs(LdapPasswordAuthenticatorTest.class)); usersMap = populateUsers(ldapContextHolder.get().getSecond(), LdapPasswordAuthenticatorTest.class, LdapPasswordAuthenticator.DEFAULT_PASSWORD_ATTR_NAME); + assertFalse("No users retrieved", GenericUtils.isEmpty(usersMap)); } @AfterClass http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e6991a73/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java ---------------------------------------------------------------------- diff --git a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java new file mode 100644 index 0000000..c697a56 --- /dev/null +++ b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java @@ -0,0 +1,95 @@ +/* + * 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.pubkey; + +import java.security.PublicKey; +import java.util.HashMap; +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.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.PublicKeyEntryResolver; +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.server.auth.BaseAuthenticatorTest; +import org.apache.sshd.server.config.keys.AuthorizedKeyEntry; +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 LdapPublickeyAuthenticatorTest extends BaseAuthenticatorTest { + private static final AtomicReference<Pair<LdapServer, DirectoryService>> ldapContextHolder = new AtomicReference<>(); + private static final Map<String, PublicKey> keysMap = new HashMap<>(); + // we use this instead of the default since the default requires some extra LDIF manipulation which we don't need + private static final String TEST_ATTR_NAME = "description"; + + public LdapPublickeyAuthenticatorTest() { + super(); + } + + @BeforeClass + public static void startApacheDs() throws Exception { + ldapContextHolder.set(startApacheDs(LdapPublickeyAuthenticatorTest.class)); + Map<String, String> credentials = + populateUsers(ldapContextHolder.get().getSecond(), LdapPublickeyAuthenticatorTest.class, TEST_ATTR_NAME); + assertFalse("No keys retrieved", GenericUtils.isEmpty(credentials)); + + for (Map.Entry<String, String> ce : credentials.entrySet()) { + String username = ce.getKey(); + AuthorizedKeyEntry entry = AuthorizedKeyEntry.parseAuthorizedKeyEntry(ce.getValue()); + PublicKey key = ValidateUtils.checkNotNull(entry, "No key extracted").resolvePublicKey(PublicKeyEntryResolver.FAILING); + keysMap.put(username, key); + } + } + + @AfterClass + public static void stopApacheDs() throws Exception { + stopApacheDs(ldapContextHolder.getAndSet(null)); + } + + @Test + public void testPublicKeyComparison() throws Exception { + LdapPublickeyAuthenticator auth = new LdapPublickeyAuthenticator(); + auth.setBaseDN(BASE_DN_TEST); + auth.setPort(getPort(ldapContextHolder.get())); + auth.setKeyAttributeName(TEST_ATTR_NAME); + auth.setRetrievedAttributes(TEST_ATTR_NAME); + + ServerSession session = Mockito.mock(ServerSession.class); + for (Map.Entry<String, PublicKey> ke : keysMap.entrySet()) { + String username = ke.getKey(); + PublicKey key = ke.getValue(); + outputDebugMessage("Authenticate: user=%s, key-type=%s, fingerprint=%s", + username, KeyUtils.getKeyType(key), KeyUtils.getFingerPrint(key)); + assertTrue("Failed to authenticate user=" + username, auth.authenticate(username, key, session)); + } + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e6991a73/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 index 4792099..0fc3b18 100644 --- a/sshd-ldap/src/test/resources/auth-users.ldif +++ b/sshd-ldap/src/test/resources/auth-users.ldif @@ -1,31 +1,35 @@ version: 1 +## Add People group definition dn: ou=People,dc=sshd,dc=apache,dc=org ou: People objectClass: top objectClass: organizationalUnit description: Parent object of all users accounts +## Add users dn: cn=Guillaume Nodet,ou=People,dc=sshd,dc=apache,dc=org objectClass: top objectClass: person objectClass: organizationalPerson -objectclass: inetOrgPerson +objectClass: inetOrgPerson cn: Guillaume Nodet givenName: Guillaume sn: Nodet uid: gnodet userpassword: gnodet mail: [email protected] +description: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDEAGEwosEIS7koPfYFf2qXAqV+gGiuf3wizjYCz8UdYtmSBINRQhWvVtiWAiLOcHiI/nYSTXctIFyzbWJ74BqvgoZS8eujaPUyBO+asF3enfyrnug9uvqrhsGqnsqFSaAYSQG3p4paKThRhxGX1jqMy6wJaTED1d9SxHs9gnGCZLk1yAr0w/mDkF59YmPoAibmyz1aaXGkjPBWa4ac+RBTEuOxt0hz+ykA7rNua5QPMdNx3sXrv80ECDEs78poOiZgkRvBEnA4qCvSK5FBB5DkVn+3cUhd36tEhKz79u8mLAsrUrgi/JKcrdAZSCyS4oPRt9NEm9YZxcYzqDH98RDp [email protected] dn: cn=Lyor Goldstein,ou=People,dc=sshd,dc=apache,dc=org objectClass: top objectClass: person objectClass: organizationalPerson -objectclass: inetOrgPerson +objectClass: inetOrgPerson cn: Lyor Goldstein givenName: Lyor sn: Goldstein uid: lgoldstein userpassword: lgoldstein mail: [email protected] +description: ssh-dss AAAAB3NzaC1kc3MAAACBAJ6tabphvozXBCnOe2j48OF6pSGmV7+7mRrPxiyPY5mHcHGJXwsE4Rvu81epbTghX3A/s1sme1QtmVVOU3Y3NIxmDBK5UQmSIYS+chXe45GBwC6uJawOn4lPIYw15PJg2ZfjxC1QU5fa6HSQ3vX2MZfYFhEovlBd4mGo2+XY0DqHAAAAFQDeDO2UFNZRBFNN1jEBojpeALh7oQAAAIAI/wYgC1kTjsIFaVvgK3RcGb+lYW0EiuiuG0q37+BzcqtEgjVckvxoCTeghcyf6vhB+ZbrM9we2jH3JvmF4gmtmzjW8UtgYCB2ovRYmd21cSPQW2F9U+sIl2+2sI9rsyrAdyhW1qZ4HVIrKH0RdWkAkjQ+8EGKHKXKo6aEV/HdpAAAAIAv80m0MZoocokStZV7EclLwt3ihvN7/Tjf5aHS9fY8b1ev2Z7eIVm3UHirggEeJMQgIw3MBE/T1ttYyqwnVdOatBJVcVLXYUPuXrq6f9/TYEpYxLqowjOwPaAkZAnecVYo0eU52VSkR38onu7juk4BNzHtLILTvtZppQ6l3loOpA== [email protected]
