Skip to content

Commit ff801f8

Browse files
committed
feat: added a set-auth command to set authentication details for a host in config, allowing users to store password/passphrases for specific hosts/ssh-keys in system's keyring
1 parent 86f8655 commit ff801f8

6 files changed

Lines changed: 66 additions & 12 deletions

File tree

src/sshsync/cli.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,11 @@ def sync():
168168
print_error(e, True)
169169

170170

171-
@app.command(help="Set authentication method for one or more host")
171+
@app.command(help="Set authentication method for one or more unconfigured hosts.")
172172
def set_auth():
173+
"""
174+
Set authentication method for one or more unconfigured hosts.
175+
"""
173176
try:
174177
config = Config()
175178
hosts = config.get_unconfigured_hosts()
@@ -253,7 +256,9 @@ def push(
253256
else (
254257
config.get_hosts_by_group(group, regex)
255258
if group
256-
else [host_obj] if host_obj is not None else []
259+
else [host_obj]
260+
if host_obj is not None
261+
else []
257262
)
258263
)
259264

@@ -348,7 +353,9 @@ def pull(
348353
else (
349354
config.get_hosts_by_group(group, regex)
350355
if group
351-
else [host_obj] if host_obj is not None else []
356+
else [host_obj]
357+
if host_obj is not None
358+
else []
352359
)
353360
)
354361

