Skip to content

Commit 3601209

Browse files
Ayush PatelAyush Patel
authored andcommitted
Add pkey, disabled_algorithms, transport_factory and auth_strategy parameters to paramiko.client.connect
1 parent d2f62cb commit 3601209

File tree

5 files changed

+132
-4
lines changed

5 files changed

+132
-4
lines changed

dlt/common/configuration/specs/sftp_credentials.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
1-
from typing import Any, Dict, Optional, Annotated
1+
from typing import Any, Dict, Optional, Annotated, TYPE_CHECKING
22

3-
from dlt.common.typing import TSecretStrValue, DictStrAny, SocketLike
3+
from dlt.common.typing import TSecretStrValue, SocketLike, SFTPTransportFactory, DictStrListStr
44
from dlt.common.configuration.specs.base_configuration import (
55
CredentialsConfiguration,
66
configspec,
77
NotResolved,
88
)
99

10+
if TYPE_CHECKING:
11+
try:
12+
from paramiko import PKey
13+
from paramiko.auth_strategy import AuthStrategy
14+
except ImportError:
15+
PKey = Any # type: ignore[misc, assignment]
16+
AuthStrategy = Any # type: ignore[misc, assignment]
17+
else:
18+
PKey = Any
19+
AuthStrategy = Any
20+
1021

1122
@configspec
1223
class SFTPCredentials(CredentialsConfiguration):
@@ -32,6 +43,8 @@ class SFTPCredentials(CredentialsConfiguration):
3243
sftp_port: Optional[int] = 22
3344
sftp_username: Optional[str] = None
3445
sftp_password: Optional[TSecretStrValue] = None
46+
# Runtime-only pkey; cannot be loaded from env var, skip configspec.
47+
sftp_pkey: Annotated[Optional[PKey], NotResolved()] = None
3548
sftp_key_filename: Optional[str] = None
3649
sftp_key_passphrase: Optional[TSecretStrValue] = None
3750
sftp_timeout: Optional[float] = None
@@ -48,6 +61,10 @@ class SFTPCredentials(CredentialsConfiguration):
4861
sftp_gss_deleg_creds: Optional[bool] = True
4962
sftp_gss_host: Optional[str] = None
5063
sftp_gss_trust_dns: Optional[bool] = True
64+
# Runtime-only vars below; cannot be loaded from env var, skip configspec.
65+
sftp_disabled_algorithms: Annotated[Optional[DictStrListStr], NotResolved()] = None
66+
sftp_transport_factory: Annotated[Optional[SFTPTransportFactory], NotResolved()] = None
67+
sftp_auth_strategy: Annotated[Optional[AuthStrategy], NotResolved()] = None
5168

5269
def to_fsspec_credentials(self) -> Dict[str, Any]:
5370
"""Return a dict that can be passed to fsspec SFTP/SSHClient.connect method."""
@@ -56,6 +73,7 @@ def to_fsspec_credentials(self) -> Dict[str, Any]:
5673
"port": self.sftp_port,
5774
"username": self.sftp_username,
5875
"password": self.sftp_password,
76+
"pkey": self.sftp_pkey,
5977
"key_filename": self.sftp_key_filename,
6078
"passphrase": self.sftp_key_passphrase,
6179
"timeout": self.sftp_timeout,
@@ -71,6 +89,9 @@ def to_fsspec_credentials(self) -> Dict[str, Any]:
7189
"gss_deleg_creds": self.sftp_gss_deleg_creds,
7290
"gss_host": self.sftp_gss_host,
7391
"gss_trust_dns": self.sftp_gss_trust_dns,
92+
"disabled_algorithms": self.sftp_disabled_algorithms,
93+
"transport_factory": self.sftp_transport_factory,
94+
"auth_strategy": self.sftp_auth_strategy,
7495
}
7596

7697
return credentials

