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

Reply via email to