src/sshsync/client.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,9 @@ async def _execute_command(
185185
conn_kwargs["client_keys"] = [host.identity_file]
186186
else:
187187
host_pass = self.get_host_pass(host.alias)
188-
if host_pass is not None:
188+
if host_pass:
189189
host_auth = self.config.config.host_auth.get(host.alias, None)
190-
if host_auth is not None:
190+
if host_auth:
191191
if host_auth.auth == "key":
192192
conn_kwargs["client_keys"] = [host.identity_file]
193193
conn_kwargs["passphrase"] = host_pass
@@ -287,9 +287,9 @@ async def _push(
287287
conn_kwargs["client_keys"] = [host.identity_file]
288288
else:
289289
host_pass = self.get_host_pass(host.alias)
290-
if host_pass is not None:
290+
if host_pass:
291291
host_auth = self.config.config.host_auth.get(host.alias, None)
292-
if host_auth is not None:
292+
if host_auth:
293293
if host_auth.auth == "key":
294294
conn_kwargs["client_keys"] = [host.identity_file]
295295
conn_kwargs["passphrase"] = host_pass
@@ -360,9 +360,9 @@ async def _pull(
360360
conn_kwargs["client_keys"] = [host.identity_file]
361361
else:
362362
host_pass = self.get_host_pass(host.alias)
363-
if host_pass is not None:
363+
if host_pass:
364364
host_auth = self.config.config.host_auth.get(host.alias, None)
365-
if host_auth is not None:
365+
if host_auth:
366366
if host_auth.auth == "key":
367367
conn_kwargs["client_keys"] = [host.identity_file]
368368
conn_kwargs["passphrase"] = host_pass

src/sshsync/config.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,21 @@ def __init__(self) -> None:
4747
self.configure_ssh_hosts()
4848

4949
def configured_hosts(self):
50+
"""
51+
Return a list of all configured hosts except the default host.
52+
53+
Returns:
54+
list[Host]: List of configured Host objects excluding the default.
55+
"""
5056
return list(filter(lambda x: x.alias != "default", self.hosts))
5157

5258
def _default_config(self) -> YamlConfig:
59+
"""
60+
Return a default YamlConfig object with empty groups and host_auth.
61+
62+
Returns:
63+
YamlConfig: Default configuration object.
64+
"""
5365
return YamlConfig(groups=dict(), host_auth=dict())
5466

5567
def ensure_config_directory_exists(self) -> None:
@@ -62,6 +74,16 @@ def ensure_config_directory_exists(self) -> None:
6274
def _resolve_ssh_value(
6375
self, value: str | int | list[str | int] | None, default: str | int = ""
6476
) -> str | int:
77+
"""
78+
Resolve a value from SSH config, handling lists and defaults.
79+
80+
Args:
81+
value (str | int | list[str | int] | None): The value to resolve.
82+
default (str | int): Default value if input is None or empty.
83+
84+
Returns:
85+
str | int: The resolved value.
86+
"""
6587
if isinstance(value, list):
6688
return value[0] if value else default
6789
return value or default
@@ -243,6 +265,12 @@ def get_ungrouped_hosts(self) -> list[str]:
243265
]
244266

245267
def get_unconfigured_hosts(self) -> list[dict[str, str]]:
268+
"""
269+
Get a list of hosts that do not have authentication configured.
270+
271+
Returns:
272+
list[dict[str, str]]: List of dicts with alias and identity_file for unconfigured hosts.
273+
"""
246274
return [
247275
{"alias": host.alias, "identity_file": host.identity_file}
248276
for host in self.hosts
@@ -266,6 +294,12 @@ def assign_groups_to_hosts(self, host_group_mapping: dict[str, list[str]]) -> No
266294
self._save_yaml()
267295

268296
def save_host_auth(self, host_auth_details: dict[str, HostAuth]) -> None:
297+
"""
298+
Save authentication details for hosts and update the YAML config file.
299+
300+
Args:
301+
host_auth_details (dict[str, HostAuth]): Mapping of host aliases to HostAuth objects.
302+
"""
269303
self.config.host_auth = host_auth_details
270304
self._save_yaml()
271305

src/sshsync/logging.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66

77
def get_log_path() -> Path:
8+
"""Return the log directory path, creating it if needed."""
89
home = Path.home()
910
if sys.platform.startswith("win"):
1011
log_dir = home.joinpath("AppData", "Local", "sshsync", "logs")
@@ -19,6 +20,7 @@ def get_log_path() -> Path:
1920

2021

2122
def setup_logging():
23+
"""Configure structlog for file logging."""
2224
structlog.configure(
2325
processors=[
2426
structlog.processors.TimeStamper(fmt="iso"),

src/sshsync/schemas.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ class Host:
4242

4343
@dataclass
4444
class HostAuth:
45+
"""
46+
Authentication method and passphrase info for a host.
47+
48+
Attributes:
49+
auth (Literal["key", "password"]): Authentication type.
50+
key_has_passphrase (bool | None): Whether the key has a passphrase.
51+
"""
52+
4553
auth: Literal["key", "password"]
4654
key_has_passphrase: bool | None
4755

@@ -53,18 +61,18 @@ class YamlConfig:
5361
5462
Attributes:
5563
groups (dict[str, list[str]]): A mapping of group names to lists of host aliases.
64+
host_auth (dict[str, HostAuth]): Mapping of host aliases to authentication info.
5665
"""
5766

5867
groups: dict[str, list[str]]
5968
host_auth: dict[str, HostAuth]
6069

61-
def as_dict(self) -> dict:
70+
def as_dict(self) -> dict[str, object]:
6271
"""
6372
Converts the `YamlConfig` instance into a dictionary format.
6473
6574
Returns:
66-
dict: A dictionary representation of the `YamlConfig` instance, where keys are attribute names
67-
and values are the corresponding attribute values.
75+
dict: A dictionary representation of the `YamlConfig` instance.
6876
"""
6977
return asdict(self)
7078

src/sshsync/utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ def is_key_private(key_path: str) -> bool:
9292

9393

9494
def check_key_passphrase(key_path: str, passphrase: str) -> bool:
95+
"""Check if the given passphrase unlocks the SSH key."""
9596
path = Path(key_path).expanduser()
9697
if not key_path or not path.exists() or path.is_dir():
9798
return False
@@ -105,6 +106,7 @@ def check_key_passphrase(key_path: str, passphrase: str) -> bool:
105106
def get_pass(
106107
host: str, pass_type: Literal["passphrase", "password"], key_path: str = ""
107108
) -> str:
109+
"""Prompt the user for a password or passphrase, validating if needed."""
108110
while True:
109111
val = Prompt.ask(f"Please enter {pass_type} for host `{host}`", password=True)
110112
if not val.strip():
@@ -118,6 +120,7 @@ def get_pass(
118120

119121

120122
def set_keyring(host: str, password: str):
123+
"""Store a password in the keyring for a host."""
121124
keyring.set_password("sshsync", host, password)
122125

123126

0 commit comments

Comments
 (0)