carletes / mock-ssh-server

Python mock SSH server for testing purposes
MIT License
56 stars 32 forks source link

Adds password support #25

Open ut-adamc opened 2 years ago

ut-adamc commented 2 years ago

Useful for those of us who sometimes have password credentials.

I don't know how close this would be to a viable solution, but I wanted to run it by you and get feedback. It works in local testing. It relies on changing the data structure for users to accommodate having more than one kind of credential. See the doc string for UserData in mockssh.server.

Spitfire1900 commented 1 year ago

For the purposes of using this with a mock server password credentials seems irrelevant; instead create a set of credentials just for running the tests against, through either or fixture or as a command you invoke via tox prior to running pytest.

b3n4kh commented 7 months ago

Is there a reason as not to merge this? I would be really interested in this feature as well!

I just checked and saw the tests failing right now but the fix for them to pass again is very straight forward:

diff --git a/mockssh/test_server.py b/mockssh/test_server.py
index 15108b7..1740f53 100644
--- a/mockssh/test_server.py
+++ b/mockssh/test_server.py
@@ -104,6 +104,8 @@ def test_overwrite_handler(server: Server, monkeypatch: MonkeyPatch):
             if username == "foo" and password == "bar":
                 return paramiko.AUTH_SUCCESSFUL
             return paramiko.AUTH_FAILED
+        def get_allowed_auths(self, username):
+            return "password"
     monkeypatch.setattr(server, 'handler_cls', MyHandler)
     with paramiko.SSHClient() as client:
         client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
Spitfire1900 commented 4 months ago

@ut-adamc , I don't see any stand-out issues with the implementation. Can you please update README.rst to include a password example?

Spitfire1900 commented 2 months ago

@ut-adamc please add documentation for this on the README, the following test case fails:

import os
import tempfile

from pytest import yield_fixture

import mockssh

@yield_fixture()
def server():

    users = {
        "type": "password",
        "password": "mypassword",
    }
    with mockssh.Server(users) as s:
        yield s

def test_ssh_session(server):
    for uid in server.users:
        with server.client(uid) as c:
            _, stdout, _ = c.exec_command("ls /")
            assert stdout.read()

def test_sftp_session(server):
    for uid in server.users:
        target_dir = tempfile.mkdtemp()
        target_fname = os.path.join(target_dir, "foo")
        assert not os.access(target_fname, os.F_OK)

        with server.client(uid) as c:
            sftp = c.open_sftp()
            sftp.put(__file__, target_fname, confirm=True)
            assert os.access(target_fname, os.F_OK)

outputs:

|@ pytest
================================================================================================================================================== test session starts ===================================================================================================================================================
platform linux -- Python 3.10.12, pytest-8.3.2, pluggy-1.5.0
rootdir: /home/kyle/Repos/github/mock-ssh-server-test
collected 2 items                                                                                                                                                                                                                                                                                                        

tests/mock_test.py EE                                                                                                                                                                                                                                                                                              [100%]

========================================================================================================================================================= ERRORS =========================================================================================================================================================
___________________________________________________________________________________________________________________________________________ ERROR at setup of test_ssh_session ___________________________________________________________________________________________________________________________________________

    @yield_fixture()
    def server():

        users = {
            "type": "password",
            "password": "mypassword",
            # "sample-user": "mypassword",
        }
>       with mockssh.Server(users) as s:

tests/mock_test.py:17: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.10/site-packages/mockssh/server.py:227: in __init__
    user_data = UserData(credential)
