summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJeff Forcier <jeff@bitprophet.org>2023-05-18 16:50:42 -0400
committerJeff Forcier <jeff@bitprophet.org>2023-05-22 12:22:05 -0400
commitce454580c03997d9b5873fe31e1ce27d1c64cf12 (patch)
treea0e2234b832b0ddda194da03c92b01d0f5ce6f2e
parent31a5a30cf50d3971693417ccfd1e15c1f8302147 (diff)
Start testing AuthStrategy
Plus! - Document AuthStrategy and AuthHandler modules (latter never had docs? lol) - Minor tweaks to these modules' docstrings etc - Stop comparing to __all__ in __init__.py, ugh
-rw-r--r--paramiko/__init__.py8
-rw-r--r--paramiko/auth_strategy.py29
-rw-r--r--sites/docs/api/auth.rst8
-rw-r--r--sites/docs/index.rst1
-rw-r--r--tests/auth.py42
-rw-r--r--tests/test_util.py6
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(