ACL state per context — consume hero_osis context list for access control #21

Closed
opened 2026-04-12 15:19:28 +00:00 by timur · 3 comments
Owner

Companion to lhumina_code/hero_osis#21.

Context

hero_osis now owns the authoritative list of contexts (namespaces/buckets for data isolation). The Context rootobject in the base domain has been extended with:

  • description — human-readable purpose
  • tags — grouping/filtering
  • db_path — filesystem path OSIS uses for this context's database

The admin context (X-Hero-Context: 0) is the canonical holder of the list; all context CRUD flows through base.context_* RPC methods in hero_osis.

See the referenced comment for the updated routing model (header-based contexts, claims-based auth, hero_router as sole TCP entry): lhumina_code/hero_rpc#13 (comment)

What hero_proxy needs to do

  1. Consume the context list from hero_osis. On startup (and on a refresh interval / event signal), call base.context_list against context 0 to learn which contexts exist and their metadata.
  2. Maintain per-context ACL state. Proxy keeps its own local state (separate from hero_osis) mapping each context to:
    • Allowed identities / claims
    • Method allow/deny rules
    • Any other access-control policy the proxy enforces
  3. Enforce access control on every request. Before forwarding a request tagged with X-Hero-Context: <n>, verify the caller is authorized for that context per local ACL state. Reject with an appropriate error otherwise.
  4. Decide on ACL storage. Review how hero_proxy currently keeps state and propose where ACLs live (embedded DB, config file, a hero_osis domain, etc.). Document the choice in this issue before implementing.
  5. Handle context lifecycle events. When a context is created/deleted in hero_osis, proxy should react (drop stale ACLs, seed defaults for new contexts). Decide on push (event) vs. pull (polling) model.

Deliverables

  • Design note in this issue: where ACL state lives + sync model (push/pull) with hero_osis
  • Client code in hero_proxy that calls base.context_list on hero_osis
  • Per-context ACL store + enforcement in the request path
  • Tests covering: unknown context → reject, known context + wrong claims → reject, known context + right claims → allow
  • Docs update

Notes

  • Per the updated architecture, hero_router is the sole TCP entry point; hero_proxy's role in that picture should be clarified as part of the design step. If hero_router is absorbing the proxy responsibilities, this work may need to land there instead — flag that before implementing.
  • Trust model reminder: no X-Hero-Claims header = FULL TRUST (internal call). Claims present = restricted. ACL enforcement should only kick in on the restricted path.
Companion to lhumina_code/hero_osis#21. ## Context hero_osis now owns the authoritative list of **contexts** (namespaces/buckets for data isolation). The `Context` rootobject in the `base` domain has been extended with: - `description` — human-readable purpose - `tags` — grouping/filtering - `db_path` — filesystem path OSIS uses for this context's database The admin context (X-Hero-Context: 0) is the canonical holder of the list; all context CRUD flows through `base.context_*` RPC methods in hero_osis. See the referenced comment for the updated routing model (header-based contexts, claims-based auth, hero_router as sole TCP entry): https://forge.ourworld.tf/lhumina_code/hero_rpc/issues/13#issuecomment-17993 ## What hero_proxy needs to do 1. **Consume the context list from hero_osis.** On startup (and on a refresh interval / event signal), call `base.context_list` against context 0 to learn which contexts exist and their metadata. 2. **Maintain per-context ACL state.** Proxy keeps its own local state (separate from hero_osis) mapping each context to: - Allowed identities / claims - Method allow/deny rules - Any other access-control policy the proxy enforces 3. **Enforce access control on every request.** Before forwarding a request tagged with `X-Hero-Context: <n>`, verify the caller is authorized for that context per local ACL state. Reject with an appropriate error otherwise. 4. **Decide on ACL storage.** Review how hero_proxy currently keeps state and propose where ACLs live (embedded DB, config file, a hero_osis domain, etc.). Document the choice in this issue before implementing. 5. **Handle context lifecycle events.** When a context is created/deleted in hero_osis, proxy should react (drop stale ACLs, seed defaults for new contexts). Decide on push (event) vs. pull (polling) model. ## Deliverables - [ ] Design note in this issue: where ACL state lives + sync model (push/pull) with hero_osis - [ ] Client code in hero_proxy that calls `base.context_list` on hero_osis - [ ] Per-context ACL store + enforcement in the request path - [ ] Tests covering: unknown context → reject, known context + wrong claims → reject, known context + right claims → allow - [ ] Docs update ## Notes - Per the updated architecture, **hero_router** is the sole TCP entry point; hero_proxy's role in that picture should be clarified as part of the design step. If hero_router is absorbing the proxy responsibilities, this work may need to land there instead — flag that before implementing. - Trust model reminder: no `X-Hero-Claims` header = FULL TRUST (internal call). Claims present = restricted. ACL enforcement should only kick in on the restricted path.
Author
Owner