.venv/lib/python3.10/site-packages/mockssh/server.py:77: in __init__
    self.public_key = self.calculate_public_key(
.venv/lib/python3.10/site-packages/mockssh/server.py:95: in calculate_public_key
    public_key = paramiko.RSAKey.from_private_key_file(
.venv/lib/python3.10/site-packages/paramiko/pkey.py:435: in from_private_key_file
    key = cls(filename=filename, password=password)
.venv/lib/python3.10/site-packages/paramiko/rsakey.py:64: in __init__
    self._from_private_key_file(filename, password)
.venv/lib/python3.10/site-packages/paramiko/rsakey.py:196: in _from_private_key_file
    data = self._read_private_key_file("RSA", filename, password)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <[AttributeError("'NoneType' object has no attribute 'key_size'") raised in repr()] RSAKey object at 0x7f0d4a574250>, tag = 'RSA', filename = 'password', password = None

    def _read_private_key_file(self, tag, filename, password=None):
        """
        Read an SSH2-format private key file, looking for a string of the type
        ``"BEGIN xxx PRIVATE KEY"`` for some ``xxx``, base64-decode the text we
        find, and return it as a string.  If the private key is encrypted and
        ``password`` is not ``None``, the given password will be used to
        decrypt the key (otherwise `.PasswordRequiredException` is thrown).

        :param str tag: ``"RSA"`` or ``"DSA"``, the tag used to mark the
            data block.
        :param str filename: name of the file to read.
        :param str password:
            an optional password to use to decrypt the key file, if it's
            encrypted.
        :return: the `bytes` that make up the private key.

        :raises: ``IOError`` -- if there was an error reading the file.
        :raises: `.PasswordRequiredException` -- if the private key file is
            encrypted, and ``password`` is ``None``.
        :raises: `.SSHException` -- if the key file is invalid.
        """
>       with open(filename, "r") as f:
E       FileNotFoundError: [Errno 2] No such file or directory: 'password'

.venv/lib/python3.10/site-packages/paramiko/pkey.py:508: FileNotFoundError
__________________________________________________________________________________________________________________________________________ ERROR at setup of test_sftp_session ___________________________________________________________________________________________________________________________________________

    @yield_fixture()
    def server():

        users = {
            "type": "password",
            "password": "mypassword",
            # "sample-user": "mypassword",
        }
>       with mockssh.Server(users) as s:

tests/mock_test.py:17: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.10/site-packages/mockssh/server.py:227: in __init__
    user_data = UserData(credential)
.venv/lib/python3.10/site-packages/mockssh/server.py:77: in __init__
    self.public_key = self.calculate_public_key(
.venv/lib/python3.10/site-packages/mockssh/server.py:95: in calculate_public_key
    public_key = paramiko.RSAKey.from_private_key_file(
.venv/lib/python3.10/site-packages/paramiko/pkey.py:435: in from_private_key_file
    key = cls(filename=filename, password=password)
.venv/lib/python3.10/site-packages/paramiko/rsakey.py:64: in __init__
    self._from_private_key_file(filename, password)
.venv/lib/python3.10/site-packages/paramiko/rsakey.py:196: in _from_private_key_file
    data = self._read_private_key_file("RSA", filename, password)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <[AttributeError("'NoneType' object has no attribute 'key_size'") raised in repr()] RSAKey object at 0x7f0d4a5ca530>, tag = 'RSA', filename = 'password', password = None

    def _read_private_key_file(self, tag, filename, password=None):
        """
        Read an SSH2-format private key file, looking for a string of the type
        ``"BEGIN xxx PRIVATE KEY"`` for some ``xxx``, base64-decode the text we
        find, and return it as a string.  If the private key is encrypted and
        ``password`` is not ``None``, the given password will be used to
        decrypt the key (otherwise `.PasswordRequiredException` is thrown).

        :param str tag: ``"RSA"`` or ``"DSA"``, the tag used to mark the
            data block.
        :param str filename: name of the file to read.
        :param str password:
            an optional password to use to decrypt the key file, if it's
            encrypted.
        :return: the `bytes` that make up the private key.

        :raises: ``IOError`` -- if there was an error reading the file.
        :raises: `.PasswordRequiredException` -- if the private key file is
            encrypted, and ``password`` is ``None``.
        :raises: `.SSHException` -- if the key file is invalid.
        """
>       with open(filename, "r") as f:
E       FileNotFoundError: [Errno 2] No such file or directory: 'password'

.venv/lib/python3.10/site-packages/paramiko/pkey.py:508: FileNotFoundError
==================================================================================================================================================== warnings summary ====================================================================================================================================================
tests/mock_test.py:9
  /home/kyle/Repos/github/mock-ssh-server-test/tests/mock_test.py:9: PytestDeprecationWarning: @pytest.yield_fixture is deprecated.
  Use @pytest.fixture instead; they are the same.
    @yield_fixture()

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================================================================================================================================ short test summary info =================================================================================================================================================
ERROR tests/mock_test.py::test_ssh_session - FileNotFoundError: [Errno 2] No such file or directory: 'password'
ERROR tests/mock_test.py::test_sftp_session - FileNotFoundError: [Errno 2] No such file or directory: 'password'
============================================================================================================================================== 1 warning, 2 errors in 0.11s ==============================================================================================================================================
Return 1
Spitfire1900 commented 2 months ago

Also test_overwrite_handler fails