fix: prepare SSH keys with container UID/GID on host before mounting
- Extend prepare_ssh_key_files() with optional uid/gid parameters - Call os.chown on created files when uid/gid are provided - Gracefully handle PermissionError if API process is not root - In start_instance, extract container user UID/GID from manifest - Pass container UID/GID when preparing instance-level SSH key mounts - Legacy clone-mode SSH keys continue to use root (0,0) - Add unit tests for prepare_ssh_key_files ownership logic - Keep apply_ssh_permissions() as fallback for cases where host chown fails Quality gates: pytest 239 passed (6 pre-existing failures), tsc --noEmit clean
This commit is contained in:
@@ -148,10 +148,32 @@ class TestApplySshPermissions:
|
||||
chmod_cmd = mock_run.call_args_list[1][0][0]
|
||||
file_mode_cmd = mock_run.call_args_list[2][0][0]
|
||||
|
||||
assert chown_cmd == ["docker", "exec", "--user", "root", "abc123", "chown", "-R", "user:user", "/home/user/.ssh"]
|
||||
assert chmod_cmd == ["docker", "exec", "--user", "root", "abc123", "chmod", "700", "/home/user/.ssh"]
|
||||
assert chown_cmd == [
|
||||
"docker",
|
||||
"exec",
|
||||
"--user",
|
||||
"root",
|
||||
"abc123",
|
||||
"chown",
|
||||
"-R",
|
||||
"user:user",
|
||||
"/home/user/.ssh",
|
||||
]
|
||||
assert chmod_cmd == [
|
||||
"docker",
|
||||
"exec",
|
||||
"--user",
|
||||
"root",
|
||||
"abc123",
|
||||
"chmod",
|
||||
"700",
|
||||
"/home/user/.ssh",
|
||||
]
|
||||
assert file_mode_cmd[0] == "docker"
|
||||
assert "find /home/user/.ssh -name 'id_*' -type f -exec chmod 600" in file_mode_cmd[-1]
|
||||
assert (
|
||||
"find /home/user/.ssh -name 'id_*' -type f -exec chmod 600"
|
||||
in file_mode_cmd[-1]
|
||||
)
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_uses_root_user(self, mock_run) -> None:
|
||||
@@ -160,11 +182,23 @@ class TestApplySshPermissions:
|
||||
|
||||
assert result["success"] is True
|
||||
chown_cmd = mock_run.call_args_list[0][0][0]
|
||||
assert chown_cmd == ["docker", "exec", "--user", "root", "abc123", "chown", "-R", "root:root", "/root/.ssh"]
|
||||
assert chown_cmd == [
|
||||
"docker",
|
||||
"exec",
|
||||
"--user",
|
||||
"root",
|
||||
"abc123",
|
||||
"chown",
|
||||
"-R",
|
||||
"root:root",
|
||||
"/root/.ssh",
|
||||
]
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_reports_failure(self, mock_run) -> None:
|
||||
mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="chown failed")
|
||||
mock_run.return_value = MagicMock(
|
||||
returncode=1, stdout="", stderr="chown failed"
|
||||
)
|
||||
|
||||
result = apply_ssh_permissions("abc123", "/home/user/.ssh", "user")
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Unit tests for SSH key preparation."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from src.services.ssh_keys import prepare_ssh_key_files
|
||||
|
||||
|
||||
class TestPrepareSshKeyFiles:
|
||||
"""Tests for prepare_ssh_key_files."""
|
||||
|
||||
@patch("src.services.ssh_keys._get_fernet")
|
||||
def test_creates_files_with_default_permissions(self, mock_fernet, tmp_path) -> None:
|
||||
mock_fernet.return_value.decrypt.return_value = b"private-key-content"
|
||||
ssh_key = MagicMock()
|
||||
ssh_key.private_key_encrypted = "enc"
|
||||
ssh_key.public_key = "ssh-ed25519 AAA test@test"
|
||||
|
||||
ssh_dir = prepare_ssh_key_files(str(tmp_path), ssh_key)
|
||||
|
||||
assert Path(ssh_dir).exists()
|
||||
assert (Path(ssh_dir) / "id_ed25519").exists()
|
||||
assert (Path(ssh_dir) / "id_ed25519.pub").exists()
|
||||
assert (Path(ssh_dir) / "config").exists()
|
||||
assert oct(os.stat(Path(ssh_dir) / "id_ed25519").st_mode)[-3:] == "600"
|
||||
|
||||
@patch("src.services.ssh_keys._get_fernet")
|
||||
def test_sets_ownership_when_uid_gid_provided(self, mock_fernet, tmp_path) -> None:
|
||||
mock_fernet.return_value.decrypt.return_value = b"private-key-content"
|
||||
ssh_key = MagicMock()
|
||||
ssh_key.private_key_encrypted = "enc"
|
||||
ssh_key.public_key = "ssh-ed25519 AAA test@test"
|
||||
|
||||
with patch("os.chown") as mock_chown:
|
||||
ssh_dir = prepare_ssh_key_files(
|
||||
str(tmp_path), ssh_key, uid=1001, gid=1001
|
||||
)
|
||||
|
||||
# os.chown is called for the directory and each of the 3 files
|
||||
assert mock_chown.call_count == 4
|
||||
# First call is the directory
|
||||
assert mock_chown.call_args_list[0][0][1] == 1001
|
||||
assert mock_chown.call_args_list[0][0][2] == 1001
|
||||
|
||||
@patch("src.services.ssh_keys._get_fernet")
|
||||
def test_gracefully_handles_permission_error_on_chown(
|
||||
self, mock_fernet, tmp_path
|
||||
) -> None:
|
||||
mock_fernet.return_value.decrypt.return_value = b"private-key-content"
|
||||
ssh_key = MagicMock()
|
||||
ssh_key.private_key_encrypted = "enc"
|
||||
ssh_key.public_key = "ssh-ed25519 AAA test@test"
|
||||
|
||||
with patch("os.chown", side_effect=PermissionError("not allowed")):
|
||||
# Should not raise
|
||||
ssh_dir = prepare_ssh_key_files(
|
||||
str(tmp_path), ssh_key, uid=1001, gid=1001
|
||||
)
|
||||
|
||||
assert Path(ssh_dir).exists()
|
||||
Reference in New Issue
Block a user