feat(cf-005): connect-side serving identity — ServingConfig.identity + transport-identity propagation
Verifies and fixes CF-005 (alktunnels reverse-flow POC W1): the connect-side serving path built channel 0 internally and never set an identity, so a scope-gated serving op could only be satisfied via the payload auth_token. Token is now the fallback (hub-forwarding / browser path); transport/key-based identity is the primary path. - ServingConfig gains identity: Option<Identity> — the explicit override (remediation a). Semver-relevant struct-literal change → 0.7.0 (minor bump at 0.x, wire surface unchanged). - from_connection_with_serving propagates the transport Connection::identity() to the channel-0 connection via set_identity before the serving loop starts (remediation b) — mirrors the accept side's install-hook set_identity; process-local, nothing new on the wire. - Dispatch identity precedence on the serving loop: payload auth_token → identity_provider, then ServingConfig.identity, then transport identity; identity-less dispatch still fails closed (FORBIDDEN). - Public core::auth::NoopIdentityProvider (resolves nothing; the ServingConfig::default() provider — three private test copies existed). - Regression gates: four cf005_* e2e tests (transport propagation, override precedence, identity-less denial, token fallback + precedence). - Ledger CF-005 → resolved; ADR-022 §connect-side-serving amended; README example updated; changelog 0.7.0. Verification: cargo test (629) + --all-features (646), clippy (all-targets, all-features, -D warnings), fmt --check, doc --no-deps, wasm32 check, semver-checks (no update required at 0.7.0), publish --dry-run.
This commit is contained in:
1 parent
0d287a97b0
commit
db5530ed34
8 files changed
+540
-17
No files matched your search
@@ -402,12 +402,32 @@ surfaces discovery failure as `AdapterError::DiscoveryFailed`).
|
||||
`ChannelClient::from_connection_with_serving(connection,
|
||||
Option<ServingConfig>)` — with `None` (the pure-consumer default) the
|
||||
read pump resolves outbound pendings only (previous behavior). With
|
||||
`Some(ServingConfig { registry, identity_provider })` the read pump
|
||||
becomes the full-duplex serving loop (`Dispatcher::serve_single_stream`):
|
||||
inbound `call.requested` frames dispatch against the configured
|
||||
registry and resolve back to the peer; outbound pendings still resolve
|
||||
in the same loop. Serving is opt-in because a pure consumer has no
|
||||
registry to serve; the *protocol* is symmetric, the *API* is explicit.
|
||||
`Some(ServingConfig { registry, identity_provider, identity })` the
|
||||
read pump becomes the full-duplex serving loop
|
||||
(`Dispatcher::serve_single_stream`): inbound `call.requested` frames
|
||||
dispatch against the configured registry and resolve back to the peer;
|
||||
outbound pendings still resolve in the same loop. Serving is opt-in
|
||||
because a pure consumer has no registry to serve; the *protocol* is
|
||||
symmetric, the *API* is explicit.
|
||||
|
||||
**Caller identity for the serving dispatch (CF-005, 2026-09-07).**
|
||||
`from_connection_with_serving` builds channel 0 internally, so before
|
||||
the remediation there was no capture point for the
|
||||
transport-authenticated peer: the serving dispatch resolved identity
|
||||
only from the payload `auth_token`, and a scope-gated serving op could
|
||||
not authenticate a key-based (mTLS/QUIC) peer by transport identity.
|
||||
The remediation makes the identity resolution, in precedence order:
|
||||
(1) the payload `auth_token` → `ServingConfig.identity_provider`
|
||||
(hub-forwarding and browser-token path, ADR-017 §7); (2) the
|
||||
`ServingConfig.identity` override (explicit, e.g. an
|
||||
assembly-layer-resolved principal); (3) the transport connection's
|
||||
identity, propagated automatically — `from_connection_with_serving`
|
||||
copies `connection.identity()` onto the channel-0 connection via
|
||||
`set_identity` before the serving loop starts, mirroring the accept
|
||||
side, where the adapter hands the install hook the transport
|
||||
`AuthContext` and the hook sets the channel-0 identity. With no
|
||||
identity from any path the dispatch runs identity-less and
|
||||
`AccessControl::check` fails closed (`FORBIDDEN`).
|
||||
|
||||
Direction disambiguation in the loop is by table membership, not
|
||||
framing: an id that is one of *our* outbound pendings resolves there;
|
||||
|
||||
@@ -11,7 +11,13 @@ Format: date | found-in (alkhttp context) | severity | status.
|
||||
|
||||
## Open
|
||||
|
||||
### CF-005 — connect-side serving path (`from_connection_with_serving`) has no caller-identity capture; scope-gated ops are satisfiable only via payload `auth_token` (2026-09-07)
|
||||
(none)
|
||||
|
||||
---
|
||||
|
||||
## Resolved
|
||||
|
||||
### CF-005 — connect-side serving path (`from_connection_with_serving`) has no caller-identity capture; scope-gated ops are satisfiable only via payload `auth_token` (2026-09-07) — RESOLVED 2026-09-07
|
||||
|
||||
- **Found in:** alktunnels reverse-flow POC
|
||||
(`alktunnels-reverse-poc`, summary at
|
||||
@@ -43,16 +49,41 @@ Format: date | found-in (alkhttp context) | severity | status.
|
||||
topologies (from_call's ADR-017 §7 token path), but a gap for
|
||||
direct connect-side serving where the transport already
|
||||
authenticated the peer.
|
||||
- **Candidate remediations (upstream's call):** (a) `ServingConfig`
|
||||
gains an optional identity (or the channel-0 connection is exposed
|
||||
for `set_identity` before the serving loop starts); (b) documented
|
||||
token-only posture for connect-side serving. Either is additive.
|
||||
- **Status:** open — filed 2026-09-07 (alktunnels reverse-flow POC W1).
|
||||
- **Verification:** confirmed statically (the chain
|
||||
`Connection::from_source` → empty `OnceLock` identity →
|
||||
`dispatch_start` → `resolve_identity(None, payload)` →
|
||||
`AccessControl::check(None)` fails closed) and empirically (e2e
|
||||
duplex tests reproducing the POC shape; the tokenless scope-gated
|
||||
call was denied `FORBIDDEN: authentication required` while the
|
||||
token path succeeded).
|
||||
- **Fix (both remediations (a) + (b), 2026-09-07):** the caller
|
||||
identity for the connect-side serving dispatch resolves in
|
||||
precedence order — (1) payload `auth_token` →
|
||||
`ServingConfig.identity_provider` (unchanged; the hub-forwarding /
|
||||
browser-token fallback, ADR-017 §7); (2) new
|
||||
`ServingConfig.identity: Option<Identity>` — the explicit override
|
||||
(remediation (a)); (3) the transport connection's identity,
|
||||
propagated automatically: `from_connection_with_serving` copies
|
||||
`connection.identity()` to the channel-0 connection via
|
||||
`set_identity` before the serving loop starts (remediation (b) —
|
||||
the native-client mTLS/QUIC key-based path; mirrors the accept
|
||||
side's install-hook `set_identity`). Also added the public
|
||||
`core::auth::NoopIdentityProvider` (resolves nothing) — the
|
||||
identity-less posture and the `ServingConfig::default()`
|
||||
identity provider; three private test copies of it existed.
|
||||
- **Behavior change (semver-relevant):** `ServingConfig` gained an
|
||||
`identity` field — struct literals must add `identity: None`
|
||||
(0.x; changelog note). The call half is unaffected
|
||||
(`spawn_dispatch` takes the consumer-owned `Connection`, so the
|
||||
consumer can `set_identity` before wrapping).
|
||||
- The corollary (`register_openable`'s closed-over
|
||||
establishment-time `AuthContext` vs the per-call opener identity)
|
||||
remains open as a design follow-up — it affects the establisher /
|
||||
pump handler seam, not the ACL gate; tracked separately.
|
||||
- **Status:** resolved — 2026-09-07 (regression gates: `cf005_*`
|
||||
tests in `src/channels/client.rs`).
|
||||
|
||||
---
|
||||
|
||||
## Resolved
|
||||
|
||||
### CF-004 — `services_schema_handler` discloses Internal/ACL-restricted op specs — no visibility or AccessControl check (2026-08-30) — RESOLVED 2026-08-31
|
||||
|
||||
- **Found in:** alkhttp Review 002, finding PRJ-16
|
||||
|
||||
Reference in new issue
Block a user