427 lines
16 KiB
Python
427 lines
16 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
import aios_isolated_writer_policy as writer_policy
|
|
import isolated_writer_path_policy as path_policy
|
|
from isolated_writer_authz import AuthorizationError, _validate_grant
|
|
|
|
|
|
TARGET = "deploy/systemd/aios-codex-review-test-worker.service"
|
|
BRANCH = "patch/codex-review-test-worker-20260724"
|
|
AUDIENCE = "aios-codex-review-worker-writer.service"
|
|
|
|
VALID_UNIT = """\
|
|
[Unit]
|
|
Description=AIOS Codex Review/Test Worker
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=aiman
|
|
Group=gitops
|
|
WorkingDirectory=/usr/libexec/aios-codex-review-test-worker/current
|
|
Environment=AIOS_CODEX_WORKER_MODE=shadow
|
|
Environment=AIOS_TASK_PLANE_EXECUTION_ENABLED=false
|
|
Environment=PYTHONDONTWRITEBYTECODE=1
|
|
ExecStart=/usr/bin/python3 -m task_plane.workers.codex_review_test
|
|
Restart=on-failure
|
|
RestartSec=2
|
|
NoNewPrivileges=true
|
|
PrivateTmp=true
|
|
PrivateDevices=true
|
|
ProtectSystem=strict
|
|
ProtectHome=true
|
|
ProtectKernelTunables=true
|
|
ProtectKernelModules=true
|
|
ProtectControlGroups=true
|
|
LockPersonality=true
|
|
RestrictSUIDSGID=true
|
|
RestrictRealtime=true
|
|
MemoryDenyWriteExecute=true
|
|
RuntimeDirectory=aios-codex-review-test-worker
|
|
StateDirectory=aios-codex-review-test-worker
|
|
CacheDirectory=aios-codex-review-test-worker
|
|
ReadOnlyPaths=/data/aios-gitops/repo.git
|
|
ReadOnlyPaths=/usr/libexec/aios-codex-review-test-worker
|
|
ReadWritePaths=/run/aios-codex-review-test-worker
|
|
ReadWritePaths=/var/lib/aios-codex-review-test-worker
|
|
ReadWritePaths=/var/cache/aios-codex-review-test-worker
|
|
UMask=0077
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
"""
|
|
|
|
|
|
def document(*, paths=None, **updates):
|
|
value = {
|
|
"schema_version": 1,
|
|
"policy_id": "codex-review-worker-path-policy-v1",
|
|
"audience": AUDIENCE,
|
|
"repository_identity": "aios/aios",
|
|
"allowed_branch": BRANCH,
|
|
"allowed_operations": {
|
|
"commit_file.atomic": {
|
|
"exact_paths": paths or [TARGET],
|
|
"max_content_bytes": 32768,
|
|
"content_profile": "systemd-codex-review-worker-v1",
|
|
}
|
|
},
|
|
}
|
|
value.update(updates)
|
|
return value
|
|
|
|
|
|
class ExactPathPolicyTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.saved = dict(os.environ)
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.root = Path(self.temp.name)
|
|
self.policy_dir = self.root / "policies"
|
|
self.policy_dir.mkdir()
|
|
self.policy_dir.chmod(0o750)
|
|
self.policy_file = self.policy_dir / "writer.json"
|
|
self.workspace = self.root / "workspace"
|
|
self.workspace.mkdir(mode=0o700)
|
|
os.environ.update(
|
|
{
|
|
"AIOS_GITOPS_ISOLATED_WRITER_ENFORCEMENT": "required",
|
|
"AIOS_GITOPS_SERVICE_AUDIENCE": AUDIENCE,
|
|
"AIOS_GITOPS_REPOSITORY_IDENTITY": "aios/aios",
|
|
"AIOS_GITOPS_ALLOWED_BRANCH": BRANCH,
|
|
}
|
|
)
|
|
|
|
def tearDown(self):
|
|
path_policy.clear_path_policy_cache()
|
|
os.environ.clear()
|
|
os.environ.update(self.saved)
|
|
self.temp.cleanup()
|
|
|
|
def write_policy(self, value=None):
|
|
payload = document() if value is None else value
|
|
raw = (
|
|
json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
+ "\n"
|
|
)
|
|
self.policy_file.write_text(raw, encoding="utf-8")
|
|
self.policy_file.chmod(0o640)
|
|
return path_policy.load_path_policy(
|
|
self.policy_file,
|
|
expected_owner_uid=os.getuid(),
|
|
expected_group_gid=os.getgid(),
|
|
)
|
|
|
|
def configured(self, loaded):
|
|
return mock.patch.object(
|
|
path_policy,
|
|
"configured_path_policy",
|
|
return_value=loaded,
|
|
)
|
|
|
|
def test_exact_systemd_path_and_exact_token_path_are_allowed(self):
|
|
loaded = self.write_policy()
|
|
with self.configured(loaded):
|
|
self.assertEqual(TARGET, writer_policy.validate_path(TARGET, self.workspace))
|
|
writer_policy.validate_content_for_path(TARGET, VALID_UNIT)
|
|
|
|
def test_token_path_cannot_expand_service_policy(self):
|
|
loaded = self.write_policy(document(paths=["deploy/systemd/other.service"]))
|
|
with self.configured(loaded), self.assertRaisesRegex(
|
|
PermissionError, "isolated_writer_service_path_rejected"
|
|
):
|
|
writer_policy.validate_path(TARGET, self.workspace)
|
|
|
|
def test_service_policy_cannot_expand_token_path(self):
|
|
now = datetime.now(timezone.utc)
|
|
grant = {
|
|
"schema_version": 2,
|
|
"audience": AUDIENCE,
|
|
"source_cidrs": ["127.0.0.1/32"],
|
|
"created_at": (now - timedelta(seconds=5)).isoformat(),
|
|
"not_before": (now - timedelta(seconds=2)).isoformat(),
|
|
"expires_at": (now + timedelta(minutes=5)).isoformat(),
|
|
"revoked": False,
|
|
"allowed_operations": ["commit_file.atomic"],
|
|
"allowed_branch": BRANCH,
|
|
"allowed_paths": ["deploy/systemd/other.service"],
|
|
"approved_base_oid": "a" * 40,
|
|
}
|
|
with mock.patch(
|
|
"isolated_writer_authz._git_is_ancestor", return_value=True
|
|
), self.assertRaisesRegex(AuthorizationError, "writer_token_path_rejected"):
|
|
_validate_grant(
|
|
grant,
|
|
audience=AUDIENCE,
|
|
source_ip="127.0.0.1",
|
|
operation="commit_file.atomic",
|
|
branch=BRANCH,
|
|
path=TARGET,
|
|
workspace=self.workspace,
|
|
current_time=now,
|
|
)
|
|
|
|
def test_other_service_file_is_rejected(self):
|
|
loaded = self.write_policy()
|
|
with self.configured(loaded), self.assertRaisesRegex(
|
|
PermissionError, "isolated_writer_service_path_rejected"
|
|
):
|
|
writer_policy.validate_path(
|
|
"deploy/systemd/other.service", self.workspace
|
|
)
|
|
|
|
def test_path_traversal_is_rejected(self):
|
|
for value in ("deploy/systemd/../x", "../deploy/systemd/x"):
|
|
with self.subTest(value=value), self.assertRaises(PermissionError):
|
|
writer_policy.validate_path(value, self.workspace)
|
|
|
|
def test_url_encoded_traversal_is_rejected(self):
|
|
with self.assertRaises(PermissionError):
|
|
writer_policy.validate_path(
|
|
"deploy/systemd/%2e%2e/x", self.workspace
|
|
)
|
|
|
|
def test_symlink_policy_file_is_rejected(self):
|
|
self.write_policy()
|
|
link = self.policy_dir / "link.json"
|
|
link.symlink_to(self.policy_file)
|
|
with self.assertRaisesRegex(
|
|
path_policy.PathPolicyError, "isolated_writer_path_policy_symlink_rejected"
|
|
):
|
|
path_policy.load_path_policy(
|
|
link,
|
|
expected_owner_uid=os.getuid(),
|
|
expected_group_gid=os.getgid(),
|
|
)
|
|
|
|
def test_world_writable_policy_file_is_rejected(self):
|
|
self.write_policy()
|
|
self.policy_file.chmod(0o646)
|
|
with self.assertRaisesRegex(
|
|
path_policy.PathPolicyError,
|
|
"isolated_writer_path_policy_permissions_rejected",
|
|
):
|
|
path_policy.load_path_policy(
|
|
self.policy_file,
|
|
expected_owner_uid=os.getuid(),
|
|
expected_group_gid=os.getgid(),
|
|
)
|
|
|
|
def test_unknown_policy_field_is_rejected(self):
|
|
value = document()
|
|
value["unexpected"] = True
|
|
with self.assertRaisesRegex(
|
|
path_policy.PathPolicyError,
|
|
"isolated_writer_path_policy_fields_rejected",
|
|
):
|
|
self.write_policy(value)
|
|
|
|
def test_audience_mismatch_is_rejected(self):
|
|
loaded = self.write_policy()
|
|
os.environ["AIOS_GITOPS_PATH_POLICY_FILE"] = str(self.policy_file)
|
|
os.environ["AIOS_GITOPS_SERVICE_AUDIENCE"] = "other.service"
|
|
path_policy.clear_path_policy_cache()
|
|
with mock.patch.object(
|
|
path_policy.grp,
|
|
"getgrnam",
|
|
return_value=mock.Mock(gr_gid=os.getgid()),
|
|
), mock.patch.object(
|
|
path_policy, "load_path_policy", return_value=loaded
|
|
), self.assertRaisesRegex(
|
|
path_policy.PathPolicyError,
|
|
"isolated_writer_path_policy_audience_rejected",
|
|
):
|
|
path_policy.configured_path_policy()
|
|
|
|
def test_branch_mismatch_is_rejected(self):
|
|
loaded = self.write_policy()
|
|
os.environ["AIOS_GITOPS_PATH_POLICY_FILE"] = str(self.policy_file)
|
|
os.environ["AIOS_GITOPS_ALLOWED_BRANCH"] = "patch/other"
|
|
path_policy.clear_path_policy_cache()
|
|
with mock.patch.object(
|
|
path_policy.grp,
|
|
"getgrnam",
|
|
return_value=mock.Mock(gr_gid=os.getgid()),
|
|
), mock.patch.object(
|
|
path_policy, "load_path_policy", return_value=loaded
|
|
), self.assertRaisesRegex(
|
|
path_policy.PathPolicyError,
|
|
"isolated_writer_path_policy_branch_rejected",
|
|
):
|
|
path_policy.configured_path_policy()
|
|
|
|
def assert_profile_rejected(self, content):
|
|
loaded = self.write_policy()
|
|
with self.configured(loaded), self.assertRaisesRegex(
|
|
PermissionError, "isolated_writer_systemd_profile_rejected"
|
|
):
|
|
writer_policy.validate_content_for_path(TARGET, content)
|
|
|
|
def test_disallowed_execution_and_credential_directives_are_rejected(self):
|
|
directives = (
|
|
"ExecStartPre=/usr/bin/id",
|
|
"ExecStartPost=/usr/bin/id",
|
|
"ExecReload=/usr/bin/id",
|
|
"ExecStop=/usr/bin/id",
|
|
"ExecStopPost=/usr/bin/id",
|
|
"ExecCondition=/usr/bin/true",
|
|
"EnvironmentFile=/run/worker.env",
|
|
"PassEnvironment=PATH",
|
|
"LoadCredential=worker:/run/worker",
|
|
"SetCredential=worker:value",
|
|
"ImportCredential=worker",
|
|
"StandardInput=socket",
|
|
"StandardOutput=socket",
|
|
)
|
|
for directive in directives:
|
|
with self.subTest(directive=directive):
|
|
self.assert_profile_rejected(
|
|
VALID_UNIT.replace(
|
|
"Restart=on-failure",
|
|
f"Restart=on-failure\n{directive}",
|
|
)
|
|
)
|
|
|
|
def test_disallowed_namespace_and_capability_directives_are_rejected(self):
|
|
directives = (
|
|
"RootDirectory=/srv/worker",
|
|
"RootImage=/srv/worker.raw",
|
|
"ReadWritePaths=/",
|
|
"BindPaths=/etc:/etc",
|
|
"BindReadOnlyPaths=/etc:/etc",
|
|
"TemporaryFileSystem=/etc",
|
|
"CapabilityBoundingSet=CAP_SYS_ADMIN",
|
|
"AmbientCapabilities=CAP_SYS_ADMIN",
|
|
"DeviceAllow=/dev/sda rw",
|
|
"SupplementaryGroups=docker",
|
|
"Sockets=unsafe.socket",
|
|
)
|
|
for directive in directives:
|
|
with self.subTest(directive=directive):
|
|
self.assert_profile_rejected(
|
|
VALID_UNIT.replace(
|
|
"Restart=on-failure",
|
|
f"Restart=on-failure\n{directive}",
|
|
)
|
|
)
|
|
|
|
def test_duplicate_single_directives_and_sections_are_rejected(self):
|
|
bad_values = (
|
|
VALID_UNIT.replace("User=aiman", "User=aiman\nUser=aiman"),
|
|
VALID_UNIT.replace(
|
|
"ExecStart=/usr/bin/python3 -m task_plane.workers.codex_review_test",
|
|
"ExecStart=/usr/bin/python3 -m task_plane.workers.codex_review_test\n"
|
|
"ExecStart=/usr/bin/python3 -m task_plane.workers.codex_review_test",
|
|
),
|
|
VALID_UNIT + "\n[Service]\nType=simple\n",
|
|
)
|
|
for value in bad_values:
|
|
with self.subTest(value=value):
|
|
self.assert_profile_rejected(value)
|
|
|
|
def test_empty_reset_continuation_crlf_nul_and_long_lines_are_rejected(self):
|
|
bad_values = (
|
|
VALID_UNIT.replace(
|
|
"ExecStart=/usr/bin/python3 -m task_plane.workers.codex_review_test",
|
|
"ExecStart=",
|
|
),
|
|
VALID_UNIT.replace(
|
|
"Restart=on-failure",
|
|
"Restart=on-failure\n continuation-injection",
|
|
),
|
|
VALID_UNIT.replace("\n", "\r\n"),
|
|
VALID_UNIT.replace("Restart=on-failure", "Restart=on-failure\x00"),
|
|
VALID_UNIT.replace(
|
|
"Description=AIOS Codex Review/Test Worker",
|
|
"Description=" + ("A" * 4097),
|
|
),
|
|
)
|
|
for value in bad_values:
|
|
with self.subTest(kind=repr(value[:80])):
|
|
self.assert_profile_rejected(value)
|
|
|
|
def test_unknown_environment_and_production_execution_are_rejected(self):
|
|
bad_values = (
|
|
VALID_UNIT.replace(
|
|
"Environment=PYTHONDONTWRITEBYTECODE=1",
|
|
"Environment=UNKNOWN_KEY=value",
|
|
),
|
|
VALID_UNIT.replace(
|
|
"AIOS_TASK_PLANE_EXECUTION_ENABLED=false",
|
|
"AIOS_TASK_PLANE_EXECUTION_ENABLED=true",
|
|
),
|
|
VALID_UNIT.replace(
|
|
"Environment=PYTHONDONTWRITEBYTECODE=1",
|
|
"Environment=Authorization: Bearer placeholder",
|
|
),
|
|
)
|
|
for value in bad_values:
|
|
with self.subTest(value=value):
|
|
self.assert_profile_rejected(value)
|
|
|
|
def test_identity_working_directory_and_path_prefix_bypasses_are_rejected(self):
|
|
bad_values = (
|
|
VALID_UNIT.replace("User=aiman", "User=root"),
|
|
VALID_UNIT.replace(
|
|
"/usr/libexec/aios-codex-review-test-worker/current",
|
|
"/data/aios-gitops/worktrees/worker",
|
|
),
|
|
VALID_UNIT.replace(
|
|
"ReadWritePaths=/run/aios-codex-review-test-worker",
|
|
"ReadWritePaths=/etc",
|
|
),
|
|
VALID_UNIT.replace(
|
|
"ReadOnlyPaths=/data/aios-gitops/repo.git",
|
|
"ReadOnlyPaths=/data",
|
|
),
|
|
)
|
|
for value in bad_values:
|
|
with self.subTest(value=value):
|
|
self.assert_profile_rejected(value)
|
|
|
|
def test_unknown_section_directive_and_unicode_lookalike_are_rejected(self):
|
|
bad_values = (
|
|
VALID_UNIT.replace("[Install]", "[Socket]"),
|
|
VALID_UNIT.replace(
|
|
"Restart=on-failure",
|
|
"Restart=on-failure\nProtectClock=true",
|
|
),
|
|
VALID_UNIT.replace("ExecStart=", "\u0415xecStart="),
|
|
)
|
|
for value in bad_values:
|
|
with self.subTest(value=value):
|
|
self.assert_profile_rejected(value)
|
|
|
|
def test_invalid_unicode_surrogate_and_missing_final_newline_are_rejected(self):
|
|
for value in (VALID_UNIT + "\ud800", VALID_UNIT.rstrip("\n")):
|
|
with self.subTest(value=repr(value[-30:])):
|
|
self.assert_profile_rejected(value)
|
|
|
|
def test_writer_request_cannot_modify_policy_file(self):
|
|
with self.assertRaises(PermissionError):
|
|
writer_policy.validate_path(
|
|
"etc/aios-gitops/writer-path-policies/codex-review-worker.json",
|
|
self.workspace,
|
|
)
|
|
|
|
def test_coarse_file_and_commit_endpoints_remain_forbidden(self):
|
|
for endpoint in ("/api/file", "/api/commit"):
|
|
with self.subTest(endpoint=endpoint), self.assertRaisesRegex(
|
|
PermissionError, "isolated_writer_operation_forbidden"
|
|
):
|
|
writer_policy.validate_endpoint(
|
|
"POST", endpoint, self.workspace, self.root
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|