Files
Unraid-AppOps-Gateway-Public/backend/tests/test_managed_storage_action.py
T

274 lines
12 KiB
Python

from __future__ import annotations
import pytest
from sqlmodel import Session, SQLModel, create_engine, select
from backend.app.db.models import AppSetting, AuditEvent, JobRun, Policy
from backend.app.services.action_execution import (
approval_execution_supported,
execute_governed_action,
mcp_execution_supported,
)
from backend.app.services.action_registry import (
ACTION_RISK,
ACTIONS,
action_default_target,
)
from backend.app.services.policy_engine import evaluate_policy
ACTION = "controller.managed-storage.initialize"
TARGET = "tower/deployment-controller"
def test_managed_storage_initialization_is_typed_high_risk_and_fixed_target():
assert ACTION in ACTIONS
assert ACTION_RISK[ACTION] == "high"
assert action_default_target(ACTION) == TARGET
assert not approval_execution_supported(ACTION)
assert mcp_execution_supported(ACTION)
def test_managed_storage_policy_defaults_to_blocked_and_requires_exact_manual_allow(tmp_path):
engine = create_engine(f"sqlite:///{tmp_path / 'managed-storage-policy.db'}")
SQLModel.metadata.create_all(engine)
with Session(engine) as session:
assert evaluate_policy(session, ACTION, TARGET, "manual")["decision"] == "blocked"
session.add(Policy(
id="managed-storage-wildcard",
action=ACTION,
target="*",
voice="blocked",
manual="allowed",
risk="high",
))
session.commit()
assert evaluate_policy(session, ACTION, TARGET, "manual")["decision"] == "blocked"
session.add(Policy(
id="managed-storage-exact",
action=ACTION,
target=TARGET,
voice="allowed",
manual="allowed",
risk="high",
))
session.commit()
assert evaluate_policy(session, ACTION, TARGET, "manual")["decision"] == "allowed"
assert evaluate_policy(session, ACTION, "tower/other-controller", "manual")["decision"] == "blocked"
assert evaluate_policy(session, ACTION, TARGET, "voice")["decision"] == "blocked"
engine.dispose()
def _executor_engine(tmp_path):
engine = create_engine(f"sqlite:///{tmp_path / 'managed-storage-executor.db'}")
SQLModel.metadata.create_all(engine)
return engine
def _managed_trust():
from backend.app.schemas.enrollment import EnrollmentTrustConfigV1
return EnrollmentTrustConfigV1.model_validate({
"controller_identity": "unraid-controller",
"bootstrap_secret_ref": "ENROLLMENT_BOOTSTRAP",
"trusted_appdata_root": "/var/lib/docker/volumes/itworx-managed-appdata/_data",
"managed_appdata_backup_acknowledged": True,
"approved_networks": ["itworx-mcp-hub-npm"],
"allowed_domain_suffixes": ["example.com"],
"autonomous_profiles": ["repopulse-authenticated"],
"repopulse_proxy_access_list_id": 42,
"oidc_issuer": None,
"oidc_role_claim": "roles",
"administrator_group": None,
"support_agent_group": None,
"read_only_group": None,
})
def _execute_initializer(session):
return execute_governed_action(
session,
action=ACTION,
target=TARGET,
source="chatgpt-mcp",
risk="high",
reason="Operator explicitly selected managed storage and acknowledged its backup scope.",
original_policy_decision="allowed",
)
@pytest.mark.parametrize("trust_state", ["missing", "legacy"])
def test_managed_storage_executor_refuses_missing_selection_or_ack_without_broker_call(
tmp_path, monkeypatch, trust_state
):
from backend.app.services.deployment_broker import DeploymentBrokerClient
engine = _executor_engine(tmp_path)
if trust_state == "legacy":
legacy = _managed_trust().model_copy(update={
"trusted_appdata_root": "/srv/example/appdata",
"managed_appdata_backup_acknowledged": False,
})
with Session(engine) as setup:
setup.add(AppSetting(
key="deployment_enrollment:trust",
value_json=legacy.model_dump(mode="json"),
is_secret=False,
))
setup.commit()
monkeypatch.setattr(
DeploymentBrokerClient,
"initialize_managed_appdata",
lambda *_args, **_kwargs: pytest.fail("broker must not be called without managed trust opt-in"),
)
with Session(engine) as session:
result = _execute_initializer(session)
assert result["success"] is False
assert result["result"] == "blocked"
assert result["auditId"]
assert session.exec(select(JobRun)).all() == []
event = session.exec(select(AuditEvent)).one()
assert event.result == "blocked"
assert event.redacted_metadata == {"executor": ACTION, "trustOptIn": False}
engine.dispose()
def test_managed_storage_executor_requires_bootstrap_secret_without_broker_call(tmp_path, monkeypatch):
from backend.app.services.deployment_broker import DeploymentBrokerClient
from backend.app.services.enrollment_plane import EnrollmentPlane
engine = _executor_engine(tmp_path)
monkeypatch.setattr(EnrollmentPlane, "get_trust_config", staticmethod(lambda _session: _managed_trust()))
monkeypatch.setattr("backend.app.services.secret_store.get_secret", lambda *_args, **_kwargs: None)
monkeypatch.setattr(
DeploymentBrokerClient,
"initialize_managed_appdata",
lambda *_args, **_kwargs: pytest.fail("broker must not be called without bootstrap secret"),
)
with Session(engine) as session:
result = _execute_initializer(session)
assert result["success"] is False
assert result["result"] == "failed"
assert result["auditId"]
assert session.exec(select(JobRun)).all() == []
event = session.exec(select(AuditEvent)).one()
assert event.result == "failed"
assert event.redacted_metadata == {"executor": ACTION, "secretMaterialReturned": "[REDACTED]"}
engine.dispose()
@pytest.mark.parametrize("status", ["initialized", "already_initialized"])
def test_managed_storage_executor_records_durable_success_without_secret_material(
tmp_path, monkeypatch, status
):
from backend.app.services.deployment_broker import DeploymentBrokerClient
from backend.app.services.enrollment_plane import EnrollmentPlane
engine = _executor_engine(tmp_path)
monkeypatch.setattr(EnrollmentPlane, "get_trust_config", staticmethod(lambda _session: _managed_trust()))
monkeypatch.setattr("backend.app.services.secret_store.get_secret", lambda *_args, **_kwargs: "temporary-bootstrap-secret")
broker_calls = []
def initialize(_client, *, secret):
broker_calls.append(secret)
return {
"status": status,
"trustedAppdataRoot": "/var/lib/docker/volumes/itworx-managed-appdata/_data",
"backupLocation": "dockerRootDir",
}
monkeypatch.setattr(DeploymentBrokerClient, "initialize_managed_appdata", initialize)
with Session(engine) as session:
result = _execute_initializer(session)
assert result["success"] is True
assert result["status"] == "success"
assert result["jobId"] and result["auditId"]
assert broker_calls == ["temporary-bootstrap-secret"]
with Session(engine) as session:
job = session.get(JobRun, result["jobId"])
event = session.get(AuditEvent, result["auditId"])
assert job is not None and job.status == "succeeded"
assert event is not None and event.result == "success"
assert event.after_state == {
"status": status,
"trustedAppdataRoot": "/var/lib/docker/volumes/itworx-managed-appdata/_data",
"backupLocation": "dockerRootDir",
}
assert event.redacted_metadata["secretMaterialReturned"] == "[REDACTED]"
assert "temporary-bootstrap-secret" not in repr(event.model_dump())
assert "temporary-bootstrap-secret" not in repr(result)
engine.dispose()
@pytest.mark.parametrize(
"broker_result",
[
{"status": "initialized", "trustedAppdataRoot": "/tmp/wrong-root", "backupLocation": "dockerRootDir"},
{"status": "initialized", "trustedAppdataRoot": "/var/lib/docker/volumes/itworx-managed-appdata/_data", "backupLocation": "external-share"},
{
"status": "secret-sentinel-/var/lib/private",
"trustedAppdataRoot": "/var/lib/docker/volumes/itworx-managed-appdata/_data",
"backupLocation": "dockerRootDir",
},
],
ids=["wrong-root", "wrong-backup-location", "untrusted-status"],
)
def test_managed_storage_executor_fails_closed_on_mismatched_broker_attestation(
tmp_path, monkeypatch, broker_result
):
from backend.app.services.deployment_broker import DeploymentBrokerClient
from backend.app.services.enrollment_plane import EnrollmentPlane
engine = _executor_engine(tmp_path)
monkeypatch.setattr(EnrollmentPlane, "get_trust_config", staticmethod(lambda _session: _managed_trust()))
monkeypatch.setattr("backend.app.services.secret_store.get_secret", lambda *_args, **_kwargs: "temporary-bootstrap-secret")
monkeypatch.setattr(DeploymentBrokerClient, "initialize_managed_appdata", lambda *_args, **_kwargs: broker_result)
with Session(engine) as session:
result = _execute_initializer(session)
assert result["success"] is False
assert result["result"] == "failed"
assert "temporary-bootstrap-secret" not in repr(result)
assert "/tmp/wrong-root" not in repr(result)
assert "secret-sentinel-/var/lib/private" not in repr(result)
event = session.get(AuditEvent, result["auditId"])
assert event is not None and event.result == "failed"
assert event.after_state["trustedAppdataRoot"] is None
assert event.after_state["backupLocation"] is None
if broker_result["status"].startswith("secret-sentinel"):
assert event.after_state["status"] == "refused"
assert "/tmp/wrong-root" not in repr(event.model_dump())
assert "temporary-bootstrap-secret" not in repr(event.model_dump())
assert "secret-sentinel-/var/lib/private" not in repr(event.model_dump())
job = session.get(JobRun, result["jobId"])
assert job is not None and job.status == "failed"
assert "secret-sentinel-/var/lib/private" not in repr(job.model_dump())
engine.dispose()
def test_managed_storage_executor_sanitizes_broker_exception(tmp_path, monkeypatch):
from backend.app.services.deployment_broker import DeploymentBrokerClient
from backend.app.services.enrollment_plane import EnrollmentPlane
engine = _executor_engine(tmp_path)
monkeypatch.setattr(EnrollmentPlane, "get_trust_config", staticmethod(lambda _session: _managed_trust()))
monkeypatch.setattr("backend.app.services.secret_store.get_secret", lambda *_args, **_kwargs: "temporary-bootstrap-secret")
def raise_broker_error(*_args, **_kwargs):
raise RuntimeError("temporary-bootstrap-secret failed under /var/lib/private")
monkeypatch.setattr(DeploymentBrokerClient, "initialize_managed_appdata", raise_broker_error)
with Session(engine) as session:
result = _execute_initializer(session)
assert result["success"] is False
assert result["message"] == "Managed storage initialization was refused or unavailable."
assert "temporary-bootstrap-secret" not in repr(result)
assert "/var/lib/private" not in repr(result)
event = session.get(AuditEvent, result["auditId"])
assert event is not None and event.result == "failed"
assert "temporary-bootstrap-secret" not in repr(event.model_dump())
assert "/var/lib/private" not in repr(event.model_dump())
engine.dispose()