diff --git a/authentik/outposts/controllers/base.py b/authentik/outposts/controllers/base.py index 6f00015dd..fbd665dad 100644 --- a/authentik/outposts/controllers/base.py +++ b/authentik/outposts/controllers/base.py @@ -9,7 +9,11 @@ from structlog.testing import capture_logs from authentik import ENV_GIT_HASH_KEY, __version__ from authentik.lib.config import CONFIG from authentik.lib.sentry import SentryIgnoredException -from authentik.outposts.models import Outpost, OutpostServiceConnection +from authentik.outposts.models import ( + Outpost, + OutpostServiceConnection, + OutpostServiceConnectionState, +) FIELD_MANAGER = "goauthentik.io" @@ -28,11 +32,25 @@ class DeploymentPort: inner_port: Optional[int] = None +class BaseClient: + """Base class for custom clients""" + + def fetch_state(self) -> OutpostServiceConnectionState: + """Get state, version info""" + raise NotImplementedError + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + """Cleanup after usage""" + + class BaseController: """Base Outpost deployment controller""" deployment_ports: list[DeploymentPort] - + client: BaseClient outpost: Outpost connection: OutpostServiceConnection @@ -63,6 +81,13 @@ class BaseController: self.down() return [x["event"] for x in logs] + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + """Cleanup after usage""" + self.client.__exit__() + def get_static_deployment(self) -> str: """Return a static deployment configuration""" raise NotImplementedError diff --git a/authentik/outposts/controllers/docker.py b/authentik/outposts/controllers/docker.py index adae08ef7..0567e6464 100644 --- a/authentik/outposts/controllers/docker.py +++ b/authentik/outposts/controllers/docker.py @@ -1,17 +1,75 @@ """Docker controller""" from time import sleep +from typing import Optional +from urllib.parse import urlparse from django.conf import settings from django.utils.text import slugify -from docker import DockerClient +from docker import DockerClient as UpstreamDockerClient from docker.errors import DockerException, NotFound from docker.models.containers import Container +from docker.utils.utils import kwargs_from_env +from structlog.stdlib import get_logger from yaml import safe_dump from authentik import __version__ -from authentik.outposts.controllers.base import BaseController, ControllerException +from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException +from authentik.outposts.docker_ssh import DockerInlineSSH +from authentik.outposts.docker_tls import DockerInlineTLS from authentik.outposts.managed import MANAGED_OUTPOST -from authentik.outposts.models import DockerServiceConnection, Outpost, ServiceConnectionInvalid +from authentik.outposts.models import ( + DockerServiceConnection, + Outpost, + OutpostServiceConnectionState, + ServiceConnectionInvalid, +) + + +class DockerClient(UpstreamDockerClient, BaseClient): + """Custom docker client, which can handle TLS and SSH from a database.""" + + tls: Optional[DockerInlineTLS] + ssh: Optional[DockerInlineSSH] + + def __init__(self, connection: DockerServiceConnection): + self.tls = None + self.ssh = None + if connection.local: + # Same result as DockerClient.from_env + super().__init__(kwargs_from_env()) + else: + parsed_url = urlparse(connection.url) + tls_config = False + if parsed_url.scheme == "ssh": + self.ssh = DockerInlineSSH(parsed_url.hostname, connection.tls_authentication) + self.ssh.write() + else: + self.tls = DockerInlineTLS( + verification_kp=connection.tls_verification, + authentication_kp=connection.tls_authentication, + ) + tls_config = self.tls.write() + super().__init__( + base_url=connection.url, + tls=tls_config, + ) + self.logger = get_logger() + # Ensure the client actually works + self.containers.list() + + def fetch_state(self) -> OutpostServiceConnectionState: + try: + return OutpostServiceConnectionState(version=self.info()["ServerVersion"], healthy=True) + except (ServiceConnectionInvalid, DockerException): + return OutpostServiceConnectionState(version="", healthy=False) + + def __exit__(self, exc_type, exc_value, traceback): + if self.tls: + self.logger.debug("Cleaning up TLS") + self.tls.cleanup() + if self.ssh: + self.logger.debug("Cleaning up SSH") + self.ssh.cleanup() class DockerController(BaseController): @@ -27,8 +85,9 @@ class DockerController(BaseController): if outpost.managed == MANAGED_OUTPOST: return try: - self.client = connection.client() - except ServiceConnectionInvalid as exc: + self.client = DockerClient(connection) + except DockerException as exc: + self.logger.warning(exc) raise ControllerException from exc @property diff --git a/authentik/outposts/controllers/kubernetes.py b/authentik/outposts/controllers/kubernetes.py index 8fd6a1b57..45c966b2e 100644 --- a/authentik/outposts/controllers/kubernetes.py +++ b/authentik/outposts/controllers/kubernetes.py @@ -2,19 +2,53 @@ from io import StringIO from typing import Type +from kubernetes.client import VersionApi, VersionInfo from kubernetes.client.api_client import ApiClient +from kubernetes.client.configuration import Configuration from kubernetes.client.exceptions import OpenApiException +from kubernetes.config.config_exception import ConfigException +from kubernetes.config.incluster_config import load_incluster_config +from kubernetes.config.kube_config import load_kube_config_from_dict from structlog.testing import capture_logs from urllib3.exceptions import HTTPError from yaml import dump_all -from authentik.outposts.controllers.base import BaseController, ControllerException +from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler from authentik.outposts.controllers.k8s.secret import SecretReconciler from authentik.outposts.controllers.k8s.service import ServiceReconciler from authentik.outposts.controllers.k8s.service_monitor import PrometheusServiceMonitorReconciler -from authentik.outposts.models import KubernetesServiceConnection, Outpost, ServiceConnectionInvalid +from authentik.outposts.models import ( + KubernetesServiceConnection, + Outpost, + OutpostServiceConnectionState, + ServiceConnectionInvalid, +) + + +class KubernetesClient(ApiClient, BaseClient): + """Custom kubernetes client based on service connection""" + + def __init__(self, connection: KubernetesServiceConnection): + config = Configuration() + try: + if connection.local: + load_incluster_config(client_configuration=config) + else: + load_kube_config_from_dict(connection.kubeconfig, client_configuration=config) + super().__init__(config) + except ConfigException as exc: + raise ServiceConnectionInvalid from exc + + def fetch_state(self) -> OutpostServiceConnectionState: + """Get version info""" + try: + api_instance = VersionApi(self) + version: VersionInfo = api_instance.get_code() + return OutpostServiceConnectionState(version=version.git_version, healthy=True) + except (OpenApiException, HTTPError, ServiceConnectionInvalid): + return OutpostServiceConnectionState(version="", healthy=False) class KubernetesController(BaseController): @@ -23,12 +57,12 @@ class KubernetesController(BaseController): reconcilers: dict[str, Type[KubernetesObjectReconciler]] reconcile_order: list[str] - client: ApiClient + client: KubernetesClient connection: KubernetesServiceConnection def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection) -> None: super().__init__(outpost, connection) - self.client = connection.client() + self.client = KubernetesClient(connection) self.reconcilers = { "secret": SecretReconciler, "deployment": DeploymentReconciler, diff --git a/authentik/outposts/docker_ssh.py b/authentik/outposts/docker_ssh.py new file mode 100644 index 000000000..9f0191244 --- /dev/null +++ b/authentik/outposts/docker_ssh.py @@ -0,0 +1,77 @@ +"""Docker SSH helper""" +import os +from pathlib import Path +from tempfile import gettempdir + +from authentik.crypto.models import CertificateKeyPair + +HEADER = "### Managed by authentik" +FOOTER = "### End Managed by authentik" + + +def opener(path, flags): + """File opener to create files as 700 perms""" + return os.open(path, flags, 0o700) + + +class DockerInlineSSH: + """Create paramiko ssh config from CertificateKeyPair""" + + host: str + keypair: CertificateKeyPair + + key_path: str + config_path: Path + header: str + + def __init__(self, host: str, keypair: CertificateKeyPair) -> None: + self.host = host + self.keypair = keypair + self.config_path = Path("~/.ssh/config").expanduser() + self.header = f"{HEADER} - {self.host}\n" + + def write_config(self, key_path: str) -> bool: + """Update the local user's ssh config file""" + with open(self.config_path, "a+", encoding="utf-8") as ssh_config: + if self.header in ssh_config.readlines(): + return False + ssh_config.writelines( + [ + self.header, + f"Host {self.host}\n", + f" IdentityFile {key_path}\n", + f"{FOOTER}\n", + "\n", + ] + ) + return True + + def write_key(self): + """Write keypair's private key to a temporary file""" + path = Path(gettempdir(), f"{self.keypair.pk}_private.pem") + with open(path, "w", encoding="utf8", opener=opener) as _file: + _file.write(self.keypair.key_data) + return str(path) + + def write(self): + """Write keyfile and update ssh config""" + self.key_path = self.write_key() + was_written = self.write_config(self.key_path) + if not was_written: + self.cleanup() + + def cleanup(self): + """Cleanup when we're done""" + os.unlink(self.key_path) + with open(self.config_path, "r+", encoding="utf-8") as ssh_config: + start = 0 + end = 0 + lines = ssh_config.readlines() + for idx, line in enumerate(lines): + if line == self.header: + start = idx + if start != 0 and line == f"{FOOTER}\n": + end = idx + with open(self.config_path, "w+", encoding="utf-8") as ssh_config: + lines = lines[:start] + lines[end + 2 :] + ssh_config.writelines(lines) diff --git a/authentik/outposts/docker_tls.py b/authentik/outposts/docker_tls.py index 0630571f3..79e9bdaca 100644 --- a/authentik/outposts/docker_tls.py +++ b/authentik/outposts/docker_tls.py @@ -1,4 +1,5 @@ """Create Docker TLSConfig from CertificateKeyPair""" +from os import unlink from pathlib import Path from tempfile import gettempdir from typing import Optional @@ -14,6 +15,8 @@ class DockerInlineTLS: verification_kp: Optional[CertificateKeyPair] authentication_kp: Optional[CertificateKeyPair] + _paths: list[str] + def __init__( self, verification_kp: Optional[CertificateKeyPair], @@ -21,14 +24,21 @@ class DockerInlineTLS: ) -> None: self.verification_kp = verification_kp self.authentication_kp = authentication_kp + self._paths = [] def write_file(self, name: str, contents: str) -> str: """Wrapper for mkstemp that uses fdopen""" path = Path(gettempdir(), name) with open(path, "w", encoding="utf8") as _file: _file.write(contents) + self._paths.append(str(path)) return str(path) + def cleanup(self): + """Clean up certificates when we're done""" + for path in self._paths: + unlink(path) + def write(self) -> TLSConfig: """Create TLSConfig with Certificate Key pairs""" # So yes, this is quite ugly. But sadly, there is no clean way to pass diff --git a/authentik/outposts/models.py b/authentik/outposts/models.py index 7f73b4510..0b0afe63d 100644 --- a/authentik/outposts/models.py +++ b/authentik/outposts/models.py @@ -11,21 +11,11 @@ from django.core.cache import cache from django.db import IntegrityError, models, transaction from django.db.models.base import Model from django.utils.translation import gettext_lazy as _ -from docker.client import DockerClient -from docker.errors import DockerException from guardian.models import UserObjectPermission from guardian.shortcuts import assign_perm -from kubernetes.client import VersionApi, VersionInfo -from kubernetes.client.api_client import ApiClient -from kubernetes.client.configuration import Configuration -from kubernetes.client.exceptions import OpenApiException -from kubernetes.config.config_exception import ConfigException -from kubernetes.config.incluster_config import load_incluster_config -from kubernetes.config.kube_config import load_kube_config_from_dict from model_utils.managers import InheritanceManager from packaging.version import LegacyVersion, Version, parse from structlog.stdlib import get_logger -from urllib3.exceptions import HTTPError from authentik import ENV_GIT_HASH_KEY, __version__ from authentik.core.models import ( @@ -44,7 +34,6 @@ from authentik.lib.sentry import SentryIgnoredException from authentik.lib.utils.errors import exception_to_string from authentik.managed.models import ManagedModel from authentik.outposts.controllers.k8s.utils import get_namespace -from authentik.outposts.docker_tls import DockerInlineTLS from authentik.tenants.models import Tenant OUR_VERSION = parse(__version__) @@ -150,10 +139,6 @@ class OutpostServiceConnection(models.Model): return OutpostServiceConnectionState("", False) return state - def fetch_state(self) -> OutpostServiceConnectionState: - """Fetch current Service Connection state""" - raise NotImplementedError - @property def component(self) -> str: """Return component used to edit this object""" @@ -211,35 +196,6 @@ class DockerServiceConnection(OutpostServiceConnection): def __str__(self) -> str: return f"Docker Service-Connection {self.name}" - def client(self) -> DockerClient: - """Get DockerClient""" - try: - client = None - if self.local: - client = DockerClient.from_env() - else: - client = DockerClient( - base_url=self.url, - tls=DockerInlineTLS( - verification_kp=self.tls_verification, - authentication_kp=self.tls_authentication, - ).write(), - ) - client.containers.list() - except DockerException as exc: - LOGGER.warning(exc) - raise ServiceConnectionInvalid from exc - return client - - def fetch_state(self) -> OutpostServiceConnectionState: - try: - client = self.client() - return OutpostServiceConnectionState( - version=client.info()["ServerVersion"], healthy=True - ) - except ServiceConnectionInvalid: - return OutpostServiceConnectionState(version="", healthy=False) - class Meta: verbose_name = _("Docker Service-Connection") @@ -266,27 +222,6 @@ class KubernetesServiceConnection(OutpostServiceConnection): def __str__(self) -> str: return f"Kubernetes Service-Connection {self.name}" - def fetch_state(self) -> OutpostServiceConnectionState: - try: - client = self.client() - api_instance = VersionApi(client) - version: VersionInfo = api_instance.get_code() - return OutpostServiceConnectionState(version=version.git_version, healthy=True) - except (OpenApiException, HTTPError, ServiceConnectionInvalid): - return OutpostServiceConnectionState(version="", healthy=False) - - def client(self) -> ApiClient: - """Get Kubernetes client configured from kubeconfig""" - config = Configuration() - try: - if self.local: - load_incluster_config(client_configuration=config) - else: - load_kube_config_from_dict(self.kubeconfig, client_configuration=config) - return ApiClient(config) - except ConfigException as exc: - raise ServiceConnectionInvalid from exc - class Meta: verbose_name = _("Kubernetes Service-Connection") diff --git a/authentik/outposts/tasks.py b/authentik/outposts/tasks.py index 2b9c9e07e..64acd1c1a 100644 --- a/authentik/outposts/tasks.py +++ b/authentik/outposts/tasks.py @@ -25,6 +25,8 @@ from authentik.events.monitored_tasks import ( ) from authentik.lib.utils.reflection import path_to_class from authentik.outposts.controllers.base import BaseController, ControllerException +from authentik.outposts.controllers.docker import DockerClient +from authentik.outposts.controllers.kubernetes import KubernetesClient from authentik.outposts.models import ( DockerServiceConnection, KubernetesServiceConnection, @@ -45,21 +47,21 @@ LOGGER = get_logger() CACHE_KEY_OUTPOST_DOWN = "outpost_teardown_%s" -def controller_for_outpost(outpost: Outpost) -> Optional[BaseController]: +def controller_for_outpost(outpost: Outpost) -> Optional[type[BaseController]]: """Get a controller for the outpost, when a service connection is defined""" if not outpost.service_connection: return None service_connection = outpost.service_connection if outpost.type == OutpostType.PROXY: if isinstance(service_connection, DockerServiceConnection): - return ProxyDockerController(outpost, service_connection) + return ProxyDockerController if isinstance(service_connection, KubernetesServiceConnection): - return ProxyKubernetesController(outpost, service_connection) + return ProxyKubernetesController if outpost.type == OutpostType.LDAP: if isinstance(service_connection, DockerServiceConnection): - return LDAPDockerController(outpost, service_connection) + return LDAPDockerController if isinstance(service_connection, KubernetesServiceConnection): - return LDAPKubernetesController(outpost, service_connection) + return LDAPKubernetesController return None @@ -71,7 +73,12 @@ def outpost_service_connection_state(connection_pk: Any): ) if not connection: return - state = connection.fetch_state() + if isinstance(connection, DockerServiceConnection): + cls = DockerClient + if isinstance(connection, KubernetesServiceConnection): + cls = KubernetesClient + with cls(connection) as client: + state = client.fetch_state() cache.set(connection.state_key, state, timeout=None) @@ -114,14 +121,15 @@ def outpost_controller( return self.set_uid(slugify(outpost.name)) try: - controller = controller_for_outpost(outpost) - if not controller: + controller_type = controller_for_outpost(outpost) + if not controller_type: return - logs = getattr(controller, f"{action}_with_logs")() - LOGGER.debug("---------------Outpost Controller logs starting----------------") - for log in logs: - LOGGER.debug(log) - LOGGER.debug("-----------------Outpost Controller logs end-------------------") + with controller_type(outpost, outpost.service_connection) as controller: + logs = getattr(controller, f"{action}_with_logs")() + LOGGER.debug("---------------Outpost Controller logs starting----------------") + for log in logs: + LOGGER.debug(log) + LOGGER.debug("-----------------Outpost Controller logs end-------------------") except (ControllerException, ServiceConnectionInvalid) as exc: self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) else: diff --git a/poetry.lock b/poetry.lock index cfa547753..d3ec9302c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -175,6 +175,22 @@ GitPython = ">=1.0.1" PyYAML = ">=5.3.1" stevedore = ">=1.20.0" +[[package]] +name = "bcrypt" +version = "3.2.0" +description = "Modern password hashing for your software and your servers" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.1" +six = ">=1.4.1" + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + [[package]] name = "billiard" version = "3.6.4.0" @@ -1162,6 +1178,25 @@ python-versions = ">=3.6" [package.dependencies] pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" +[[package]] +name = "paramiko" +version = "2.9.1" +description = "SSH2 protocol library" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +bcrypt = ">=3.1.3" +cryptography = ">=2.5" +pynacl = ">=1.0.1" + +[package.extras] +all = ["pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "bcrypt (>=3.1.3)", "invoke (>=1.3)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] +ed25519 = ["pynacl (>=1.0.1)", "bcrypt (>=3.1.3)"] +gssapi = ["pyasn1 (>=0.1.7)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] +invoke = ["invoke (>=1.3)"] + [[package]] name = "pathspec" version = "0.9.0" @@ -1332,6 +1367,22 @@ python-versions = "*" [package.dependencies] pylint = ">=1.7" +[[package]] +name = "pynacl" +version = "1.4.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +cffi = ">=1.4.1" +six = "*" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"] + [[package]] name = "pyopenssl" version = "21.0.0" @@ -2024,7 +2075,7 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "2caacecbae1850c6cd20e52ce70723b2e21e57cc54a9a3cd9dd8e00e6a6da481" +content-hash = "39b437e0cbd49396c867f8a7cfd3d0581facfbb830e069f758c71c89be09d1f6" [metadata.files] aiohttp = [ @@ -2152,6 +2203,15 @@ bandit = [ {file = "bandit-1.7.1-py3-none-any.whl", hash = "sha256:f5acd838e59c038a159b5c621cf0f8270b279e884eadd7b782d7491c02add0d4"}, {file = "bandit-1.7.1.tar.gz", hash = "sha256:a81b00b5436e6880fa8ad6799bc830e02032047713cbb143a12939ac67eb756c"}, ] +bcrypt = [ + {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"}, + {file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"}, + {file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"}, + {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"}, +] billiard = [ {file = "billiard-3.6.4.0-py3-none-any.whl", hash = "sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b"}, {file = "billiard-3.6.4.0.tar.gz", hash = "sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547"}, @@ -2903,6 +2963,10 @@ packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] +paramiko = [ + {file = "paramiko-2.9.1-py2.py3-none-any.whl", hash = "sha256:db5d3f19607941b1c90233588d60213c874392c4961c6297037da989c24f8070"}, + {file = "paramiko-2.9.1.tar.gz", hash = "sha256:a1fdded3b55f61d23389e4fe52d9ae428960ac958d2edf50373faa5d8926edd0"}, +] pathspec = [ {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, @@ -3051,6 +3115,26 @@ pylint-plugin-utils = [ {file = "pylint-plugin-utils-0.6.tar.gz", hash = "sha256:57625dcca20140f43731311cd8fd879318bf45a8b0fd17020717a8781714a25a"}, {file = "pylint_plugin_utils-0.6-py3-none-any.whl", hash = "sha256:2f30510e1c46edf268d3a195b2849bd98a1b9433229bb2ba63b8d776e1fc4d0a"}, ] +pynacl = [ + {file = "PyNaCl-1.4.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-win32.whl", hash = "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80"}, + {file = "PyNaCl-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"}, + {file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"}, + {file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"}, + {file = "PyNaCl-1.4.0-cp35-abi3-win32.whl", hash = "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634"}, + {file = "PyNaCl-1.4.0-cp35-abi3-win_amd64.whl", hash = "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6"}, + {file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"}, + {file = "PyNaCl-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25"}, + {file = "PyNaCl-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4"}, + {file = "PyNaCl-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6"}, + {file = "PyNaCl-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f"}, + {file = "PyNaCl-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f"}, + {file = "PyNaCl-1.4.0-cp38-cp38-win32.whl", hash = "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96"}, + {file = "PyNaCl-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420"}, + {file = "PyNaCl-1.4.0.tar.gz", hash = "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505"}, +] pyopenssl = [ {file = "pyOpenSSL-21.0.0-py2.py3-none-any.whl", hash = "sha256:8935bd4920ab9abfebb07c41a4f58296407ed77f04bd1a92914044b848ba1ed6"}, {file = "pyOpenSSL-21.0.0.tar.gz", hash = "sha256:5e2d8c5e46d0d865ae933bef5230090bdaf5506281e9eec60fa250ee80600cb3"}, diff --git a/pyproject.toml b/pyproject.toml index c7fe987b3..24f8a3698 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -145,6 +145,7 @@ webauthn = "*" xmlsec = "*" flower = "*" wsproto = "*" +paramiko = "^2.9.1" [tool.poetry.dev-dependencies] bandit = "*" diff --git a/web/src/locales/en.po b/web/src/locales/en.po index 770d13eb0..06b5de472 100644 --- a/web/src/locales/en.po +++ b/web/src/locales/en.po @@ -666,8 +666,12 @@ msgid "Callback URL" msgstr "Callback URL" #: src/pages/outposts/ServiceConnectionDockerForm.ts -msgid "Can be in the format of 'unix://' when connecting to a local docker daemon, or 'https://:2376' when connecting to a remote system." -msgstr "Can be in the format of 'unix://' when connecting to a local docker daemon, or 'https://:2376' when connecting to a remote system." +#~ msgid "Can be in the format of 'unix://' when connecting to a local docker daemon, or 'https://:2376' when connecting to a remote system." +#~ msgstr "Can be in the format of 'unix://' when connecting to a local docker daemon, or 'https://:2376' when connecting to a remote system." + +#: src/pages/outposts/ServiceConnectionDockerForm.ts +msgid "Can be in the format of 'unix://' when connecting to a local docker daemon, using 'ssh://' to connect via SSH, or 'https://:2376' when connecting to a remote system." +msgstr "Can be in the format of 'unix://' when connecting to a local docker daemon, using 'ssh://' to connect via SSH, or 'https://:2376' when connecting to a remote system." #: src/elements/forms/ConfirmationForm.ts #: src/elements/forms/DeleteBulkForm.ts @@ -4885,8 +4889,12 @@ msgid "System task execution" msgstr "System task execution" #: src/pages/outposts/ServiceConnectionDockerForm.ts -msgid "TLS Authentication Certificate" -msgstr "TLS Authentication Certificate" +#~ msgid "TLS Authentication Certificate" +#~ msgstr "TLS Authentication Certificate" + +#: src/pages/outposts/ServiceConnectionDockerForm.ts +msgid "TLS Authentication Certificate/SSH Keypair" +msgstr "TLS Authentication Certificate/SSH Keypair" #: #~ msgid "TLS Server name" @@ -5879,6 +5887,10 @@ msgstr "When a valid username/email has been entered, and this option is enabled msgid "When connecting to an LDAP Server with TLS, certificates are not checked by default. Specify a keypair to validate the remote certificate." msgstr "When connecting to an LDAP Server with TLS, certificates are not checked by default. Specify a keypair to validate the remote certificate." +#: src/pages/outposts/ServiceConnectionDockerForm.ts +msgid "When connecting via SSH, this keypair is used for authentication." +msgstr "When connecting via SSH, this keypair is used for authentication." + #: src/pages/stages/email/EmailStageForm.ts msgid "When enabled, global Email connection settings will be used and connection settings below will be ignored." msgstr "When enabled, global Email connection settings will be used and connection settings below will be ignored." diff --git a/web/src/locales/fr_FR.po b/web/src/locales/fr_FR.po index 190f4ca88..038038ddf 100644 --- a/web/src/locales/fr_FR.po +++ b/web/src/locales/fr_FR.po @@ -669,8 +669,12 @@ msgid "Callback URL" msgstr "URL de rappel" #: src/pages/outposts/ServiceConnectionDockerForm.ts -msgid "Can be in the format of 'unix://' when connecting to a local docker daemon, or 'https://:2376' when connecting to a remote system." -msgstr "Peut être au format \"unix://\" pour une connexion à un service docker local, ou \"https://:2376\" pour une connexion à un système distant." +#~ msgid "Can be in the format of 'unix://' when connecting to a local docker daemon, or 'https://:2376' when connecting to a remote system." +#~ msgstr "Peut être au format \"unix://\" pour une connexion à un service docker local, ou \"https://:2376\" pour une connexion à un système distant." + +#: src/pages/outposts/ServiceConnectionDockerForm.ts +msgid "Can be in the format of 'unix://' when connecting to a local docker daemon, using 'ssh://' to connect via SSH, or 'https://:2376' when connecting to a remote system." +msgstr "" #: src/elements/forms/ConfirmationForm.ts #: src/elements/forms/DeleteBulkForm.ts @@ -4841,8 +4845,12 @@ msgid "System task execution" msgstr "Exécution de tâche système" #: src/pages/outposts/ServiceConnectionDockerForm.ts -msgid "TLS Authentication Certificate" -msgstr "Certificat TLS d'authentification" +#~ msgid "TLS Authentication Certificate" +#~ msgstr "Certificat TLS d'authentification" + +#: src/pages/outposts/ServiceConnectionDockerForm.ts +msgid "TLS Authentication Certificate/SSH Keypair" +msgstr "" #~ msgid "TLS Server name" #~ msgstr "Nom TLS du serveur" @@ -5818,6 +5826,10 @@ msgstr "Lorsqu'un nom d'utilisateur/email valide a été saisi, et si cette opti msgid "When connecting to an LDAP Server with TLS, certificates are not checked by default. Specify a keypair to validate the remote certificate." msgstr "" +#: src/pages/outposts/ServiceConnectionDockerForm.ts +msgid "When connecting via SSH, this keypair is used for authentication." +msgstr "" + #: src/pages/stages/email/EmailStageForm.ts msgid "When enabled, global Email connection settings will be used and connection settings below will be ignored." msgstr "Si activé, les paramètres globaux de connexion courriel seront utilisés et les paramètres de connexion ci-dessous seront ignorés." diff --git a/web/src/locales/pseudo-LOCALE.po b/web/src/locales/pseudo-LOCALE.po index 179cdecce..e9403bf46 100644 --- a/web/src/locales/pseudo-LOCALE.po +++ b/web/src/locales/pseudo-LOCALE.po @@ -662,7 +662,11 @@ msgid "Callback URL" msgstr "" #: src/pages/outposts/ServiceConnectionDockerForm.ts -msgid "Can be in the format of 'unix://' when connecting to a local docker daemon, or 'https://:2376' when connecting to a remote system." +#~ msgid "Can be in the format of 'unix://' when connecting to a local docker daemon, or 'https://:2376' when connecting to a remote system." +#~ msgstr "" + +#: src/pages/outposts/ServiceConnectionDockerForm.ts +msgid "Can be in the format of 'unix://' when connecting to a local docker daemon, using 'ssh://' to connect via SSH, or 'https://:2376' when connecting to a remote system." msgstr "" #: src/elements/forms/ConfirmationForm.ts @@ -4875,7 +4879,11 @@ msgid "System task execution" msgstr "" #: src/pages/outposts/ServiceConnectionDockerForm.ts -msgid "TLS Authentication Certificate" +#~ msgid "TLS Authentication Certificate" +#~ msgstr "" + +#: src/pages/outposts/ServiceConnectionDockerForm.ts +msgid "TLS Authentication Certificate/SSH Keypair" msgstr "" #: @@ -5859,6 +5867,10 @@ msgstr "" msgid "When connecting to an LDAP Server with TLS, certificates are not checked by default. Specify a keypair to validate the remote certificate." msgstr "" +#: src/pages/outposts/ServiceConnectionDockerForm.ts +msgid "When connecting via SSH, this keypair is used for authentication." +msgstr "" + #: src/pages/stages/email/EmailStageForm.ts msgid "When enabled, global Email connection settings will be used and connection settings below will be ignored." msgstr "" diff --git a/web/src/pages/outposts/ServiceConnectionDockerForm.ts b/web/src/pages/outposts/ServiceConnectionDockerForm.ts index 7700c5a81..f2884956a 100644 --- a/web/src/pages/outposts/ServiceConnectionDockerForm.ts +++ b/web/src/pages/outposts/ServiceConnectionDockerForm.ts @@ -72,7 +72,7 @@ export class ServiceConnectionDockerForm extends ModelForm

- ${t`Can be in the format of 'unix://' when connecting to a local docker daemon, or 'https://:2376' when connecting to a remote system.`} + ${t`Can be in the format of 'unix://' when connecting to a local docker daemon, using 'ssh://' to connect via SSH, or 'https://:2376' when connecting to a remote system.`}