|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
5 | 5 | import math |
| 6 | +import os |
| 7 | +import stat |
6 | 8 | from dataclasses import dataclass, field |
7 | 9 | from pathlib import Path |
8 | 10 | from typing import Any |
|
22 | 24 | PROTECTED_DATABASES = frozenset({'postgres', 'template0', 'template1'}) |
23 | 25 |
|
24 | 26 |
|
| 27 | +def resolve_ssh_agent_socket() -> Path: |
| 28 | + raw_path = os.environ.get('SSH_AUTH_SOCK') |
| 29 | + if not raw_path: |
| 30 | + raise ConfigurationError('--ssh-agent requires SSH_AUTH_SOCK to reference a running agent') |
| 31 | + path = Path(raw_path).expanduser() |
| 32 | + try: |
| 33 | + mode = path.stat().st_mode |
| 34 | + except OSError as exc: |
| 35 | + raise ConfigurationError(f'SSH agent socket is not available: {path}') from exc |
| 36 | + if not stat.S_ISSOCK(mode): |
| 37 | + raise ConfigurationError(f'SSH_AUTH_SOCK is not a socket: {path}') |
| 38 | + return path |
| 39 | + |
| 40 | + |
25 | 41 | def _positive_int(value: Any, option: str) -> int: |
26 | 42 | try: |
27 | 43 | parsed = int(value) |
@@ -100,6 +116,7 @@ class HostConfig: |
100 | 116 | ssh_port: int = 22 |
101 | 117 | ssh_user: str = 'postgres' |
102 | 118 | ssh_key: Path | None = None |
| 119 | + ssh_agent: bool = False |
103 | 120 | ssh_known_hosts: Path | None = None |
104 | 121 | ssh_insecure_no_host_key_check: bool = False |
105 | 122 | remote_pg_host: str | None = None |
@@ -131,8 +148,14 @@ def connection_kwargs( |
131 | 148 | 'host': self.ssh_host, |
132 | 149 | 'port': self.ssh_port, |
133 | 150 | 'username': self.ssh_user, |
134 | | - 'client_keys': str(self.ssh_key) if self.ssh_key else None, |
| 151 | + 'client_keys': [str(self.ssh_key)] if self.ssh_key else [], |
| 152 | + 'agent_path': str(resolve_ssh_agent_socket()) if self.ssh_agent else None, |
| 153 | + 'agent_forwarding': False, |
135 | 154 | 'known_hosts': known_hosts, |
| 155 | + 'config': None, |
| 156 | + 'preferred_auth': ['publickey'], |
| 157 | + 'password_auth': False, |
| 158 | + 'kbdint_auth': False, |
136 | 159 | 'connect_timeout': min(self.command_timeout, 30.0), |
137 | 160 | }, |
138 | 161 | 'env': env, |
@@ -267,6 +290,7 @@ def build_runtime_config(args: Any) -> RuntimeConfig: |
267 | 290 | ssh_port=_positive_int(values.get('ssh_port') or 22, '--ssh-port'), |
268 | 291 | ssh_user=values.get('ssh_user') or 'postgres', |
269 | 292 | ssh_key=Path(ssh_key_value).expanduser() if ssh_key_value else None, |
| 293 | + ssh_agent=bool(values.get('ssh_agent')), |
270 | 294 | ssh_known_hosts=( |
271 | 295 | Path(values['ssh_known_hosts']).expanduser() if values.get('ssh_known_hosts') else None |
272 | 296 | ), |
@@ -401,10 +425,12 @@ def _validate_host(host: HostConfig, *, needs_database: bool) -> None: |
401 | 425 | return |
402 | 426 | _required(host.ssh_host, '--ssh-host') |
403 | 427 | _required(host.ssh_user, '--ssh-user') |
404 | | - _required(host.ssh_key, '--ssh-key') |
405 | | - assert host.ssh_key is not None |
406 | | - if not host.ssh_key.is_file(): |
| 428 | + if (host.ssh_key is None) == (not host.ssh_agent): |
| 429 | + raise ConfigurationError('SSH requires exactly one of --ssh-key or --ssh-agent') |
| 430 | + if host.ssh_key is not None and not host.ssh_key.is_file(): |
407 | 431 | raise ConfigurationError(f'SSH private key does not exist: {host.ssh_key}') |
| 432 | + if host.ssh_agent: |
| 433 | + resolve_ssh_agent_socket() |
408 | 434 | if not host.ssh_insecure_no_host_key_check: |
409 | 435 | known_hosts = host.ssh_known_hosts or Path('~/.ssh/known_hosts').expanduser() |
410 | 436 | if not known_hosts.is_file(): |
|
0 commit comments