feat(init): create /var/lib/decnet/artifacts with setgid + group-write

DEBT-035 step 2. Today the artifacts subtree is auto-created by
Docker as root when a decoy container's bind-mount fires for the
first time. The resulting permissions are root:root 0o755 — the API
process (running as the decnet user) hits PermissionError trying to
read transcripts written by the container, and the soft-fail 404
path gets exercised on every fresh deploy.

Add `/var/lib/decnet/artifacts` to init's dirs list with mode 0o2775:

* 0o2000 — setgid bit. New files inherit the directory's group
  (decnet), regardless of which uid created them. This is the load-
  bearing bit for cross-container reads.
* 0o0775 — owner+group rwx, world rx. Group-write lets the API
  process and the local TTP worker read each other's outputs
  without a manual chown.

`_ensure_dir` already respects the full mode word via `os.chmod`,
no helper change needed.

Test asserts the resulting directory carries exactly 0o2775 after
a fresh `decnet init --prefix`. Defence-in-depth: this works even
if the per-decoy compose `user:` directive (next commit) misses a
template — files still land in the decnet group.
This commit is contained in:
2026-05-02 19:35:20 -04:00
parent 39a298f685
commit b27332169d
2 changed files with 28 additions and 0 deletions

View File

@@ -764,6 +764,13 @@ def register(app: typer.Typer) -> None:
(pfx / _install_rel, 0o755, user, group),
(pfx / "var/lib/decnet", 0o750, user, group),
(pfx / "var/lib/decnet/geoip", 0o755, user, group),
# DEBT-035 / DEBT-047: artifact root carries setgid (the
# 0o2... bit) so every file written under it inherits the
# decnet group regardless of which container's uid created
# it. Group-write (0o2775) lets the API process and the
# local TTP worker read each other's outputs without a
# manual chown after every fresh deploy.
(pfx / "var/lib/decnet/artifacts", 0o2775, user, group),
(pfx / "var/log/decnet", 0o750, user, group),
(etc_decnet, 0o755, "root", group),
(pfx / "run/decnet", 0o755, "root", group),

View File

@@ -241,6 +241,27 @@ def test_init_persists_api_user_group_to_decnet_ini(
assert "api-group = decoygrp" in body
def test_init_creates_artifacts_dir_with_setgid_and_group_write(
monkeypatch: Any, tmp_path: Path, subprocess_calls: List[List[str]],
no_missing_tools: None, missing_user_and_group: None,
) -> None:
"""DEBT-035: `/var/lib/decnet/artifacts` must come up with
setgid (0o2000) + group-write (0o0070) so every file written
by a decoy container inherits the decnet group, regardless of
which container's uid did the creating."""
_seed_deploy(monkeypatch, tmp_path)
prefix = tmp_path / "root"
r = runner.invoke(app, ["init", "--no-start", "--prefix", str(prefix)])
assert r.exit_code == 0, r.output
artifacts = prefix / "var/lib/decnet/artifacts"
assert artifacts.is_dir(), "artifacts dir must be created at init time"
mode = artifacts.stat().st_mode & 0o7777
assert mode == 0o2775, (
f"expected 0o2775 (setgid + 0o775), got {oct(mode)}"
)
def test_init_decnet_ini_loads_via_config_ini(
monkeypatch: Any, tmp_path: Path, subprocess_calls: List[List[str]],
no_missing_tools: None, missing_user_and_group: None,