diff options
-rw-r--r-- | paramiko/__init__.py | 8 | ||||
-rw-r--r-- | paramiko/auth_strategy.py | 29 | ||||
-rw-r--r-- | sites/docs/api/auth.rst | 8 | ||||
-rw-r--r-- | sites/docs/index.rst | 1 | ||||
-rw-r--r-- | tests/auth.py | 42 | ||||
-rw-r--r-- | tests/test_util.py | 6 |
6 files changed, 88 insertions, 6 deletions
diff --git a/paramiko/__init__.py b/paramiko/__init__.py index c93a405d..4bb261e0 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -32,7 +32,12 @@ from paramiko.client import ( WarningPolicy, ) from paramiko.auth_handler import AuthHandler -from paramiko.auth_strategy import AuthStrategy, AuthResult +from paramiko.auth_strategy import ( + AuthStrategy, + AuthResult, + AuthSource, + NoneAuth, +) from paramiko.ssh_gss import GSSAuth, GSS_AUTH_AVAILABLE, GSS_EXCEPTIONS from paramiko.channel import ( Channel, @@ -106,6 +111,7 @@ key_classes = [DSSKey, RSAKey, Ed25519Key, ECDSAKey] __author__ = "Jeff Forcier <jeff@bitprophet.org>" __license__ = "GNU Lesser General Public License (LGPL)" +# TODO 4.0: remove this, jeez __all__ = [ "Agent", "AgentKey", diff --git a/paramiko/auth_strategy.py b/paramiko/auth_strategy.py index 7ae657df..bbb52141 100644 --- a/paramiko/auth_strategy.py +++ b/paramiko/auth_strategy.py @@ -1,3 +1,10 @@ +""" +Modern, adaptable authentication machinery. + +Replaces certain parts of `.SSHClient`. For a concrete implementation, see the +``OpenSSHAuthStrategy`` class in `Fabric <https://fabfile.org>`_. +""" + from collections import namedtuple from .agent import AgentKey @@ -34,7 +41,7 @@ class AuthSource: raise NotImplementedError -def NoneAuth(AuthSource): +class NoneAuth(AuthSource): """ Auth type "none", ie https://www.rfc-editor.org/rfc/rfc4252#section-5.2 . """ @@ -54,7 +61,7 @@ class Password(AuthSource): If you already know the password at instantiation time, you should simply use something like ``lambda: "my literal"`` (for a literal, but - also, shame on you!) or ``lambda: variable_name (for something stored + also, shame on you!) or ``lambda: variable_name`` (for something stored in a variable). """ @@ -169,6 +176,19 @@ class AuthResult(list): return value from the relevant `.Transport` method, or an exception object). + .. note:: + Transport auth method results are always themselves a ``list`` of "next + allowable authentication methods". + + In the simple case of "you just authenticated successfully", it's an + empty list; if your auth was rejected but you're allowed to try again, + it will be a list of string method names like ``pubkey`` or + ``password``. + + The ``__str__`` of this class represents the empty-list scenario as the + word ``success``, which should make reading the result of an + authentication session more obvious to humans. + Instances also have a `strategy` attribute referencing the `AuthStrategy` which was attempted. """ @@ -181,7 +201,10 @@ class AuthResult(list): # NOTE: meaningfully distinct from __repr__, which still wants to use # superclass' implementation. # TODO: go hog wild, use rich.Table? how is that on degraded term's? - return "\n".join(f"{x.source} -> {x.result}" for x in self) + # TODO: test this lol + return "\n".join( + f"{x.source} -> {x.result or 'success'}" for x in self + ) # TODO 4.0: descend from SSHException or even just Exception diff --git a/sites/docs/api/auth.rst b/sites/docs/api/auth.rst new file mode 100644 index 00000000..b6bce36c --- /dev/null +++ b/sites/docs/api/auth.rst @@ -0,0 +1,8 @@ +Authentication modules +====================== + +.. automodule:: paramiko.auth_strategy + :member-order: bysource + +.. automodule:: paramiko.auth_handler + :member-order: bysource diff --git a/sites/docs/index.rst b/sites/docs/index.rst index 87265d95..675fe596 100644 --- a/sites/docs/index.rst +++ b/sites/docs/index.rst @@ -47,6 +47,7 @@ Authentication & keys --------------------- .. toctree:: + api/auth api/agent api/hostkeys api/keys diff --git a/tests/auth.py b/tests/auth.py index 23d578d0..eacaf210 100644 --- a/tests/auth.py +++ b/tests/auth.py @@ -4,12 +4,16 @@ Tests focusing primarily on the authentication step. Thus, they concern AuthHandler and AuthStrategy, with a side of Transport. """ +from unittest.mock import Mock + from pytest import raises from paramiko import ( AuthenticationException, + AuthSource, BadAuthenticationType, DSSKey, + NoneAuth, PKey, RSAKey, SSHException, @@ -278,7 +282,6 @@ class SHA2SignaturePubkeys: privkey = RSAKey.from_private_key_file(_support("rsa.key")) with server( pubkeys=[privkey], - # TODO: why is this passing without a username? connect=dict(pkey=privkey), init=dict( disabled_algorithms=dict(pubkeys=["ssh-rsa", "rsa-sha2-256"]) @@ -311,3 +314,40 @@ class SHA2SignaturePubkeys: ) as (tc, ts): assert tc.is_authenticated() assert tc._agreed_pubkey_algorithm == "rsa-sha2-256" + + +class AuthSource_: + class base_class: + def init_requires_and_saves_username(self): + with raises(TypeError): + AuthSource() + assert AuthSource(username="foo").username == "foo" + + def dunder_repr_delegates_to_helper(self): + source = AuthSource("foo") + source._repr = Mock(wraps=lambda: "whatever") + repr(source) + source._repr.assert_called_once_with() + + def repr_helper_prints_basic_kv_pairs(self): + assert repr(AuthSource("foo")) == "AuthSource()" + assert ( + AuthSource("foo")._repr(bar="open") == "AuthSource(bar='open')" + ) + + def authenticate_takes_transport_and_is_abstract(self): + # TODO: this test kinda just goes away once we're typed? + with raises(TypeError): + AuthSource("foo").authenticate() + with raises(NotImplementedError): + AuthSource("foo").authenticate(None) + + class NoneAuth_: + def authenticate_auths_none(self): + trans = Mock() + NoneAuth("foo").authenticate(trans) + trans.auth_none.assert_called_once_with("foo") + + +class AuthStrategy_: + pass diff --git a/tests/test_util.py b/tests/test_util.py index ec03846b..4a8cf972 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -49,6 +49,9 @@ class UtilTest(unittest.TestCase): "Agent", "AgentKey", "AuthenticationException", + "AuthHandler", + "AuthSource", + "AuthStrategy", "AutoAddPolicy", "BadAuthenticationType", "BufferedFile", @@ -60,6 +63,7 @@ class UtilTest(unittest.TestCase): "HostKeys", "Message", "MissingHostKeyPolicy", + "NoneAuth", "PasswordRequiredException", "RSAKey", "RejectPolicy", @@ -82,7 +86,7 @@ class UtilTest(unittest.TestCase): "WarningPolicy", "util", ): - assert name in paramiko.__all__ + assert name in dir(paramiko) def test_generate_key_bytes(self): key_bytes = paramiko.util.generate_key_bytes( |