331 lines
12 KiB
Python
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()
|