Authenticate flex-auth calls with a rotating caller token

Closes the caller side of FLEX-WP-0015. FlexAuthHTTPAdapter reads the
audience-scoped projected ServiceAccount token from a file on every decision,
so hourly rotation needs no restart, and runtime configuration now requires
USER_ENGINE_FLEX_AUTH_TOKEN_FILE.

A missing, empty, or unreadable token file fails closed as a denial without
reaching flex-auth: OSError joins the caught set and an empty read raises.
Coverage proves all three unusable-token cases deny before any request is
made, and that neither the deny reason nor the decision repr carries the
token value.

Tenant-authority reads now identify user-engine as actor `user-engine` under
the protected tenant.read action, keeping tenant ids opaque and URL-encoded.

Contract: docs/flex-auth-caller-identity.md. Full suite: 148 tests, 3
provider-gated skips.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-18 10:53:31 +02:00
parent 6f6bbf5e4a
commit 4622b64061
7 changed files with 174 additions and 8 deletions

View file

@ -62,7 +62,7 @@ class TenantManagementAdapterTests(unittest.TestCase):
self.assertEqual(json.loads(request.data), {
"tenant_id": "tenant:friendly:new",
"identifier": "tenant:friendly:new",
"actor": "tenant-engine",
"actor": "user-engine",
})
self.assertEqual(result.status, "created")
self.assertEqual(result.external_ref, "tenant:friendly:new")
@ -86,7 +86,7 @@ class TenantLifecycleAdapterTests(unittest.TestCase):
)
self.assertEqual(
request.full_url,
"http://tenant-engine/tenants/tenant%3Afriendly%3Abinky",
"http://tenant-engine/tenants/tenant%3Afriendly%3Abinky?actor=user-engine",
)
self.assertEqual(request.get_method(), "GET")
self.assertIsNone(request.data)
@ -108,7 +108,7 @@ class TenantLifecycleAdapterTests(unittest.TestCase):
self.assertEqual(request.headers["Idempotency-key"], "tenant-update-1")
self.assertEqual(json.loads(request.data), {
"metadata": {"display_name": "Binky Ltd"},
"actor": "tenant-engine",
"actor": "user-engine",
"reason": "operator rename",
"correlation_id": "corr-1",
})