Design note (per deliverable #1)

Where ACL state lives

Keep it in the existing SQLite DB (hero_proxy.db). Rationale:

  • roles.contexts column (comma-separated) already filters claims by context
  • authz::resolve_claims_for_user() already accepts a context param and applies the filter
  • No need for a new storage layer — we just need to (a) know which contexts exist, (b) know which context a request belongs to

Schema additions:

  • New table contexts — a cached mirror of hero_osis's context list (not the source of truth, just a lookup for validation + UI display)
    • sid TEXT PRIMARY KEY — hero_osis SID
    • name TEXT NOT NULL UNIQUE — the integer string used in X-Hero-Context (e.g. "0", "1")
    • description TEXT, tags TEXT (comma-separated), db_path TEXT
    • synced_at TEXT — timestamp of last sync from hero_osis
  • New column on domain_routes: context TEXT NOT NULL DEFAULT '0' — declares which context this route forwards to. Overrides the hardcoded "0" at proxy.rs:548.

Sync model: pull (polling), not push

hero_osis has no event/pubsub mechanism today. Start with a background task:

  • On startup: one-shot sync of context list from hero_osis
  • Every CONTEXT_SYNC_INTERVAL_SECS (default 60s): refresh
  • Strategy: list all contexts from hero_osis (base.context_listbase.context_get per sid), diff against local contexts table, upsert new/changed, delete ones that disappeared
  • Connection: BaseClient::new("unix://$HERO_SOCKET_DIR/hero_osis/rpc.sock", "0") — always query via context 0 (the admin context, per hero_osis#21)
  • Resilience: sync failures logged as warnings, don't crash proxy. Stale cache is fine — worst case, a new context isn't reachable until next tick.

Push model can be added later if/when hero_osis grows an event stream.

Enforcement in the request path

Replace the hardcoded "0" at proxy.rs:548 with:

  1. Read domain_route.context (new column) as the intended context for this route
  2. Look up in local contexts cache — if not present, reject with 404 Not Found (leaks less than 403; the request simply has nowhere to go)
  3. Pass the validated context to resolve_claims_for_user(..., context=Some(&ctx)) — existing code already filters roles by context
  4. Inject X-Hero-Context: <ctx> — already stripped from incoming requests for security, we own the value

Trust model reminder: internal calls without X-Hero-Claims still get FULL TRUST at the service. The proxy's ACL layer only kicks in for authenticated external requests. No change to that.

Scope of this issue (Phase 1)

  • Design note (this comment)
  • DB migration: contexts table + domain_routes.context column
  • hero_osis_sdk dependency with base feature
  • context_sync module with background refresh task
  • proxy.rs: context resolution + validation + injection
  • Tests: unknown context → 404, known context + matching claims → allow, known context + non-matching claims → claims filtered out

Explicitly out of scope (follow-up issues)

  • UI for managing domain_routes.context in hero_proxy_ui
  • Push-based sync (event stream from hero_osis)
  • Per-context rate limiting / quotas
  • Migration of existing roles.contexts values (those are still honored; no breaking change)

Router question — still open

Per hero_rpc#13, hero_router is the sole TCP entry point. Today hero_proxy is still the active ingress on ports 9997/9996. Proceeding with implementation in hero_proxy because it's the current runtime reality. If hero_router absorbs ingress later, this code moves with the proxy logic; the DB schema + sync module are reusable.

## Design note (per deliverable #1) ### Where ACL state lives **Keep it in the existing SQLite DB (`hero_proxy.db`).** Rationale: - `roles.contexts` column (comma-separated) already filters claims by context - `authz::resolve_claims_for_user()` already accepts a `context` param and applies the filter - No need for a new storage layer — we just need to (a) know which contexts exist, (b) know which context a request belongs to Schema additions: - New table `contexts` — a **cached mirror** of hero_osis's context list (not the source of truth, just a lookup for validation + UI display) - `sid TEXT PRIMARY KEY` — hero_osis SID - `name TEXT NOT NULL UNIQUE` — the integer string used in `X-Hero-Context` (e.g. "0", "1") - `description TEXT`, `tags TEXT` (comma-separated), `db_path TEXT` - `synced_at TEXT` — timestamp of last sync from hero_osis - New column on `domain_routes`: `context TEXT NOT NULL DEFAULT '0'` — declares which context this route forwards to. Overrides the hardcoded `"0"` at proxy.rs:548. ### Sync model: pull (polling), not push hero_osis has no event/pubsub mechanism today. Start with a background task: - On startup: one-shot sync of context list from hero_osis - Every `CONTEXT_SYNC_INTERVAL_SECS` (default 60s): refresh - Strategy: list all contexts from hero_osis (`base.context_list` → `base.context_get` per sid), diff against local `contexts` table, upsert new/changed, delete ones that disappeared - Connection: `BaseClient::new("unix://$HERO_SOCKET_DIR/hero_osis/rpc.sock", "0")` — always query via context 0 (the admin context, per hero_osis#21) - Resilience: sync failures logged as warnings, don't crash proxy. Stale cache is fine — worst case, a new context isn't reachable until next tick. Push model can be added later if/when hero_osis grows an event stream. ### Enforcement in the request path Replace the hardcoded `"0"` at proxy.rs:548 with: 1. Read `domain_route.context` (new column) as the intended context for this route 2. Look up in local `contexts` cache — if not present, reject with **404 Not Found** (leaks less than 403; the request simply has nowhere to go) 3. Pass the validated context to `resolve_claims_for_user(..., context=Some(&ctx))` — existing code already filters roles by context 4. Inject `X-Hero-Context: <ctx>` — already stripped from incoming requests for security, we own the value Trust model reminder: internal calls without `X-Hero-Claims` still get FULL TRUST at the service. The proxy's ACL layer only kicks in for authenticated external requests. No change to that. ### Scope of this issue (Phase 1) - [x] Design note (this comment) - [ ] DB migration: `contexts` table + `domain_routes.context` column - [ ] `hero_osis_sdk` dependency with `base` feature - [ ] `context_sync` module with background refresh task - [ ] proxy.rs: context resolution + validation + injection - [ ] Tests: unknown context → 404, known context + matching claims → allow, known context + non-matching claims → claims filtered out ### Explicitly out of scope (follow-up issues) - UI for managing `domain_routes.context` in hero_proxy_ui - Push-based sync (event stream from hero_osis) - Per-context rate limiting / quotas - Migration of existing `roles.contexts` values (those are still honored; no breaking change) ### Router question — still open Per hero_rpc#13, hero_router is the sole TCP entry point. Today hero_proxy is still the active ingress on ports 9997/9996. **Proceeding with implementation in hero_proxy** because it's the current runtime reality. If hero_router absorbs ingress later, this code moves with the proxy logic; the DB schema + sync module are reusable.
Author
Owner

First-pass implementation in #22.

Delivered:

  • Design note (comment above)
  • Local context cache + sync task against hero_osis rpc.sock
  • Per-context ACL enforcement in dispatch_domain_route (404 on unknown context; claims filtered by route.context)
  • Unit tests (6) + existing integration tests still pass (26)

Deferred to follow-ups:

  • Exposing context on AddDomainRoute/UpdateDomainRoute + SDK/UI
  • Push-based sync from hero_osis
  • Per-context rate limiting / quotas
  • hero_router vs hero_proxy ownership question (per hero_rpc#13) — unchanged scope
First-pass implementation in #22. Delivered: - [x] Design note (comment above) - [x] Local context cache + sync task against hero_osis rpc.sock - [x] Per-context ACL enforcement in dispatch_domain_route (404 on unknown context; claims filtered by route.context) - [x] Unit tests (6) + existing integration tests still pass (26) Deferred to follow-ups: - Exposing `context` on AddDomainRoute/UpdateDomainRoute + SDK/UI - Push-based sync from hero_osis - Per-context rate limiting / quotas - hero_router vs hero_proxy ownership question (per hero_rpc#13) — unchanged scope
timur closed this issue 2026-04-12 16:32:07 +00:00
Author
Owner

Closing. The per-route model from #22 was the wrong shape (context is a header dimension tied to identity, not hostname, per hero_skills/hero_os_architecture/context_and_security.md). Corrected in #23/#24: context now sourced from users.context, injected as X-Hero-Context after each auth branch resolves a user. Follow-ups (admin RPC/UI for users.context, multi-context users, live validation at write time) tracked on #23 or separate issues when needed.

Closing. The per-route model from #22 was the wrong shape (context is a header dimension tied to identity, not hostname, per hero_skills/hero_os_architecture/context_and_security.md). Corrected in #23/#24: context now sourced from `users.context`, injected as `X-Hero-Context` after each auth branch resolves a user. Follow-ups (admin RPC/UI for users.context, multi-context users, live validation at write time) tracked on #23 or separate issues when needed.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_proxy#21
No description provided.