Files
aios/active-runtime/app/isolated_writer_path_policy.py
T

423 lines
16 KiB
Python

"""Strict service-level exact-path policy for isolated Git writers."""
from __future__ import annotations
import grp
import hashlib
import json
import os
import re
import stat
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path, PurePosixPath, PureWindowsPath
from typing import Any
from urllib.parse import unquote
MAX_POLICY_BYTES = 65_536
POLICY_FIELDS = frozenset(
{
"schema_version",
"policy_id",
"audience",
"repository_identity",
"allowed_branch",
"allowed_operations",
}
)
OPERATION_FIELDS = frozenset({"exact_paths", "max_content_bytes", "content_profile"})
SUPPORTED_OPERATIONS = frozenset({"commit_file.atomic"})
SUPPORTED_CONTENT_PROFILES = frozenset({"systemd-codex-review-worker-v1"})
_SECRET_RE = re.compile(
r"(?i)(authorization\s*:|bearer\s+|(?:token|password|secret)\s*[:=])"
)
_SYSTEMD_NAME_RE = re.compile(r"[A-Za-z][A-Za-z0-9]*\Z")
_SYSTEMD_MAX_LINE_BYTES = 4096
_SYSTEMD_ALLOWED_DIRECTIVES = {
"Unit": frozenset({"Description", "After", "Wants"}),
"Service": frozenset(
{
"Type",
"User",
"Group",
"WorkingDirectory",
"Environment",
"ExecStart",
"Restart",
"RestartSec",
"NoNewPrivileges",
"PrivateTmp",
"PrivateDevices",
"ProtectSystem",
"ProtectHome",
"ProtectKernelTunables",
"ProtectKernelModules",
"ProtectControlGroups",
"LockPersonality",
"RestrictSUIDSGID",
"RestrictRealtime",
"MemoryDenyWriteExecute",
"RuntimeDirectory",
"StateDirectory",
"CacheDirectory",
"ReadOnlyPaths",
"ReadWritePaths",
"UMask",
}
),
"Install": frozenset({"WantedBy"}),
}
_SYSTEMD_MULTI_DIRECTIVES = frozenset(
{
("Service", "Environment"),
("Service", "ReadOnlyPaths"),
("Service", "ReadWritePaths"),
}
)
_SYSTEMD_EXACT_SINGLE_VALUES = {
("Unit", "Description"): "AIOS Codex Review/Test Worker",
("Unit", "After"): "network-online.target",
("Unit", "Wants"): "network-online.target",
("Service", "Type"): "simple",
("Service", "User"): "aiman",
("Service", "Group"): "gitops",
(
"Service",
"WorkingDirectory",
): "/usr/libexec/aios-codex-review-test-worker/current",
(
"Service",
"ExecStart",
): "/usr/bin/python3 -m task_plane.workers.codex_review_test",
("Service", "Restart"): "on-failure",
("Service", "RestartSec"): "2",
("Service", "NoNewPrivileges"): "true",
("Service", "PrivateTmp"): "true",
("Service", "PrivateDevices"): "true",
("Service", "ProtectSystem"): "strict",
("Service", "ProtectHome"): "true",
("Service", "ProtectKernelTunables"): "true",
("Service", "ProtectKernelModules"): "true",
("Service", "ProtectControlGroups"): "true",
("Service", "LockPersonality"): "true",
("Service", "RestrictSUIDSGID"): "true",
("Service", "RestrictRealtime"): "true",
("Service", "MemoryDenyWriteExecute"): "true",
("Service", "RuntimeDirectory"): "aios-codex-review-test-worker",
("Service", "StateDirectory"): "aios-codex-review-test-worker",
("Service", "CacheDirectory"): "aios-codex-review-test-worker",
("Service", "UMask"): "0077",
("Install", "WantedBy"): "multi-user.target",
}
_SYSTEMD_EXACT_MULTI_VALUES = {
("Service", "Environment"): (
"AIOS_CODEX_WORKER_MODE=shadow",
"AIOS_TASK_PLANE_EXECUTION_ENABLED=false",
"PYTHONDONTWRITEBYTECODE=1",
),
("Service", "ReadOnlyPaths"): (
"/data/aios-gitops/repo.git",
"/usr/libexec/aios-codex-review-test-worker",
),
("Service", "ReadWritePaths"): (
"/run/aios-codex-review-test-worker",
"/var/lib/aios-codex-review-test-worker",
"/var/cache/aios-codex-review-test-worker",
),
}
class PathPolicyError(PermissionError):
pass
@dataclass(frozen=True)
class OperationPolicy:
exact_paths: tuple[str, ...]
max_content_bytes: int
content_profile: str
@dataclass(frozen=True)
class ServicePathPolicy:
schema_version: int
policy_id: str
audience: str
repository_identity: str
allowed_branch: str
allowed_operations: dict[str, OperationPolicy]
sha256: str
def operation(self, name: str) -> OperationPolicy:
try:
return self.allowed_operations[name]
except KeyError:
raise PathPolicyError("isolated_writer_service_operation_rejected") from None
def require_exact_path(self, operation: str, path: str) -> OperationPolicy:
selected = self.operation(operation)
if path not in selected.exact_paths:
raise PathPolicyError("isolated_writer_service_path_rejected")
return selected
def _canonical_json(value: Any) -> bytes:
return (
json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
+ "\n"
).encode("utf-8")
def validate_relative_path_syntax(path: str) -> str:
if not isinstance(path, str):
raise PathPolicyError("isolated_writer_path_rejected")
if not path or path != path.strip() or "\x00" in path or "\\" in path:
raise PathPolicyError("isolated_writer_path_rejected")
try:
decoded = unquote(path, errors="strict")
except (UnicodeDecodeError, ValueError):
raise PathPolicyError("isolated_writer_path_rejected") from None
if decoded != path:
raise PathPolicyError("isolated_writer_path_rejected")
posix = PurePosixPath(path)
windows = PureWindowsPath(path)
parts = path.split("/")
if (
posix.is_absolute()
or windows.is_absolute()
or bool(windows.drive)
or any(part in {"", ".", ".."} for part in parts)
):
raise PathPolicyError("isolated_writer_path_rejected")
return path
def _require_fields(value: dict[str, Any], allowed: frozenset[str], code: str) -> None:
if set(value) != allowed:
raise PathPolicyError(code)
def _parse_policy(document: Any, canonical: bytes) -> ServicePathPolicy:
if not isinstance(document, dict):
raise PathPolicyError("isolated_writer_path_policy_object_required")
_require_fields(document, POLICY_FIELDS, "isolated_writer_path_policy_fields_rejected")
if document["schema_version"] != 1:
raise PathPolicyError("isolated_writer_path_policy_schema_rejected")
text_fields = (
"policy_id",
"audience",
"repository_identity",
"allowed_branch",
)
if any(
not isinstance(document[field], str) or not document[field].strip()
for field in text_fields
):
raise PathPolicyError("isolated_writer_path_policy_value_rejected")
raw_operations = document["allowed_operations"]
if not isinstance(raw_operations, dict) or not raw_operations:
raise PathPolicyError("isolated_writer_path_policy_operations_rejected")
if not set(raw_operations).issubset(SUPPORTED_OPERATIONS):
raise PathPolicyError("isolated_writer_path_policy_operations_rejected")
operations: dict[str, OperationPolicy] = {}
for name, raw in raw_operations.items():
if not isinstance(raw, dict):
raise PathPolicyError("isolated_writer_path_policy_operation_rejected")
_require_fields(
raw,
OPERATION_FIELDS,
"isolated_writer_path_policy_operation_fields_rejected",
)
paths = raw["exact_paths"]
if (
not isinstance(paths, list)
or not paths
or any(not isinstance(item, str) for item in paths)
):
raise PathPolicyError("isolated_writer_path_policy_paths_rejected")
validated = tuple(validate_relative_path_syntax(item) for item in paths)
if len(set(validated)) != len(validated):
raise PathPolicyError("isolated_writer_path_policy_paths_rejected")
maximum = raw["max_content_bytes"]
if not isinstance(maximum, int) or isinstance(maximum, bool) or not 1 <= maximum <= 1_048_576:
raise PathPolicyError("isolated_writer_path_policy_size_rejected")
profile = raw["content_profile"]
if profile not in SUPPORTED_CONTENT_PROFILES:
raise PathPolicyError("isolated_writer_path_policy_profile_rejected")
operations[name] = OperationPolicy(validated, maximum, profile)
return ServicePathPolicy(
schema_version=1,
policy_id=document["policy_id"],
audience=document["audience"],
repository_identity=document["repository_identity"],
allowed_branch=document["allowed_branch"],
allowed_operations=operations,
sha256=hashlib.sha256(canonical).hexdigest(),
)
def load_path_policy(
path: Path,
*,
expected_owner_uid: int = 0,
expected_group_gid: int | None = None,
expected_parent_mode: int = 0o750,
expected_file_mode: int = 0o640,
) -> ServicePathPolicy:
lexical = Path(os.path.abspath(str(path)))
if lexical.is_symlink():
raise PathPolicyError("isolated_writer_path_policy_symlink_rejected")
try:
parent_info = lexical.parent.stat(follow_symlinks=False)
file_info = lexical.stat(follow_symlinks=False)
except FileNotFoundError:
raise PathPolicyError("isolated_writer_path_policy_missing") from None
if stat.S_ISLNK(parent_info.st_mode) or not stat.S_ISDIR(parent_info.st_mode):
raise PathPolicyError("isolated_writer_path_policy_parent_rejected")
if stat.S_ISLNK(file_info.st_mode) or not stat.S_ISREG(file_info.st_mode):
raise PathPolicyError("isolated_writer_path_policy_file_rejected")
if lexical.resolve(strict=True) != lexical:
raise PathPolicyError("isolated_writer_path_policy_symlink_rejected")
if (
parent_info.st_uid != expected_owner_uid
or stat.S_IMODE(parent_info.st_mode) != expected_parent_mode
):
raise PathPolicyError("isolated_writer_path_policy_parent_permissions_rejected")
if (
file_info.st_uid != expected_owner_uid
or stat.S_IMODE(file_info.st_mode) != expected_file_mode
):
raise PathPolicyError("isolated_writer_path_policy_permissions_rejected")
if expected_group_gid is not None and (
parent_info.st_gid != expected_group_gid or file_info.st_gid != expected_group_gid
):
raise PathPolicyError("isolated_writer_path_policy_group_rejected")
if file_info.st_size <= 0 or file_info.st_size > MAX_POLICY_BYTES:
raise PathPolicyError("isolated_writer_path_policy_file_size_rejected")
raw = lexical.read_bytes()
try:
document = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError):
raise PathPolicyError("isolated_writer_path_policy_json_rejected") from None
canonical = _canonical_json(document)
if raw != canonical:
raise PathPolicyError("isolated_writer_path_policy_not_canonical")
return _parse_policy(document, canonical)
@lru_cache(maxsize=1)
def configured_path_policy() -> ServicePathPolicy | None:
configured = os.getenv("AIOS_GITOPS_PATH_POLICY_FILE", "").strip()
if not configured:
return None
if not Path(configured).is_absolute():
raise PathPolicyError("isolated_writer_path_policy_path_rejected")
try:
gitops_gid = grp.getgrnam("gitops").gr_gid
except KeyError:
raise PathPolicyError("isolated_writer_path_policy_group_rejected") from None
loaded = load_path_policy(Path(configured), expected_group_gid=gitops_gid)
if loaded.audience != os.getenv("AIOS_GITOPS_SERVICE_AUDIENCE", ""):
raise PathPolicyError("isolated_writer_path_policy_audience_rejected")
if loaded.repository_identity != os.getenv("AIOS_GITOPS_REPOSITORY_IDENTITY", ""):
raise PathPolicyError("isolated_writer_path_policy_repository_rejected")
if loaded.allowed_branch != os.getenv("AIOS_GITOPS_ALLOWED_BRANCH", ""):
raise PathPolicyError("isolated_writer_path_policy_branch_rejected")
return loaded
def clear_path_policy_cache() -> None:
configured_path_policy.cache_clear()
def require_service_path(operation: str, path: str) -> OperationPolicy:
loaded = configured_path_policy()
if loaded is None:
raise PathPolicyError("isolated_writer_service_path_policy_required")
return loaded.require_exact_path(operation, path)
def configured_path_policy_sha256() -> str | None:
loaded = configured_path_policy()
return None if loaded is None else loaded.sha256
def validate_content(operation: str, path: str, content: str) -> None:
selected = require_service_path(operation, path)
try:
encoded = content.encode("utf-8", errors="strict")
except (AttributeError, UnicodeEncodeError):
raise PathPolicyError("isolated_writer_systemd_profile_rejected") from None
if len(encoded) > selected.max_content_bytes:
raise PathPolicyError("isolated_writer_service_content_too_large")
if selected.content_profile == "systemd-codex-review-worker-v1":
_validate_systemd_codex_review_worker(content)
return
raise PathPolicyError("isolated_writer_path_policy_profile_rejected")
def _validate_systemd_codex_review_worker(content: str) -> None:
if not isinstance(content, str):
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
try:
content.encode("utf-8", errors="strict")
except UnicodeEncodeError:
raise PathPolicyError("isolated_writer_systemd_profile_rejected") from None
if (
"\x00" in content
or "\r" in content
or _SECRET_RE.search(content)
or not content.endswith("\n")
):
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
sections: set[str] = set()
values: dict[tuple[str, str], list[str]] = {}
section: str | None = None
for line in content.splitlines():
if len(line.encode("utf-8")) > _SYSTEMD_MAX_LINE_BYTES:
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
if not line:
continue
if line[0].isspace() or line.startswith(("#", ";")):
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
if line.startswith("[") and line.endswith("]"):
candidate = line[1:-1]
if (
not _SYSTEMD_NAME_RE.fullmatch(candidate)
or candidate not in _SYSTEMD_ALLOWED_DIRECTIVES
or candidate in sections
):
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
sections.add(candidate)
section = candidate
continue
if section is None or "=" not in line:
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
directive, value = line.split("=", 1)
if (
not _SYSTEMD_NAME_RE.fullmatch(directive)
or directive not in _SYSTEMD_ALLOWED_DIRECTIVES[section]
or not value
or value != value.strip()
):
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
key = (section, directive)
if key in values and key not in _SYSTEMD_MULTI_DIRECTIVES:
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
values.setdefault(key, []).append(value)
if sections != set(_SYSTEMD_ALLOWED_DIRECTIVES):
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
if set(values) != set(_SYSTEMD_EXACT_SINGLE_VALUES) | set(
_SYSTEMD_EXACT_MULTI_VALUES
):
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
for key, expected in _SYSTEMD_EXACT_SINGLE_VALUES.items():
if values.get(key) != [expected]:
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
for key, expected in _SYSTEMD_EXACT_MULTI_VALUES.items():
observed = values.get(key, [])
if len(observed) != len(set(observed)) or tuple(observed) != expected:
raise PathPolicyError("isolated_writer_systemd_profile_rejected")