Repository: mina-sshd Updated Branches: refs/heads/master d03d98113 -> 1e8ac3095
[SSHD-762] Add InteractivePasswordIdentityProvider Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/1e8ac309 Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/1e8ac309 Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/1e8ac309 Branch: refs/heads/master Commit: 1e8ac3095656fcc55c9a0fb7b64242cca8f3a4c2 Parents: d03d981 Author: Goldstein Lyor <l...@c-b4.com> Authored: Sun Aug 13 17:08:07 2017 +0300 Committer: Goldstein Lyor <l...@c-b4.com> Committed: Mon Aug 14 07:33:51 2017 +0300 ---------------------------------------------------------------------- README.md | 56 +++++++- .../InteractivePasswordIdentityProvider.java | 140 +++++++++++++++++++ .../SimpleAccessControlScpEventListener.java | 1 + .../proxyprotocol/ProxyProtocolAcceptor.java | 1 + .../SimpleAccessControlSftpEventListener.java | 1 + ...InteractivePasswordIdentityProviderTest.java | 99 +++++++++++++ .../client/auth/keyboard/UserInteraction.java | 6 +- 7 files changed, 298 insertions(+), 6 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/1e8ac309/README.md ---------------------------------------------------------------------- diff --git a/README.md b/README.md index f1971b3..de1e2dd 100644 --- a/README.md +++ b/README.md @@ -1129,7 +1129,9 @@ decide whether to accept a successfully authenticated session. # Extension modules -There are several extension modules available +There are several extension modules available - specifically, the _sshd-contrib_ module contains some of them. **Note:** the module contains experimental code that may find its way some time +in the future to a standard artifact. It is also subject to changes and/or deletion without any prior announcement. Therefore, any code that relies on it should also store a copy of the sources +in case the classes it used it are modified or deleted. ## Command line clients @@ -1162,15 +1164,61 @@ The _sshd-ldap_ artifact contains an [LdapPasswordAuthenticator](https://issues. ## PROXY / SSLH protocol hooks -The code contains [support for "wrapper" protocols](https://issues.apache.org/jira/browse/SSHD-656) such as [PROXY](http://www.haproxy.org/download/1.6/doc/proxy-protocol.txt) or [sslh](http://www.rutschle.net/tech/sslh.shtml). The idea is that one can register either a `ClientProxyConnector` or `ServerProxyAcceptor` and intercept the 1st packet being sent/received (respectively) **before** it reaches the SSHD code. This gives the programmer the capability to write a front-end that routes outgoing/incoming packets: +The code contains [support for "wrapper" protocols](https://issues.apache.org/jira/browse/SSHD-656) such as [PROXY](http://www.haproxy.org/download/1.6/doc/proxy-protocol.txt) or [sslh](http://www.rutschle.net/tech/sslh.shtml). The idea is that one can register either a `ClientProxyConnector` or `ServerProxyAcceptor` and intercept the 1st packet being sent/received (respectively) **before** it reaches the SSHD code. This gives the programmer the capability to write a front-end that routes outgoing/incoming packets: * `SshClient/ClientSesssion#setClientProxyConnector` - sets a proxy that intercepts the 1st packet before being sent to the server * `SshServer/ServerSession#setServerProxyAcceptor` - sets a proxy that intercept the 1st incoming packet before being processed by the server -## PUTTY key file(s) readers +## Useful extra components in _sshd-contrib_ + +* PUTTY key file(s) readers - see `org.apache.sshd.common.config.keys.loader.putty` package - specifically `PuttyKeyUtils#DEFAULT_INSTANCE KeyPairResourceParser`. + + +* `InteractivePasswordIdentityProvider` - helps implement a `PasswordIdentityProvider` by delegating calls to `UserInteraction#getUpdatedPassword`. +The way to use it would be as follows: + + +```java +try (ClientSession session = client.connect(login, host, port).await().getSession()) { + session.setUserInteraction(...); // this can also be set at the client level + PasswordIdentityProvider passwordIdentityProvider = + InteractivePasswordIdentityProvider.providerOf(session, "My prompt"); + session.setPasswordIdentityProvider(passwordIdentityProvider); + session.auth.verify(...timeout...); + ... continue with the authenticated session ... +} +``` + +or + + +```java +UserInteraction ui = ....; +try (ClientSession session = client.connect(login, host, port).await().getSession()) { + PasswordIdentityProvider passwordIdentityProvider = + InteractivePasswordIdentityProvider.providerOf(session, ui, "My prompt"); + session.setPasswordIdentityProvider(passwordIdentityProvider); + session.auth.verify(...timeout...); + ... continue with the authenticated session ... +} +``` + + +**Note:** `UserInteraction#isInteractionAllowed` is consulted prior to invoking `getUpdatedPassword` - if it returns _false_ then password retrieval method is not invoked, +and it is assumed that no more passwords are available + + +* `SimpleAccessControlScpEventListener` - Provides a simple access control by making a distinction between methods that upload data and ones that download it via SCP. +In order to use it, simply extend it and override its `isFileUpload/DownloadAllowed` methods + + +* `SimpleAccessControlSftpEventListener` - Provides a simple access control by making a distinction between methods that provide SFTP file information - including +reading data - and those that modify it + + +* `ProxyProtocolAcceptor` - A working prototype to support the PROXY protocol as described in [HAProxy Documentation](http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) -Part of the _sshd-contrib_ artifact. # Builtin components http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/1e8ac309/sshd-contrib/src/main/java/org/apache/sshd/client/auth/password/InteractivePasswordIdentityProvider.java ---------------------------------------------------------------------- diff --git a/sshd-contrib/src/main/java/org/apache/sshd/client/auth/password/InteractivePasswordIdentityProvider.java b/sshd-contrib/src/main/java/org/apache/sshd/client/auth/password/InteractivePasswordIdentityProvider.java new file mode 100644 index 0000000..798735b --- /dev/null +++ b/sshd-contrib/src/main/java/org/apache/sshd/client/auth/password/InteractivePasswordIdentityProvider.java @@ -0,0 +1,140 @@ +/* + * 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.client.auth.password; + +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.sshd.client.auth.keyboard.UserInteraction; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.client.session.ClientSessionHolder; +import org.apache.sshd.common.util.GenericUtils; + +/** + * <P> + * Helps implement a {@link PasswordIdentityProvider} by delegating calls + * to {@link UserInteraction#getUpdatedPassword(ClientSession, String, String)}. + * The way to use it would be as follows: + * </P> + * <CODE><PRE> + * try (ClientSession session = client.connect(login, host, port).await().getSession()) { + * session.setUserInteraction(...); // this can also be set at the client level + * PasswordIdentityProvider passwordIdentityProvider = + * InteractivePasswordIdentityProvider.providerOf(session, "My prompt"); + * session.setPasswordIdentityProvider(passwordIdentityProvider); + * session.auth.verify(...timeout...); + * } + * + * or + * + * UserInteraction ui = ....; + * try (ClientSession session = client.connect(login, host, port).await().getSession()) { + * PasswordIdentityProvider passwordIdentityProvider = + * InteractivePasswordIdentityProvider.providerOf(session, ui, "My prompt"); + * session.setPasswordIdentityProvider(passwordIdentityProvider); + * session.auth.verify(...timeout...); + * } + * </PRE></CODE> + * + * <B>Note:</B> {@link UserInteraction#isInteractionAllowed(ClientSession)} is consulted + * prior to invoking {@code getUpdatedPassword} - if returns {@code false} then password + * retrieval method is not invoked, and it is assumed that no more passwords are available + * + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public class InteractivePasswordIdentityProvider implements Iterator<String>, ClientSessionHolder { + /** Special marker to indicate that we exhausted all attempts */ + protected static final String EOF = "EOF"; + + private ClientSession clientSession; + private UserInteraction userInteraction; + private String prompt; + private AtomicReference<String> nextPassword = new AtomicReference<>(); + + public InteractivePasswordIdentityProvider(ClientSession clientSession, UserInteraction userInteraction, String prompt) { + this.clientSession = Objects.requireNonNull(clientSession, "No client session provided"); + this.userInteraction = Objects.requireNonNull(userInteraction, "No user interaction instance configured"); + this.prompt = prompt; + } + + @Override + public ClientSession getClientSession() { + return clientSession; + } + + public UserInteraction getUserInteraction() { + return userInteraction; + } + + public String getPrompt() { + return prompt; + } + + @Override + public boolean hasNext() { + String password = nextPassword.get(); + if (GenericUtils.isEmpty(password)) { + password = resolveNextPassword(); + if (GenericUtils.isEmpty(password)) { + password = EOF; + } + nextPassword.set(password); + } + + return !GenericUtils.isSameReference(password, EOF); + } + + @Override + public String next() { + String password = nextPassword.get(); + if (password == null) { + throw new IllegalStateException("hasNext() not called before next()"); + } + + if (GenericUtils.isSameReference(password, EOF)) { + throw new NoSuchElementException("All passwords exhausted"); + } + + nextPassword.set(null); // force read of next password when 'hasNext' invoked + return password; + } + + protected String resolveNextPassword() { + ClientSession session = getClientSession(); + UserInteraction ui = getUserInteraction(); + if (!ui.isInteractionAllowed(session)) { + return null; + } + + return ui.getUpdatedPassword(session, getPrompt(), ""); + } + + public static PasswordIdentityProvider providerOf(ClientSession clientSession, String prompt) { + return providerOf(clientSession, (clientSession == null) ? null : clientSession.getUserInteraction(), prompt); + } + + public static PasswordIdentityProvider providerOf(ClientSession clientSession, UserInteraction userInteraction, String prompt) { + Objects.requireNonNull(clientSession, "No client session provided"); + Objects.requireNonNull(userInteraction, "No user interaction instance configured"); + Iterable<String> passwords = () -> new InteractivePasswordIdentityProvider(clientSession, userInteraction, prompt); + return () -> passwords; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/1e8ac309/sshd-contrib/src/main/java/org/apache/sshd/server/scp/SimpleAccessControlScpEventListener.java ---------------------------------------------------------------------- diff --git a/sshd-contrib/src/main/java/org/apache/sshd/server/scp/SimpleAccessControlScpEventListener.java b/sshd-contrib/src/main/java/org/apache/sshd/server/scp/SimpleAccessControlScpEventListener.java index 527b48d..9139bcb 100644 --- a/sshd-contrib/src/main/java/org/apache/sshd/server/scp/SimpleAccessControlScpEventListener.java +++ b/sshd-contrib/src/main/java/org/apache/sshd/server/scp/SimpleAccessControlScpEventListener.java @@ -30,6 +30,7 @@ import org.apache.sshd.common.scp.AbstractScpTransferEventListenerAdapter; /** * Provides a simple access control by making a distinction between methods * that upload data and ones that download it + * * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> */ public abstract class SimpleAccessControlScpEventListener extends AbstractScpTransferEventListenerAdapter { http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/1e8ac309/sshd-contrib/src/main/java/org/apache/sshd/server/session/proxyprotocol/ProxyProtocolAcceptor.java ---------------------------------------------------------------------- diff --git a/sshd-contrib/src/main/java/org/apache/sshd/server/session/proxyprotocol/ProxyProtocolAcceptor.java b/sshd-contrib/src/main/java/org/apache/sshd/server/session/proxyprotocol/ProxyProtocolAcceptor.java index 29696aa..11eb561 100644 --- a/sshd-contrib/src/main/java/org/apache/sshd/server/session/proxyprotocol/ProxyProtocolAcceptor.java +++ b/sshd-contrib/src/main/java/org/apache/sshd/server/session/proxyprotocol/ProxyProtocolAcceptor.java @@ -34,6 +34,7 @@ import org.apache.sshd.server.session.ServerSession; /** * A working prototype to support PROXY protocol as described in * <A HREF="http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt">HAProxy Documentation</A>. + * * @see <A HREF="https://gist.github.com/codingtony/a8684c9ffa08ad56899f94d3b6c2a040">Tony Bussieres's</A> contribution * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> */ http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/1e8ac309/sshd-contrib/src/main/java/org/apache/sshd/server/subsystem/sftp/SimpleAccessControlSftpEventListener.java ---------------------------------------------------------------------- diff --git a/sshd-contrib/src/main/java/org/apache/sshd/server/subsystem/sftp/SimpleAccessControlSftpEventListener.java b/sshd-contrib/src/main/java/org/apache/sshd/server/subsystem/sftp/SimpleAccessControlSftpEventListener.java index 1a80bd0..f1fa81f 100644 --- a/sshd-contrib/src/main/java/org/apache/sshd/server/subsystem/sftp/SimpleAccessControlSftpEventListener.java +++ b/sshd-contrib/src/main/java/org/apache/sshd/server/subsystem/sftp/SimpleAccessControlSftpEventListener.java @@ -34,6 +34,7 @@ import org.apache.sshd.server.session.ServerSession; /** * Provides a simple access control by making a distinction between methods * that provide information - including reading data - and those that modify it + * * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> */ public abstract class SimpleAccessControlSftpEventListener extends AbstractSftpEventListenerAdapter { http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/1e8ac309/sshd-contrib/src/test/java/org/apache/sshd/client/auth/password/InteractivePasswordIdentityProviderTest.java ---------------------------------------------------------------------- diff --git a/sshd-contrib/src/test/java/org/apache/sshd/client/auth/password/InteractivePasswordIdentityProviderTest.java b/sshd-contrib/src/test/java/org/apache/sshd/client/auth/password/InteractivePasswordIdentityProviderTest.java new file mode 100644 index 0000000..af52e7a --- /dev/null +++ b/sshd-contrib/src/test/java/org/apache/sshd/client/auth/password/InteractivePasswordIdentityProviderTest.java @@ -0,0 +1,99 @@ +/* + * 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.client.auth.password; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.sshd.client.auth.keyboard.UserInteraction; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.util.test.BaseTestSupport; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; +import org.mockito.Matchers; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** + * TODO Add javadoc + * + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class InteractivePasswordIdentityProviderTest extends BaseTestSupport { + public InteractivePasswordIdentityProviderTest() { + super(); + } + + @Test + public void testPasswordEnumerations() { + List<String> expected = Arrays.asList(getClass().getSimpleName(), getClass().getPackage().getName(), getCurrentTestName()); + ClientSession session = Mockito.mock(ClientSession.class); + AtomicInteger passwordIndex = new AtomicInteger(0); + String prompt = getCurrentTestName(); + UserInteraction userInteraction = Mockito.mock(UserInteraction.class); + Mockito.when(userInteraction.isInteractionAllowed(Matchers.any(ClientSession.class))).thenReturn(Boolean.TRUE); + Mockito.when(userInteraction.getUpdatedPassword(Matchers.any(ClientSession.class), Matchers.anyString(), Matchers.anyString())) + .thenAnswer(new Answer<String>() { + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + assertSame("Mismatched session instance at index=" + passwordIndex, session, args[0]); + assertSame("Mismatched prompt instance at index=" + passwordIndex, prompt, args[1]); + + int index = passwordIndex.getAndIncrement(); + if (index < expected.size()) { + return expected.get(index); + } + assertEquals("Mismatched last call index", expected.size(), index); + return null; + } + }); + Mockito.when(session.getUserInteraction()).thenReturn(userInteraction); + + PasswordIdentityProvider provider = InteractivePasswordIdentityProvider.providerOf(session, prompt); + Iterable<String> passwords = provider.loadPasswords(); + int expIndex = 0; + for (String actValue : passwords) { + String expValue = expected.get(expIndex); + assertSame("Mismatched password provided at index=" + expIndex, expValue, actValue); + expIndex++; + } + + assertEquals("Not all passwords exhausted", expected.size() + 1, passwordIndex.get()); + assertEquals("Mismatched retrieved passwords count", expIndex, expected.size()); + } + + @Test + public void testInteractionAllowedConsultation() { + ClientSession session = Mockito.mock(ClientSession.class); + UserInteraction userInteraction = Mockito.mock(UserInteraction.class); + Mockito.when(userInteraction.isInteractionAllowed(Matchers.any(ClientSession.class))).thenReturn(Boolean.FALSE); + Mockito.when(userInteraction.getUpdatedPassword(Matchers.any(ClientSession.class), Matchers.anyString(), Matchers.anyString())) + .thenThrow(new UnsupportedOperationException("Unexpected call")); + PasswordIdentityProvider provider = InteractivePasswordIdentityProvider.providerOf(session, userInteraction, getCurrentTestName()); + Iterable<String> passwords = provider.loadPasswords(); + for (String p : passwords) { + fail("Unexpected password: " + p); + } + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/1e8ac309/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java index a641243..56f5e1d 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java @@ -59,9 +59,11 @@ public interface UserInteraction { /** * * @param session The {@link ClientSession} - * @return {@code true} if user interaction allowed for this session + * @return {@code true} if user interaction allowed for this session (default) */ - boolean isInteractionAllowed(ClientSession session); + default boolean isInteractionAllowed(ClientSession session) { + return true; + } /** * Called if the server sent any extra information beyond the standard