Compare commits
3 Commits
a8fbca9ef5
...
9f8058223a
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f8058223a | |||
| b483a34517 | |||
| de8c47c81c |
@@ -232,14 +232,6 @@ def upgrade() -> None:
|
|||||||
"writable": True,
|
"writable": True,
|
||||||
"owner": "user",
|
"owner": "user",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "ssh_keys",
|
|
||||||
"target": "/home/user/.ssh",
|
|
||||||
"source_type": "ssh_key",
|
|
||||||
"mode": "0700",
|
|
||||||
"file_mode": "0600",
|
|
||||||
"readonly": True,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "pi_state",
|
"name": "pi_state",
|
||||||
"target": "/tmp/.pi/agents",
|
"target": "/tmp/.pi/agents",
|
||||||
|
|||||||
@@ -1341,11 +1341,27 @@ async def start_instance(
|
|||||||
manifest_def = await session.get(ToolDefinitionManifest, tool_type.manifest_id)
|
manifest_def = await session.get(ToolDefinitionManifest, tool_type.manifest_id)
|
||||||
if manifest_def:
|
if manifest_def:
|
||||||
manifest = dict(manifest_def.manifest)
|
manifest = dict(manifest_def.manifest)
|
||||||
|
# Merge with base definition if referenced (user config is often in base)
|
||||||
|
if manifest_def.base_definition_id:
|
||||||
|
base_def = await session.get(
|
||||||
|
ToolDefinitionManifest, manifest_def.base_definition_id
|
||||||
|
)
|
||||||
|
if base_def:
|
||||||
|
manifest = resolve_base(
|
||||||
|
deep_merge(dict(base_def.manifest), manifest)
|
||||||
|
)
|
||||||
home_dir = get_manifest_home_dir(manifest)
|
home_dir = get_manifest_home_dir(manifest)
|
||||||
user_cfg = manifest.get("user")
|
user_cfg = manifest.get("user")
|
||||||
if user_cfg:
|
if user_cfg:
|
||||||
container_uid = user_cfg.get("uid", 0)
|
container_uid = user_cfg.get("uid", 0)
|
||||||
container_gid = user_cfg.get("gid", 0)
|
container_gid = user_cfg.get("gid", 0)
|
||||||
|
logger.debug(
|
||||||
|
"Manifest user resolved for instance %s: uid=%s, gid=%s, home=%s",
|
||||||
|
instance.id,
|
||||||
|
container_uid,
|
||||||
|
container_gid,
|
||||||
|
home_dir,
|
||||||
|
)
|
||||||
|
|
||||||
# Apply selected config profile if any
|
# Apply selected config profile if any
|
||||||
instance_dir = os.path.dirname(instance.compose_path)
|
instance_dir = os.path.dirname(instance.compose_path)
|
||||||
|
|||||||
@@ -40,6 +40,17 @@ def apply_mount_permissions(
|
|||||||
"error": None,
|
"error": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Skip read-only mounts — their permissions cannot be changed
|
||||||
|
# post-start because the bind mount is locked.
|
||||||
|
if mount.get("readonly", False):
|
||||||
|
logger.debug(
|
||||||
|
"Skipping permission fix for read-only mount %s (target=%s)",
|
||||||
|
name,
|
||||||
|
target,
|
||||||
|
)
|
||||||
|
results.append(result)
|
||||||
|
continue
|
||||||
|
|
||||||
# Skip if no permission policy defined
|
# Skip if no permission policy defined
|
||||||
if not owner and not mode and not file_mode:
|
if not owner and not mode and not file_mode:
|
||||||
results.append(result)
|
results.append(result)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""SSH key service utilities for preparing keys for container use."""
|
"""SSH key service utilities for preparing keys for container use."""
|
||||||
|
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -7,6 +8,8 @@ from cryptography.fernet import Fernet
|
|||||||
|
|
||||||
from src.config import Settings
|
from src.config import Settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _get_fernet() -> Fernet:
|
def _get_fernet() -> Fernet:
|
||||||
"""Generate a valid Fernet key from the session secret."""
|
"""Generate a valid Fernet key from the session secret."""
|
||||||
@@ -75,10 +78,20 @@ def prepare_ssh_key_files(
|
|||||||
os.chown(private_key_path, effective_uid, effective_gid)
|
os.chown(private_key_path, effective_uid, effective_gid)
|
||||||
os.chown(public_key_path, effective_uid, effective_gid)
|
os.chown(public_key_path, effective_uid, effective_gid)
|
||||||
os.chown(config_path, effective_uid, effective_gid)
|
os.chown(config_path, effective_uid, effective_gid)
|
||||||
except PermissionError:
|
logger.debug(
|
||||||
# API process may not be running as root; permission fixer will
|
"Set SSH key ownership to uid=%s gid=%s for %s",
|
||||||
# handle this post-start if the mount is read-write
|
effective_uid,
|
||||||
pass
|
effective_gid,
|
||||||
|
ssh_dir,
|
||||||
|
)
|
||||||
|
except PermissionError as exc:
|
||||||
|
logger.warning(
|
||||||
|
"Cannot chown SSH keys to uid=%s gid=%s (running as uid=%s): %s",
|
||||||
|
effective_uid,
|
||||||
|
effective_gid,
|
||||||
|
os.getuid(),
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
|
||||||
return str(ssh_dir)
|
return str(ssh_dir)
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,24 @@ class TestApplyMountPermissions:
|
|||||||
"find /home/user/.ssh -type f -exec chmod 0600" in file_mode_call[0][1][2]
|
"find /home/user/.ssh -type f -exec chmod 0600" in file_mode_call[0][1][2]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@patch("src.services.permission_fixer._run_in_container")
|
||||||
|
def test_skips_readonly_mount(self, mock_run) -> None:
|
||||||
|
mounts = [
|
||||||
|
{
|
||||||
|
"name": "ssh_keys",
|
||||||
|
"target": "/home/user/.ssh",
|
||||||
|
"readonly": True,
|
||||||
|
"mode": "0700",
|
||||||
|
"file_mode": "0600",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
results = apply_mount_permissions("abc123", mounts)
|
||||||
|
|
||||||
|
assert len(results) == 1
|
||||||
|
assert results[0]["mount_name"] == "ssh_keys"
|
||||||
|
assert results[0]["success"] is True
|
||||||
|
mock_run.assert_not_called()
|
||||||
|
|
||||||
@patch("src.services.permission_fixer._run_in_container")
|
@patch("src.services.permission_fixer._run_in_container")
|
||||||
def test_skips_mount_with_no_policy(self, mock_run) -> None:
|
def test_skips_mount_with_no_policy(self, mock_run) -> None:
|
||||||
mounts = [
|
mounts = [
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ class TestPrepareSshKeyFiles:
|
|||||||
"""Tests for prepare_ssh_key_files."""
|
"""Tests for prepare_ssh_key_files."""
|
||||||
|
|
||||||
@patch("src.services.ssh_keys._get_fernet")
|
@patch("src.services.ssh_keys._get_fernet")
|
||||||
def test_creates_files_with_default_permissions(self, mock_fernet, tmp_path) -> None:
|
def test_creates_files_with_default_permissions(
|
||||||
|
self, mock_fernet, tmp_path
|
||||||
|
) -> None:
|
||||||
mock_fernet.return_value.decrypt.return_value = b"private-key-content"
|
mock_fernet.return_value.decrypt.return_value = b"private-key-content"
|
||||||
ssh_key = MagicMock()
|
ssh_key = MagicMock()
|
||||||
ssh_key.private_key_encrypted = "enc"
|
ssh_key.private_key_encrypted = "enc"
|
||||||
@@ -35,9 +37,7 @@ class TestPrepareSshKeyFiles:
|
|||||||
ssh_key.public_key = "ssh-ed25519 AAA test@test"
|
ssh_key.public_key = "ssh-ed25519 AAA test@test"
|
||||||
|
|
||||||
with patch("os.chown") as mock_chown:
|
with patch("os.chown") as mock_chown:
|
||||||
ssh_dir = prepare_ssh_key_files(
|
ssh_dir = prepare_ssh_key_files(str(tmp_path), ssh_key, uid=1001, gid=1001)
|
||||||
str(tmp_path), ssh_key, uid=1001, gid=1001
|
|
||||||
)
|
|
||||||
|
|
||||||
# os.chown is called for the directory and each of the 3 files
|
# os.chown is called for the directory and each of the 3 files
|
||||||
assert mock_chown.call_count == 4
|
assert mock_chown.call_count == 4
|
||||||
@@ -56,8 +56,6 @@ class TestPrepareSshKeyFiles:
|
|||||||
|
|
||||||
with patch("os.chown", side_effect=PermissionError("not allowed")):
|
with patch("os.chown", side_effect=PermissionError("not allowed")):
|
||||||
# Should not raise
|
# Should not raise
|
||||||
ssh_dir = prepare_ssh_key_files(
|
ssh_dir = prepare_ssh_key_files(str(tmp_path), ssh_key, uid=1001, gid=1001)
|
||||||
str(tmp_path), ssh_key, uid=1001, gid=1001
|
|
||||||
)
|
|
||||||
|
|
||||||
assert Path(ssh_dir).exists()
|
assert Path(ssh_dir).exists()
|
||||||
|
|||||||
Reference in New Issue
Block a user