Files
aios/tests/test_owner_authorization_broker.py
T

331 lines
12 KiB
Python

from __future__ import annotations
import json
import os
import stat
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
APP = ROOT / "active-runtime" / "app"
sys.path.insert(0, str(APP))
from authorized_task_writer_provisioning import ( # noqa: E402
ProvisioningError,
)
from owner_authorization_broker import ( # noqa: E402
ACTION,
OwnerAuthorizationBroker,
OwnerAuthorizationBrokerConfig,
OwnerAuthorizationError,
OwnerPrincipal,
)
TASK_ID = "V2R1C6_TASK_SCOPED_WRITER_PROVISIONING"
BRANCH = "agent/v2r1c6-mainline-reconciliation-20260805"
REPOSITORY = "aios/aios"
BASE_OID = "875d41c3101093149414292067fd700dc214d4d3"
OWNER = "aiman"
INTAKE_SCOPE = "commit:create"
CREDENTIAL_ID = "owner-current-exact"
class FakeProvisioner:
def __init__(self) -> None:
self.calls = []
def provision(self, raw, owner):
self.calls.append((dict(raw), owner))
return {
"status": "READY_FOR_CODE_AUTHORING",
"approval_plan_id": "sha256:" + "1" * 64,
"grant_id": "grant-test",
"grant_digest_sha256": "2" * 64,
"signing_key_identity": "runtime-secret-ref:owner-signer",
"branch": BRANCH,
"branch_oid": BASE_OID,
"base_oid": BASE_OID,
"workspace_path": "/tmp/task-writer",
"grant_consumed": True,
"writer_scope": {
"workspace_role": "task-scoped-isolated-writer",
"writes_enabled": True,
"allowed_branch": BRANCH,
"allowed_base_oid": BASE_OID,
"allowed_routes": ["POST /api/commit-file"],
"allow_main_write": False,
"allow_tag_write": False,
"allow_baseline_write": False,
"allow_force_push": False,
},
}
class BlockingProvisioner:
def provision(self, raw, owner):
raise ProvisioningError("AUTHORIZED_BASE_OID_DRIFT", 409)
class SensitiveProvisioner:
def provision(self, raw, owner):
return {
"status": "READY_FOR_CODE_AUTHORING",
"grant_consumed": True,
"access_token": "must-not-be-written",
}
class OwnerAuthorizationBrokerCase(unittest.TestCase):
def setUp(self) -> None:
self.temp = tempfile.TemporaryDirectory()
self.root = Path(self.temp.name)
self.config = OwnerAuthorizationBrokerConfig(
repository_identity=REPOSITORY,
state_root=self.root / "state",
allowed_owner_identities=frozenset({OWNER}),
allowed_intake_scopes=frozenset({INTAKE_SCOPE}),
allowed_credential_ids=frozenset({CREDENTIAL_ID}),
max_expiry_seconds=900,
)
self.provisioner = FakeProvisioner()
def tearDown(self) -> None:
self.temp.cleanup()
def principal(self, **updates):
value = {
"identity": OWNER,
"role": "owner",
"authenticated_scope": INTAKE_SCOPE,
"token_scopes": frozenset({INTAKE_SCOPE, "file:write"}),
"credential_id": CREDENTIAL_ID,
}
value.update(updates)
return OwnerPrincipal(**value)
def request(self, **updates):
value = {
"action": ACTION,
"task_id": TASK_ID,
"repository": REPOSITORY,
"base_ref": "refs/heads/main",
"base_oid": BASE_OID,
"target_branch": BRANCH,
"one_time_use": True,
"expires_in_seconds": 900,
"production_change_authorized": False,
"tag_change_authorized": False,
"baseline_change_authorized": False,
"main_write_authorized": False,
"allowed_operations": [
"create_worktree",
"create_target_branch_if_absent",
"commit_to_target_branch",
"push_target_branch",
],
"denied_operations": [
"direct_main_push",
"force_push",
"tag_create",
"tag_update",
"tag_delete",
"branch_delete",
"repository_admin",
"baseline_write",
"production_deploy",
"production_marker_write",
"history_rewrite",
],
}
value.update(updates)
return value
def broker(self, provisioner=None):
return OwnerAuthorizationBroker(
self.config, provisioner or self.provisioner
)
def test_success_creates_audited_event_and_calls_existing_bridge(self):
result = self.broker().authorize_and_provision(
self.request(), self.principal()
)
self.assertEqual("READY_FOR_CODE_AUTHORING", result["status"])
self.assertTrue(result["owner_authorization_consumed"])
self.assertFalse(result["owner_authorization_idempotent"])
self.assertEqual(1, len(self.provisioner.calls))
forwarded, owner = self.provisioner.calls[0]
self.assertNotIn("action", forwarded)
self.assertEqual(
result["owner_authorization_reference"],
forwarded["owner_authorization_reference"],
)
self.assertEqual(OWNER, owner.identity)
event_path = next(
(self.config.state_root / "events").glob("*/event.json")
)
event = json.loads(event_path.read_text(encoding="utf-8"))
self.assertEqual("CONSUMED", event["status"])
self.assertEqual(TASK_ID, event["request"]["task_id"])
self.assertFalse(event["owner"]["wildcard_scope_used"])
self.assertEqual(0o600, stat.S_IMODE(event_path.stat().st_mode))
self.assertEqual(
0o700,
stat.S_IMODE(self.config.state_root.stat().st_mode),
)
def test_idempotent_retry_does_not_call_provisioner_twice(self):
first = self.broker().authorize_and_provision(
self.request(), self.principal()
)
second = self.broker().authorize_and_provision(
self.request(), self.principal()
)
self.assertEqual(1, len(self.provisioner.calls))
self.assertFalse(first["owner_authorization_idempotent"])
self.assertTrue(second["owner_authorization_idempotent"])
self.assertEqual(
first["owner_authorization_event_id"],
second["owner_authorization_event_id"],
)
def test_non_owner_is_rejected(self):
with self.assertRaises(OwnerAuthorizationError) as captured:
self.broker().authorize_and_provision(
self.request(), self.principal(role="agent")
)
self.assertEqual("OWNER_ROLE_REQUIRED", captured.exception.code)
def test_owner_identity_must_be_explicitly_allowed(self):
with self.assertRaises(OwnerAuthorizationError) as captured:
self.broker().authorize_and_provision(
self.request(), self.principal(identity="other-owner")
)
self.assertEqual(
"OWNER_IDENTITY_NOT_ALLOWED", captured.exception.code
)
def test_wildcard_owner_credential_is_rejected(self):
with self.assertRaises(OwnerAuthorizationError) as captured:
self.broker().authorize_and_provision(
self.request(),
self.principal(token_scopes=frozenset({"*"})),
)
self.assertEqual(
"WILDCARD_OWNER_CREDENTIAL_REJECTED",
captured.exception.code,
)
def test_scope_must_be_provable_by_scope_or_credential_id(self):
with self.assertRaises(OwnerAuthorizationError) as captured:
self.broker().authorize_and_provision(
self.request(),
self.principal(
token_scopes=frozenset(), credential_id=""
),
)
self.assertEqual(
"OWNER_CREDENTIAL_SCOPE_NOT_PROVABLE",
captured.exception.code,
)
result = self.broker().authorize_and_provision(
self.request(),
self.principal(token_scopes=frozenset()),
)
self.assertEqual("READY_FOR_CODE_AUTHORING", result["status"])
def test_writer_provision_cannot_be_used_as_bootstrap_scope(self):
config = OwnerAuthorizationBrokerConfig(
repository_identity=REPOSITORY,
state_root=self.root / "state-two",
allowed_owner_identities=frozenset({OWNER}),
allowed_intake_scopes=frozenset({"writer:provision"}),
allowed_credential_ids=frozenset({CREDENTIAL_ID}),
)
with self.assertRaises(OwnerAuthorizationError) as captured:
OwnerAuthorizationBroker(
config, self.provisioner
).authorize_and_provision(
self.request(),
self.principal(
authenticated_scope="writer:provision",
token_scopes=frozenset({"writer:provision"}),
),
)
self.assertEqual(
"WRITER_PROVISION_SCOPE_BOOTSTRAP_REJECTED",
captured.exception.code,
)
def test_production_capability_is_rejected_before_provisioner(self):
with self.assertRaises(OwnerAuthorizationError) as captured:
self.broker().authorize_and_provision(
self.request(production_change_authorized=True),
self.principal(),
)
self.assertEqual(
"PRODUCTION_CAPABILITY_REJECTED", captured.exception.code
)
self.assertEqual([], self.provisioner.calls)
def test_overbroad_operations_are_rejected(self):
allowed = self.request()["allowed_operations"] + ["tag_create"]
with self.assertRaises(OwnerAuthorizationError) as captured:
self.broker().authorize_and_provision(
self.request(allowed_operations=allowed),
self.principal(),
)
self.assertEqual(
"ALLOWED_OPERATIONS_MISMATCH", captured.exception.code
)
def test_bridge_failure_is_recorded_and_propagated(self):
with self.assertRaises(ProvisioningError) as captured:
self.broker(BlockingProvisioner()).authorize_and_provision(
self.request(), self.principal()
)
self.assertEqual("AUTHORIZED_BASE_OID_DRIFT", captured.exception.code)
event_path = next(
(self.config.state_root / "events").glob("*/event.json")
)
event = json.loads(event_path.read_text(encoding="utf-8"))
self.assertEqual("PROVISIONING_BLOCKED", event["status"])
self.assertEqual(
"AUTHORIZED_BASE_OID_DRIFT", event["error"]
)
def test_sensitive_provisioner_response_is_not_persisted(self):
with self.assertRaises(OwnerAuthorizationError) as captured:
self.broker(SensitiveProvisioner()).authorize_and_provision(
self.request(), self.principal()
)
self.assertEqual(
"PROVISIONER_RESPONSE_CONTAINS_SENSITIVE_DATA",
captured.exception.code,
)
responses = list(
(self.config.state_root / "events").glob("*/response.json")
)
self.assertEqual([], responses)
def test_request_requires_exact_fields(self):
request = self.request()
request["unexpected"] = True
with self.assertRaises(OwnerAuthorizationError) as captured:
self.broker().authorize_and_provision(
request, self.principal()
)
self.assertEqual(
"BROKER_REQUEST_FIELDS_REJECTED", captured.exception.code
)
if __name__ == "__main__":
unittest.main()