dlt/common/typing.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from datetime import datetime, date # noqa: I251
33
import inspect
44
import os
5+
import socket
56
from re import Pattern as _REPattern
67
from types import FunctionType
78
from typing import (
@@ -65,6 +66,15 @@
6566

6667
typingGenericAlias: Tuple[Any, ...] = (_GenericAlias, _SpecialGenericAlias, GenericAlias)
6768

69+
if TYPE_CHECKING:
70+
try:
71+
from paramiko import Transport
72+
except ImportError:
73+
Transport = Any # type: ignore[misc, assignment]
74+
else:
75+
Transport = Any
76+
77+
SFTPTransportFactory: TypeAlias = Callable[[socket.socket], Transport]
6878

6979
from dlt.common.pendulum import timedelta, pendulum
7080

@@ -89,6 +99,7 @@
8999
DictStrAny: TypeAlias = Dict[str, Any]
90100
DictStrStr: TypeAlias = Dict[str, str]
91101
DictStrOptionalStr: TypeAlias = Dict[str, Optional[str]]
102+
DictStrListStr: TypeAlias = Dict[str, List[str]]
92103
StrAny: TypeAlias = Mapping[str, Any] # immutable, covariant entity
93104
StrStr: TypeAlias = Mapping[str, str] # immutable, covariant entity
94105
StrStrStr: TypeAlias = Mapping[str, Mapping[str, str]] # immutable, covariant entity

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ dev = [
250250
"pydoclint>=0.6.5,<0.7",
251251
# limit the pyarrow version not to test on too new one
252252
"pyarrow>=16.0.0,<19.0.0",
253+
"types-paramiko>=3.5.0.20250516",
253254
]
254255
sources = [
255256
"connectorx>=0.3.3 ; python_version >= '3.9'",

tests/load/filesystem_sftp/test_filesystem_sftp.py

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import fsspec
44
import socket
55
import dlt
6-
from dlt.common.configuration.specs import SFTPCredentials
76

87
from dlt.common.json import json
98
from dlt.common.configuration.inject import with_config
@@ -12,6 +11,10 @@
1211

1312
from tests.load.utils import ALL_FILESYSTEM_DRIVERS
1413

14+
from paramiko.auth_strategy import Password
15+
from paramiko import RSAKey, Transport
16+
from paramiko.ssh_exception import SSHException
17+
1518
if "sftp" not in ALL_FILESYSTEM_DRIVERS:
1619
pytest.skip("sftp filesystem driver not configured", allow_module_level=True)
1720

@@ -126,7 +129,17 @@ def states():
126129
assert sorted(result_states) == sorted(expected_states)
127130

128131

129-
def run_sftp_auth(user, password=None, key=None, passphrase=None, sock=None):
132+
def run_sftp_auth(
133+
user,
134+
password=None,
135+
pkey=None,
136+
key=None,
137+
passphrase=None,
138+
sock=None,
139+
disabled_algorithms=None,
140+
transport_factory=None,
141+
auth_strategy=None,
142+
):
130143
env_vars = {
131144
"SOURCES__FILESYSTEM__BUCKET_URL": "sftp://localhost",
132145
"SOURCES__FILESYSTEM__CREDENTIALS__SFTP_PORT": "2222",
@@ -144,6 +157,14 @@ def run_sftp_auth(user, password=None, key=None, passphrase=None, sock=None):
144157

145158
config = get_config()
146159

160+
if disabled_algorithms:
161+
config.credentials.sftp_disabled_algorithms = disabled_algorithms # type: ignore[union-attr]
162+
if transport_factory:
163+
config.credentials.sftp_transport_factory = transport_factory # type: ignore[union-attr]
164+
if auth_strategy:
165+
config.credentials.sftp_auth_strategy = auth_strategy # type: ignore[union-attr]
166+
if pkey:
167+
config.credentials.sftp_pkey = pkey # type: ignore[union-attr]
147168
if sock:
148169
config.credentials.sftp_sock = sock # type: ignore[union-attr]
149170

@@ -163,6 +184,17 @@ def test_filesystem_sftp_auth_private_key_protected():
163184
run_sftp_auth("bobby", key=get_key_path("bobby"), passphrase="passphrase123")
164185

165186

187+
def test_filesystem_sftp_auth_pkey():
188+
run_sftp_auth("foo", pkey=RSAKey.from_private_key_file(get_key_path("foo")))
189+
190+
191+
def test_filesystem_sftp_pkey_auth_pkey_protected():
192+
run_sftp_auth(
193+
"bobby",
194+
pkey=RSAKey.from_private_key_file(filename=get_key_path("bobby"), password="passphrase123"),
195+
)
196+
197+
166198
# Test requires - ssh_agent with user's bobby key loaded. The commands and file names required are:
167199
# eval "$(ssh-agent -s)"
168200
# cp /path/to/tests/load/filesystem_sftp/bobby_rsa* ~/.ssh/id_rsa
@@ -190,3 +222,52 @@ def test_filesystem_sftp_with_socket():
190222
sock.close()
191223
with pytest.raises(OSError):
192224
run_sftp_auth("billy", key=get_key_path("billy"), sock=sock)
225+
226+
227+
class TaggedTransport(Transport):
228+
"""A Transport class that tags itself so we can detect it in tests."""
229+
230+
def __init__(self, sock, **kwargs):
231+
super().__init__(sock, **kwargs)
232+
# Add any custom state or markers you like:
233+
self.factory_tag = "used-tagged-transport"
234+
235+
236+
def test_filesystem_sftp_with_tagged_transport():
237+
created = []
238+
239+
def factory(sock, **kwargs):
240+
t = TaggedTransport(sock, **kwargs)
241+
created.append(t)
242+
return t
243+
244+
run_sftp_auth("foo", key=get_key_path("foo"), transport_factory=factory)
245+
246+
# Verify it was used
247+
assert len(created) == 1, "Custom transport factory never ran"
248+
transport = created[0]
249+
assert isinstance(transport, TaggedTransport)
250+
assert transport.factory_tag == "used-tagged-transport"
251+
252+
# And ensure it's still functional
253+
sftp = transport.open_sftp_client()
254+
assert hasattr(sftp, "listdir")
255+
sftp.close()
256+
257+
258+
def test_filesystem_sftp_disabled_algorithms():
259+
# we know foo’s server uses rsa keys
260+
with pytest.raises(SSHException):
261+
run_sftp_auth(
262+
"foo",
263+
key=get_key_path("foo"),
264+
disabled_algorithms={"pubkeys": ["ssh-rsa", "rsa-sha2-256", "rsa-sha2-512"]},
265+
)
266+
267+
268+
def test_filesystem_sftp_auth_strategy():
269+
# Verify that passing an alternate auth_strategy makes it through config.
270+
run_sftp_auth(
271+
"foo",
272+
auth_strategy=Password("foo", lambda: "pass"),
273+
)

uv.lock

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)