Implement TEN-WP-0011 security layer conformance
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 37s
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 37s
Engine/PIP declaration is now checkable (layer.yaml plus a Tooling-client scan). Writes persist a decision record or the published fail-closed stance, live-lookup freshness is published, events_for is tenant-scoped, and mutation evidence drains to audit-core from a local outbox without blocking the mutation. Sender registration is requested as AUDIT-IN-0002. Boundary-contract amendment is requested as NET-IN-0002. Assistant: grok Assistant-Session: 01a04cea-e5e8-7081-a0fc-808ebbc35fa9
This commit is contained in:
parent
80961af91e
commit
672cf4da6e
40 changed files with 2285 additions and 361 deletions
61
docs/evidence-emission.md
Normal file
61
docs/evidence-emission.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# Mutation evidence emission (TEN-WP-0011-T04)
|
||||
|
||||
Statute §9.6. Companion §6. Current event classes are **attributive**:
|
||||
no control in this engine branches on an event's presence. Role grants
|
||||
as *facts* are live PIP input; the *events* are the trail.
|
||||
|
||||
## Bound
|
||||
|
||||
The local `events` table and the local `audit_outbox` prove that the
|
||||
records they hold were not altered or truncated after arrival. They do
|
||||
not prove an event happened, and absence is not evidence of
|
||||
non-occurrence. They share this service's runtime database credential,
|
||||
so they are not independent custody.
|
||||
|
||||
Independent custody belongs to `audit-core`. This engine POSTs
|
||||
`/v1/events` and holds no SQL, no admin, and no rewrite path against
|
||||
audit-core's store. The external copy therefore cannot be rewritten
|
||||
through tenant-engine's database credential.
|
||||
|
||||
## Trade (declared)
|
||||
|
||||
| Step | Atomic with mutation? | If it fails |
|
||||
| --- | --- | --- |
|
||||
| Insert local `events` row | yes (same transaction) | mutation rolls back |
|
||||
| Insert local `audit_outbox` row | yes (same transaction) | mutation rolls back |
|
||||
| Drain outbox to audit-core | **no** — after commit | mutation already succeeded; row stays pending |
|
||||
|
||||
Emission is **non-blocking**. Unavailable audit-core MUST NOT fail-open
|
||||
a mutation (the fact is already written) and MUST NOT fail-closed a
|
||||
mutation (attributive evidence is not load-bearing). Completeness is
|
||||
not claimed. If a future control starts branching on these events, that
|
||||
class must be reclassified load-bearing before it ships, and this trade
|
||||
revisited.
|
||||
|
||||
## Envelope
|
||||
|
||||
`audit-core.event.v1alpha1`. `source` is `tenant-engine`. See
|
||||
`tenant_engine.audit_core.envelope_for`. Duplicate event ids are 200
|
||||
and not retried; 400/409 dead-letter; 503/transport retry.
|
||||
|
||||
## Credentials
|
||||
|
||||
No secret in Git. Production sender token is projected as
|
||||
`TENANT_ENGINE_AUDIT_CORE_TOKEN_FILE`, routed through `warden route`
|
||||
(`audit-core` sender registration), never through a State Hub message.
|
||||
Sender registration itself is requested as `AUDIT-IN-0002`.
|
||||
|
||||
## Backfill
|
||||
|
||||
**Decision:** no backfill of pre-cutover event classes. Those rows stay
|
||||
in the local table. Reconstructing them into audit-core would mint
|
||||
evidence this engine cannot prove was complete at the time. New
|
||||
mutations from this workplan onward enqueue the outbox.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Env var | Default | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `TENANT_ENGINE_AUDIT_CORE_URL` | unset | Drain is skipped; outbox rows stay pending. |
|
||||
| `TENANT_ENGINE_AUDIT_CORE_TOKEN_FILE` | unset | Bearer token file for POST /v1/events. |
|
||||
| `TENANT_ENGINE_AUDIT_CORE_TIMEOUT_SECONDS` | `3` | Drain timeout. |
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
# flex-auth Integration (TEN-WP-0003)
|
||||
# flex-auth Integration
|
||||
|
||||
`tenant-engine` gates every write through flex-auth's `POST /v1/check`
|
||||
(`flex_auth.FlexAuthCheckClient`, wired in as `authz.FlexAuthWriteAuthorizer`).
|
||||
`tenant-engine` gates every write, and every authorized read, through
|
||||
flex-auth's `POST /v1/check` (`flex_auth.FlexAuthCheckClient`, wired in as
|
||||
`authz.FlexAuthWriteAuthorizer`). Writes are PEP-shaped: no mutation
|
||||
without a decision record or a recorded fail-closed stance
|
||||
(`pep-stance.yaml`).
|
||||
|
||||
## What tenant-engine sends
|
||||
|
||||
|
|
@ -13,56 +16,98 @@ A `CheckRequest` per `flex-auth/schemas/check_request.schema.json`:
|
|||
"tenant": "tenant:friendly:binky",
|
||||
"subject": {"id": "<actor>", "type": "service"},
|
||||
"action": "tenant.create",
|
||||
"resource": {"id": "<tenant_id>", "type": "tenant", "system": "tenant-engine"}
|
||||
"resource": {"id": "<tenant_id>", "type": "tenant", "system": "tenant-engine"},
|
||||
"context": {}
|
||||
}
|
||||
```
|
||||
|
||||
`context` is always empty. This engine never puts `tenant_roles` or any
|
||||
other PIP claim on the check it sends. The check asks whether *this
|
||||
caller* may use *this admin/PIP surface*, not what the tenant is allowed
|
||||
to do on the platform.
|
||||
|
||||
Action → resource-type mapping (must match `flex-auth`'s
|
||||
`FLEX-WP-0008-T01` vocabulary exactly — coordinate values, don't diverge):
|
||||
|
||||
| Action | Resource type |
|
||||
| --- | --- |
|
||||
| `tenant.create` | `tenant` |
|
||||
| `tenant.role.grant` | `role-grant` |
|
||||
| `tenant.role.revoke` | `role-grant` |
|
||||
| `tenant.plan.assign` | `plan-assignment` |
|
||||
| Action | Resource type | Surface |
|
||||
| --- | --- | --- |
|
||||
| `tenant.create` | `tenant` | write |
|
||||
| `tenant.role.grant` | `role-grant` | write |
|
||||
| `tenant.role.revoke` | `role-grant` | write |
|
||||
| `tenant.plan.assign` | `plan-assignment` | write |
|
||||
| `tenant.update` | `tenant` | write |
|
||||
| `tenant.retire` | `tenant` | write |
|
||||
| `tenant.reactivate` | `tenant` | write |
|
||||
| `tenant.grouping.set` | `tenant` | write |
|
||||
| `tenant.guardrail.read` | `guardrail` | read |
|
||||
| `tenant.guardrail.set` | `guardrail` | write |
|
||||
| `tenant.read` | `tenant` | read |
|
||||
| `tenant.role.read` | `role-grant` | read (cache-read) |
|
||||
| `tenant.role.read.live` | `role-grant` | read (live-lookup) |
|
||||
|
||||
## What tenant-engine expects back
|
||||
|
||||
A `DecisionEnvelope` per `flex-auth/schemas/decision_envelope.schema.json`.
|
||||
Only `effect: "allow"` authorizes the write. Every other `effect`
|
||||
(`deny`/`redact`/`audit_only`/`not_applicable`), a non-200 response, a
|
||||
malformed body, or a transport failure/timeout all resolve to **deny** —
|
||||
`FlexAuthCheckClient.is_allowed()` never raises past its own boundary; it
|
||||
always returns a plain `bool`.
|
||||
Only `effect: "allow"` authorizes the action. Every other `effect`, a
|
||||
non-200 response, a malformed body, or a transport failure/timeout all
|
||||
resolve to **deny**. `FlexAuthCheckClient.check()` never raises past its
|
||||
own boundary; it returns a `CheckResult` that is either a decision or a
|
||||
recorded application of the fail-closed stance.
|
||||
|
||||
## The current real state
|
||||
The decision id, request digest, effect, and source are persisted on
|
||||
`authz_records` for every attempt and on the mutation event payload for
|
||||
every successful write. Verdicts are never cached: every call is a new
|
||||
POST (`pep-stance.yaml` `verdict_caching: none`).
|
||||
|
||||
Until `flex-auth/workplans/FLEX-WP-0008-tenant-engine-consumer-integration.md`
|
||||
lands (resource/action vocabulary + an authored policy package), **every
|
||||
check resolves to deny or not_applicable** — verified against real
|
||||
`flex-auth` behavior, not assumed. This is correct fail-closed behavior,
|
||||
not a bug: tenant-engine cannot perform any write against a real `flex-auth`
|
||||
deployment until that policy package exists. Verified locally with a fake
|
||||
HTTP double standing in for `flex-auth` (deny → `403`, allow → `201`,
|
||||
both over real HTTP between two processes) — see `TEN-WP-0003`'s closure
|
||||
notes for the exact commands.
|
||||
## Live-lookup is not cyclic (TEN-WP-0011-T03)
|
||||
|
||||
`GET /tenants/{id}/roles/live` authorizes via `tenant.role.read.live` then
|
||||
reads the store. That check does **not** re-enter tenant-engine:
|
||||
|
||||
1. The CheckRequest this engine sends has empty `context` and does not
|
||||
carry `tenant_roles`.
|
||||
2. `FlexAuthCheckClient` POSTs only `/v1/check`. Proven by
|
||||
`tests/test_pip_claims.py::test_live_lookup_check_does_not_reenter_tenant_engine`.
|
||||
3. flex-auth's tenant-engine policy package
|
||||
(`flex-auth/examples/tenant-engine/policy_package.md`) matches
|
||||
`subject.id` and `action` only. It does not consult tenant capability
|
||||
roles. The package's own scope note says those roles are tenant state
|
||||
a *different* protected system might consult via live-lookup;
|
||||
conflating the two would authorize the wrong thing.
|
||||
4. flex-auth's live-roles adapter is built and unwired
|
||||
(`FLEX-WP-0015`, `flex-auth/docs/tenancy-posture-review.md`): no
|
||||
non-test caller, no current policy consumes `tenant_roles` for a
|
||||
privileged path.
|
||||
|
||||
Together: GET /roles/live → POST /v1/check is a service-identity question.
|
||||
The PDP does not need tenant-engine claims to answer it, so it does not
|
||||
call back.
|
||||
|
||||
Note: `tenant.role.read` / `tenant.role.read.live` / `tenant.read` /
|
||||
`tenant.grouping.set` are not yet in that policy package's `valid_actions`.
|
||||
A live flex-auth will currently `unknown_action` those four. That is
|
||||
itself evidence they are not evaluated via tenant-role claims. Adding
|
||||
them as static service-identity rules (same shape as
|
||||
`tenant.guardrail.read`) is flex-auth work, not a cycle to unwind here.
|
||||
|
||||
## Unreachable-engine stance
|
||||
|
||||
Published in `pep-stance.yaml`. Every scope is `fail_closed`. DefaultDeny
|
||||
(URL unset) and transport failure both apply that stance and record it.
|
||||
Tests assert the file equals shipped behaviour.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Env var | Default | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `TENANT_ENGINE_FLEX_AUTH_URL` | unset | Base URL of a reachable `flex-auth` deployment. When unset, `create_app()` falls back to `authz.DefaultDenyWriteAuthorizer` — no writes ever succeed, which is the correct posture for local/test runs that have no `flex-auth` to call. |
|
||||
| `TENANT_ENGINE_FLEX_AUTH_TIMEOUT_SECONDS` | `3` | Bounded timeout on the synchronous write path — no retries, so a slow deny doesn't become a hang. |
|
||||
| `TENANT_ENGINE_FLEX_AUTH_URL` | unset | Base URL of a reachable `flex-auth` deployment. When unset, `create_app()` falls back to `authz.DefaultDenyWriteAuthorizer`. |
|
||||
| `TENANT_ENGINE_FLEX_AUTH_TIMEOUT_SECONDS` | `3` | Bounded timeout on the synchronous path — no retries. |
|
||||
| `TENANT_ENGINE_FLEX_AUTH_TOKEN_FILE` | unset | Rotating bearer token file, read on each check. |
|
||||
|
||||
## Related
|
||||
|
||||
- `pep-stance.yaml` — published fail-closed map
|
||||
- `pip-claims.yaml` — input-class freshness
|
||||
- `flex-auth/workplans/FLEX-WP-0008-tenant-engine-consumer-integration.md`
|
||||
— the flex-auth-side work this client depends on for real `allow`
|
||||
decisions, and (separately) flex-auth's own live-lookup consumption of
|
||||
`tenant-engine`'s `/roles/live` endpoint for other protected systems.
|
||||
- `key-cape/workplans/KEY-WP-0005-iam-profile-core-claims.md` — the
|
||||
cache-read/`tenant_roles` direction (`key-cape` → `tenant-engine`), not
|
||||
covered by this doc.
|
||||
- `net-kingdom/canon/standards/tenant-engine-boundary-contract_v0.1.md` —
|
||||
the Authorization Contract section this client implements.
|
||||
- `key-cape/workplans/KEY-WP-0005-iam-profile-core-claims.md`
|
||||
- `net-kingdom/canon/standards/tenant-engine-boundary-contract_v0.1.md`
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ kind: intake
|
|||
title: "Externalize tenant-engine audit evidence to audit-core"
|
||||
lane: yellow
|
||||
status: closed
|
||||
outcome: promoted
|
||||
outcome: completed
|
||||
promoted_to: TEN-WP-0011-T04
|
||||
priority: high
|
||||
owner: tenant-engine
|
||||
|
|
@ -58,7 +58,7 @@ kind: intake
|
|||
title: "Remove or authorize the tenant-engine unfiltered event-read interface"
|
||||
lane: red
|
||||
status: closed
|
||||
outcome: promoted
|
||||
outcome: completed
|
||||
promoted_to: TEN-WP-0011-T05
|
||||
priority: high
|
||||
owner: tenant-engine
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue