feat(cf-006/cf-007): per-call opener identity for open-op hooks; ADR-016 code-list sweep
Batch-fixes every remaining open alkcall-side finding from downstream consumers so 0.7.0 is the only release they need to absorb. - CF-006 (the CF-005 corollary): run_open_wrapper derives a per-call AuthContext — the opener's dispatch-resolved identity (the same identity the ACL gate and cap check saw) overlaid onto the install-time context — and passes it to both the establisher and the pump handler. Identity-less calls keep the install-time identity (no synthetic-anonymous rewrite); transport-truthful fields are never rewritten. Signatures unchanged — behavior-only; identical on per-connection registries, hub-forwarded opens now show the end client. Gates: open_wrapper_overlays_per_call_identity_on_install_time_auth + open_wrapper_keeps_install_time_identity_when_call_identityless. - CF-007 (alkhttp review 006 Part C doc drift): ADR-016 amended to the eight-code list — ALREADY_EXISTS + CONNECTION_CLOSED in the Context, §3 table, and from_openapi collision rule; new §2a documents the undelivered-vs-ambiguous write-failure distinction. - ChannelPlan type doc now states the Send + Sync payload constraint (alktunnels POC F-1 re-derived it by compiler error). - Ledger: CF-006 and CF-007 filed + resolved; CF-005's corollary note points at CF-006; the Open section is empty. ADR-049 gains the per-call-identity note. Changelog 0.7.0 covers the batch. Sweep result (all downstream reviews): no other open alkcall items — alktunnels W3/F-2 are downstream-by-design, alktty R3/P14 are closed constraints, alknet has none; OQ-24/37/39/40/41 stay deferred-by-design (no consumer pull yet). Verification: cargo test (631) + --all-features (648), clippy (all-targets, all-features, -D warnings), fmt --check, doc --no-deps, wasm32 check, semver-checks (no update required), publish --dry-run.
This commit is contained in:
+35
-8
@@ -6,10 +6,12 @@ this crate adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [0.7.0] - 2026-09-07
|
||||
|
||||
The connect-side caller-identity seam (CF-005): a connect-side serving
|
||||
op can now authenticate the transport-authenticated peer by key-based
|
||||
identity (mTLS/QUIC), not only by payload `auth_token`. The wire
|
||||
surface is unchanged.
|
||||
The connect-side caller-identity seam (CF-005) plus the full sweep of
|
||||
every open alkcall finding from downstream consumers (CF-006, CF-007):
|
||||
a connect-side serving op can now authenticate the
|
||||
transport-authenticated peer by key-based identity (mTLS/QUIC), the
|
||||
open-op establisher/pump handler see the per-call opener identity,
|
||||
and the ADR-016 code list is current. The wire surface is unchanged.
|
||||
|
||||
### Added
|
||||
|
||||
@@ -39,11 +41,36 @@ surface is unchanged.
|
||||
`AccessControl::check` fails closed (`FORBIDDEN`) — unchanged. The
|
||||
identity resolution is process-local (`set_identity` on the
|
||||
internal channel-0 `Connection`); nothing new crosses the transport.
|
||||
Regression gates: the four `cf005_*` tests in
|
||||
- **The open-op establisher and pump handler see the per-call opener
|
||||
identity (CF-006 — the CF-005 corollary).** `run_open_wrapper`
|
||||
derives a per-call `AuthContext` — the opener's dispatch-resolved
|
||||
identity overlaid onto the install-time context — and passes it to
|
||||
both the establisher (ADR-049 §1) and the pump handler
|
||||
(`OpenHandler`). Identity-less calls keep the install-time
|
||||
identity; transport-truthful fields (`alpn`, `remote_addr`,
|
||||
`tls_client_fingerprint`) are never rewritten. Signatures are
|
||||
unchanged — behavior-only; on a per-connection registry (ADR-047
|
||||
§4) this is identical to before, and on hub-forwarded opens the
|
||||
establisher now sees the end client instead of the hub.
|
||||
- **ADR-016 amended: the protocol-code list is eight codes (CF-007 —
|
||||
alkhttp review 006 Part C doc drift).** `ALREADY_EXISTS`
|
||||
(non-retryable, `op/register` collision) and `CONNECTION_CLOSED`
|
||||
(retryable, provably-undelivered call) are now in the Context
|
||||
paragraph, the §3 table, and the `from_openapi` collision rule; §2a
|
||||
documents the undelivered-vs-ambiguous write-failure distinction.
|
||||
Doc-only.
|
||||
- **`ChannelPlan` type doc carries the `Send + Sync` payload
|
||||
constraint** (alktunnels POC F-1 — a non-`Sync` boxed handle needs
|
||||
a wrapper; documented so the next consumer doesn't re-derive it by
|
||||
compiler error).
|
||||
- Regression gates: the four `cf005_*` tests in
|
||||
`src/channels/client.rs` (transport propagation, override
|
||||
precedence, identity-less failure, token fallback + precedence).
|
||||
Ledger: `docs/reviews/consumer-findings-ledger.md` CF-005 →
|
||||
resolved. Spec: ADR-022 §connect-side-serving.
|
||||
precedence, identity-less failure, token fallback + precedence) and
|
||||
the two open-op identity-overlay tests in
|
||||
`src/channels/operations.rs`. Ledger: CF-005, CF-006, CF-007 →
|
||||
resolved (the ledger's Open section is now empty). Specs: ADR-022
|
||||
§connect-side-serving, ADR-016 (code list), ADR-049 establisher
|
||||
doc note.
|
||||
|
||||
## [0.6.0] - 2026-09-07
|
||||
|
||||
|
||||
@@ -2,23 +2,30 @@
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (amended by ADR-021 — protocol-level code list extended to six)
|
||||
Accepted (amended by ADR-021 — protocol-level code list extended to six;
|
||||
amended 2026-09-07 — extended to eight, see §2a below)
|
||||
|
||||
## Context
|
||||
|
||||
The `OperationSpec` in alknet-call has `input_schema` and `output_schema` but
|
||||
no `error_schemas`. The `call.error` payload (call-protocol.md L128–134)
|
||||
carries a `code` and `message`, where `code` is one of six infrastructure
|
||||
carries a `code` and `message`, where `code` is one of eight infrastructure
|
||||
codes: `NOT_FOUND`, `FORBIDDEN`, `INVALID_INPUT`, `INVALID_OPERATION_TYPE`,
|
||||
`INTERNAL`, `TIMEOUT`.
|
||||
`INTERNAL`, `TIMEOUT`, `ALREADY_EXISTS`, `CONNECTION_CLOSED`.
|
||||
|
||||
These six codes cover **protocol-level failures** — the call protocol
|
||||
These eight codes cover **protocol-level failures** — the call protocol
|
||||
itself can always fail to find an operation, deny access, reject bad input,
|
||||
reject the wrong dispatch method for the operation type, time out, or hit
|
||||
an internal error. They are emitted by the dispatch machinery (the registry,
|
||||
reject the wrong dispatch method for the operation type, time out, hit an
|
||||
internal error, reject a registration collision, or report a provably
|
||||
undelivered call. They are emitted by the dispatch machinery (the registry,
|
||||
the adapter), not by operation handlers. `INVALID_OPERATION_TYPE` was added
|
||||
by ADR-021 (streaming handler for subscriptions — `invoke()` called on a
|
||||
`Subscription`, or `invoke_streaming()` on a `Query`/`Mutation`).
|
||||
`ALREADY_EXISTS` was added by the ADR-022 collision sub-amendment
|
||||
(2026-09-04 — `op/register` rejects a peer-announce collision; non-retryable,
|
||||
registration is state). `CONNECTION_CLOSED` was added by the consumer
|
||||
findings ledger CF-001 (2026-08-30 — write failures on provably undelivered
|
||||
`call.requested` frames; **retryable** — see §2a).
|
||||
|
||||
But operations also have **domain-level failures** that are not covered:
|
||||
|
||||
@@ -186,7 +193,7 @@ optional-array convention.
|
||||
|
||||
### 3. Protocol-level vs operation-level error codes
|
||||
|
||||
The six existing codes are **protocol-level** — emitted by the dispatch
|
||||
The eight existing codes are **protocol-level** — emitted by the dispatch
|
||||
machinery, not by handlers:
|
||||
|
||||
| Code | Emitted by | Meaning |
|
||||
@@ -197,6 +204,20 @@ machinery, not by handlers:
|
||||
| `INVALID_OPERATION_TYPE` | Registry / `OperationEnv` | Wrong dispatch path for the operation's type (`invoke()` on a `Subscription`, `invoke_streaming()` on a `Query`/`Mutation`, or `OperationEnv::invoke()` on a `Subscription` during composition — ADR-021) |
|
||||
| `INTERNAL` | Registry / Adapter | Handler panic, unhandled error, connection failure |
|
||||
| `TIMEOUT` | Adapter | Request timed out |
|
||||
| `ALREADY_EXISTS` | Registry (`op/register` collision gate) | A peer-announce or import collides with an existing registration (`replace: false`, or a serving-side name — ADR-022). Non-retryable without `replace: true`. |
|
||||
| `CONNECTION_CLOSED` | Client write path | The `call.requested` frame could not be written — the call is provably undelivered (CF-001). `retryable: true` — the only protocol code a caller may auto-retry. Mid-publish and completed-frame write failures stay `INTERNAL` (delivery ambiguous). |
|
||||
|
||||
#### 2a. `CONNECTION_CLOSED` — the retryable undelivered-call code (CF-001)
|
||||
|
||||
The write-failure mapping distinguishes **provably undelivered** from
|
||||
**delivery-ambiguous**: a failed write of the *request* frame means the
|
||||
producer never saw the call — reconnect/retry is safe. A failed write
|
||||
*after* delivery started (mid-publish, completed frame) is ambiguous —
|
||||
retry unsafe — and stays `INTERNAL`. The producer-side `fail_all(...)` on
|
||||
connection close also stays `INTERNAL` (the callee cannot know what the
|
||||
caller received). The code is additive to the wire vocabulary; consumers
|
||||
treat unknown codes per their existing policy, with the `retryable` flag
|
||||
as the machine-readable signal.
|
||||
|
||||
Operation-level domain codes are emitted by **handlers** — the operation's
|
||||
own logic determines what went wrong. They are declared in `error_schemas`
|
||||
@@ -247,9 +268,9 @@ accordingly.
|
||||
```
|
||||
|
||||
**Normative rule (review #002 W20)**: `from_openapi` must not produce error
|
||||
codes that collide with the six protocol-level codes (`NOT_FOUND`,
|
||||
codes that collide with the protocol-level codes (`NOT_FOUND`,
|
||||
`FORBIDDEN`, `INVALID_INPUT`, `INVALID_OPERATION_TYPE`, `INTERNAL`,
|
||||
`TIMEOUT`). The adapter prefixes
|
||||
`TIMEOUT`, `ALREADY_EXISTS`, `CONNECTION_CLOSED`). The adapter prefixes
|
||||
imported error codes with `HTTP_` and the status number (e.g., `HTTP_404`,
|
||||
`HTTP_429`) to avoid collision. This is a requirement for the adapter, not
|
||||
a naming convention — the `from_openapi` example above was previously shown
|
||||
@@ -410,6 +431,10 @@ enum instead of a generic `Result<Output, string>`.
|
||||
- ADR-021: Streaming handler for subscriptions (amends this ADR's
|
||||
protocol-level code list — `INVALID_OPERATION_TYPE` added as the sixth
|
||||
protocol-level code)
|
||||
- ADR-022 (collision sub-amendment 2026-09-04): `ALREADY_EXISTS` — the
|
||||
`op/register` collision rejection code
|
||||
- Consumer findings ledger CF-001 (2026-08-30): `CONNECTION_CLOSED` —
|
||||
the retryable provably-undelivered-call code
|
||||
- docs/sdd_process.md L19, L423 (Safe Exit protocol — the general principle
|
||||
of making failure typed and declared)
|
||||
- TypeScript reference: `/workspace/@alkdev/operations/src/types.ts`
|
||||
|
||||
@@ -332,6 +332,15 @@ implementation detail; the wire surface is unchanged from §1/§3):
|
||||
`Fn(Value, AuthContext) -> BoxFuture<'static,
|
||||
Result<Establishment, EstablishmentError>>`; the `Connection`
|
||||
belongs exclusively to the `OpenHandler` (unchanged).
|
||||
**Identity semantics (CF-005 corollary, 2026-09-07):** the
|
||||
`AuthContext` the establisher and the pump handler receive is the
|
||||
**per-call** context — the opener's dispatch-resolved identity (the
|
||||
same identity the ACL gate and the per-identity cap check saw)
|
||||
overlaid onto the install-time context; transport-truthful fields
|
||||
(`alpn`, `remote_addr`, `tls_client_fingerprint`) stay
|
||||
install-time. On a per-connection registry (ADR-047 §4) this is
|
||||
identical to the install-time context; on hub-forwarded opens the
|
||||
establisher sees the end client, not the hub.
|
||||
2. **The bound is the earlier of the dispatch deadline and the
|
||||
per-registration timeout.** §2 names the dispatch deadline "when
|
||||
the `OperationContext` carries one, else the crate constant";
|
||||
|
||||
@@ -78,11 +78,73 @@ Format: date | found-in (alkhttp context) | severity | status.
|
||||
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.
|
||||
was resolved in the same batch (2026-09-07, CF-006 below).
|
||||
- **Status:** resolved — 2026-09-07 (regression gates: `cf005_*`
|
||||
tests in `src/channels/client.rs`).
|
||||
|
||||
### CF-006 — open-op establisher/pump handler receive the install-time `AuthContext`, never the per-call opener's identity (CF-005 corollary) (2026-09-07) — RESOLVED 2026-09-07
|
||||
|
||||
- **Found in:** the CF-005 verification pass (same alktunnels
|
||||
reverse-flow POC W1 material; the ledger's CF-005 entry named it as
|
||||
a "design follow-up, tracked separately"). `run_open_wrapper`
|
||||
(`src/channels/operations.rs`) closed over the
|
||||
`install_channel_zero`-time `AuthContext` and passed `auth.clone()`
|
||||
verbatim to both the establisher (ADR-049 §1) and the pump handler
|
||||
(`OpenHandler`'s 4th parameter). The per-call opener's identity —
|
||||
the same identity the ACL gate and the per-identity cap check saw —
|
||||
reached neither hook. On a per-connection registry (ADR-047 §4)
|
||||
install-time identity IS the caller's, so the gap was invisible
|
||||
there; it shows on hub-forwarded opens (the establisher saw the
|
||||
hub, not the end client) and on the connect-side serving path
|
||||
(token- or `ServingConfig.identity`-resolved callers). alktty
|
||||
review 002 R3 independently recorded the same seam (closed
|
||||
per-connection by design, with the "registry must stay
|
||||
per-connection" constraint).
|
||||
- **Impact:** an establisher doing per-principal authorization or
|
||||
ownership checks on the backend could not see the actual opener;
|
||||
the pump handler tagging sessions by principal saw the hub.
|
||||
- **Fix (2026-09-07):** `run_open_wrapper` derives a **per-call
|
||||
`AuthContext`** — the opener's dispatch-resolved identity overlaid
|
||||
onto the install-time context — and passes it to both the
|
||||
establisher and the pump handler. When the call has no resolved
|
||||
identity, the install-time identity is kept (anonymous-keep; no
|
||||
synthetic-`anonymous` rewrite of a real install-time identity).
|
||||
Transport-truthful fields (`alpn`, `remote_addr`,
|
||||
`tls_client_fingerprint`) are never rewritten. Signatures are
|
||||
unchanged (`OpenEstablisher`, `OpenHandler` still take
|
||||
`AuthContext`) — behavior-only, additive; downstream code compiles
|
||||
unchanged. Regression gates:
|
||||
`open_wrapper_overlays_per_call_identity_on_install_time_auth` +
|
||||
`open_wrapper_keeps_install_time_identity_when_call_identityless`
|
||||
in `src/channels/operations.rs`.
|
||||
- **Also in this batch:** `ChannelPlan`'s type doc now spells out the
|
||||
`Send + Sync` payload constraint (alktunnels POC F-1 re-derived it
|
||||
by compiler error — a doc line prevents the next consumer repeating
|
||||
that).
|
||||
- **Status:** resolved — 2026-09-07.
|
||||
|
||||
### CF-007 — ADR-016's protocol-code list is stale: omits `ALREADY_EXISTS` and `CONNECTION_CLOSED` (2026-09-07) — RESOLVED 2026-09-07
|
||||
|
||||
- **Found in:** alkhttp review 006, Part C
|
||||
(`alkhttp/docs/reviews/006-alkcall-0.3.0-consequence-review.md`
|
||||
L207–233, L319–324 — "upstream doc drift … flagged for the next
|
||||
alkcall doc pass"). ADR-016 said "six codes" and its table listed
|
||||
only the original six, while the wire vocabulary had grown:
|
||||
`ALREADY_EXISTS` (ADR-022 collision sub-amendment, 0.3.0) and
|
||||
`CONNECTION_CLOSED` (CF-001, 0.4.x, the retryable
|
||||
provably-undelivered-call code). Downstream consumers already
|
||||
handle both; the authoritative ADR was the thing that was wrong.
|
||||
- **Fix (2026-09-07):** ADR-016 amended — Status notes the 2026-09-07
|
||||
extension to eight; the Context paragraph and the §3 table carry
|
||||
both codes with their semantics (`ALREADY_EXISTS` non-retryable,
|
||||
registration state; `CONNECTION_CLOSED` retryable — the only
|
||||
protocol code a caller may auto-retry), a §2a subsection documents
|
||||
the undelivered-vs-ambiguous write-failure distinction, the
|
||||
`from_openapi` collision rule lists all eight, and the
|
||||
cross-references cite the two amendments. Doc-only; the wire
|
||||
surface is unchanged.
|
||||
- **Status:** resolved — 2026-09-07.
|
||||
|
||||
---
|
||||
### CF-004 — `services_schema_handler` discloses Internal/ACL-restricted op specs — no visibility or AccessControl check (2026-08-30) — RESOLVED 2026-08-31
|
||||
|
||||
|
||||
+182
-60
@@ -362,6 +362,15 @@ pub type OpenHandler = Arc<
|
||||
/// The plan is **process-local**: establisher → wrapper → handler on
|
||||
/// the producing side. Nothing crosses the transport that isn't
|
||||
/// already the open op's input — the wire surface is unchanged.
|
||||
///
|
||||
/// **Payload constraint (alktunnels POC F-1):** `Arc<dyn Any + Send +
|
||||
/// Sync>` requires the concrete plan payload to be `Send + Sync`. A
|
||||
/// dialed-socket handle with boxed non-`Sync` halves (e.g.
|
||||
/// `Box<dyn AsyncRead>` without a `Sync` bound) does not satisfy it —
|
||||
/// wrap such handles in a `Sync` adapter or hold them unboxed. The
|
||||
/// bound is correct (the plan crosses the wrapper task boundary);
|
||||
/// this note exists so the next consumer doesn't re-derive it by
|
||||
/// compiler error.
|
||||
pub type ChannelPlan = Arc<dyn std::any::Any + Send + Sync>;
|
||||
|
||||
/// The establishment-phase result (ADR-049 §1, as filled by ADR-049
|
||||
@@ -441,8 +450,13 @@ impl EstablishmentError {
|
||||
/// is spawned. On failure the wrapper tears down the just-allocated
|
||||
/// channel and replies `channel:open_failed` with `details.reason`.
|
||||
///
|
||||
/// The establisher takes the open op's `input` (params) and the peer's
|
||||
/// `AuthContext`. It deliberately does **not** receive the channel's
|
||||
/// The establisher takes the open op's `input` (params) and the
|
||||
/// **per-call** `AuthContext`: the opener's resolved identity (the
|
||||
/// same identity the ACL gate and the per-identity cap check saw —
|
||||
/// CF-005 corollary, 2026-09-07) overlaid onto the install-time
|
||||
/// context; the transport-truthful fields (`alpn`, `remote_addr`,
|
||||
/// `tls_client_fingerprint`) are install-time. It deliberately does
|
||||
/// **not** receive the channel's
|
||||
/// `Connection`: the channel `BiStream` is yield-once
|
||||
/// (`ChannelBidiStreamSource`) and belongs exclusively to the pump
|
||||
/// handler (the `OpenHandler`); the establisher is pre-data-plane (its
|
||||
@@ -524,9 +538,13 @@ impl ChannelCore {
|
||||
/// overlay registry, Layer 2 per ADR-019 — this is the
|
||||
/// per-connection registration the §4 amendment blesses). The
|
||||
/// `auth` is the peer's `AuthContext`, captured at
|
||||
/// `install_channel_zero` time and closed over by the wrapper so
|
||||
/// the ALPN handler receives it without the wrapper having to
|
||||
/// reach into `OperationContext` for it.
|
||||
/// `install_channel_zero` time and closed over by the wrapper: it
|
||||
/// provides the transport-truthful fields (`alpn`,
|
||||
/// `remote_addr`, `tls_client_fingerprint`), while the identity
|
||||
/// field is overridden **per call** — the opener's resolved
|
||||
/// identity (the same identity the ACL gate and the cap check
|
||||
/// saw; CF-005 corollary, 2026-09-07), falling back to the
|
||||
/// install-time identity when the call has none.
|
||||
///
|
||||
/// **Op type:** the spec's `op_type` determines the
|
||||
/// `HandlerKind`. For `Query`/`Mutation` the wrapper is a `Once`
|
||||
@@ -639,15 +657,15 @@ impl ChannelCore {
|
||||
}
|
||||
}
|
||||
|
||||
/// The opener identity for the per-identity cap check. The direct
|
||||
/// caller (the peer that opened this channels connection) is the
|
||||
/// identity whose cap is consulted; `forwarded_for` is metadata and
|
||||
/// is NOT consulted (ADR-026, ADR-047 §7). Falls back to a synthetic
|
||||
/// "anonymous" identity when the call has no resolved identity (the
|
||||
/// cap still applies — anonymous opens count against the anonymous
|
||||
/// identity's cap, which is 256 by default).
|
||||
fn opener_identity_from_context(ctx: &OperationContext) -> Identity {
|
||||
ctx.identity.clone().unwrap_or_else(|| Identity {
|
||||
/// The opener identity for the per-identity cap check (CF-005
|
||||
/// corollary): `Some` is the dispatch-resolved caller identity; `None`
|
||||
/// falls back to a synthetic "anonymous" identity. The direct caller
|
||||
/// (the peer that opened this channels connection) is the identity
|
||||
/// whose cap is consulted; `forwarded_for` is metadata and is NOT
|
||||
/// consulted (ADR-026, ADR-047 §7). Anonymous opens count against the
|
||||
/// anonymous identity's cap, which is 256 by default.
|
||||
fn opener_identity_or_anonymous(call_identity: Option<Identity>) -> Identity {
|
||||
call_identity.unwrap_or_else(|| Identity {
|
||||
id: "anonymous".to_string(),
|
||||
scopes: vec![],
|
||||
resources: Default::default(),
|
||||
@@ -743,9 +761,24 @@ fn establishment_timeout_call_error() -> CallError {
|
||||
///
|
||||
/// `input` is the open op's input (params), passed through to the
|
||||
/// establisher (when registered) and the ALPN's `OpenHandler`.
|
||||
/// `opener_id` is the `PeerId` recorded in the opener ledger (the
|
||||
/// direct caller's identity id). `opener_identity` is the identity the
|
||||
/// cap was reserved against (un-reserved on failure paths).
|
||||
/// `call_identity` is the dispatch-resolved caller identity — `Some`
|
||||
/// when the call carried one (the same identity the ACL gate saw),
|
||||
/// `None` when identity-less. The cap is checked against it (or the
|
||||
/// synthetic `anonymous` when `None`) and it is overlaid onto the
|
||||
/// per-call auth when present.
|
||||
///
|
||||
/// The `auth` handed to the establisher and the pump handler is the
|
||||
/// **per-call** context (CF-005 corollary): the opener's resolved
|
||||
/// identity overlaid onto the install-time context. When the call has
|
||||
/// no resolved identity, the install-time identity is kept (the
|
||||
/// anonymous-keep case). Transport-truthful fields (`alpn`,
|
||||
/// `remote_addr`, `tls_client_fingerprint`) are never rewritten. In
|
||||
/// the per-connection-registry deployment (ADR-047 §4) the
|
||||
/// install-time identity IS the caller's, so this is
|
||||
/// behavior-identical there; the difference shows on hub-forwarded
|
||||
/// opens (the establisher sees the end client, not the hub) and on
|
||||
/// the connect-side serving path (token- or
|
||||
/// `ServingConfig.identity`-resolved callers).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn run_open_wrapper(
|
||||
manager: &ChannelManager,
|
||||
@@ -755,16 +788,24 @@ async fn run_open_wrapper(
|
||||
auth: &AuthContext,
|
||||
alpn: &str,
|
||||
input: Value,
|
||||
opener_id: String,
|
||||
opener_identity: Identity,
|
||||
call_identity: Option<Identity>,
|
||||
request_id: String,
|
||||
deadline: Option<Instant>,
|
||||
) -> ResponseEnvelope {
|
||||
let mut per_call_auth = auth.clone();
|
||||
if let Some(identity) = &call_identity {
|
||||
per_call_auth.identity = Some(identity.clone());
|
||||
}
|
||||
let opener_identity = opener_identity_or_anonymous(call_identity);
|
||||
|
||||
if let Err(channel_err) = policy.check_open(&opener_identity) {
|
||||
return ResponseEnvelope::error(request_id, map_channel_error_to_call_error(&channel_err));
|
||||
}
|
||||
|
||||
let channel_id = match manager.open_channel(alpn, opener_id, None).await {
|
||||
let channel_id = match manager
|
||||
.open_channel(alpn, opener_identity.id.clone(), None)
|
||||
.await
|
||||
{
|
||||
Ok((id, send, recv)) => {
|
||||
// The establishment phase (ADR-049 §1): awaited bounded,
|
||||
// before the reply and before the pump handler is spawned.
|
||||
@@ -775,7 +816,7 @@ async fn run_open_wrapper(
|
||||
let bound = establishment_bound(hook.timeout, deadline);
|
||||
let establishment = tokio::time::timeout(
|
||||
bound,
|
||||
(hook.establisher)(input.clone(), auth.clone()),
|
||||
(hook.establisher)(input.clone(), per_call_auth.clone()),
|
||||
)
|
||||
.await;
|
||||
match establishment {
|
||||
@@ -817,7 +858,7 @@ async fn run_open_wrapper(
|
||||
Arc::clone(&accepted),
|
||||
);
|
||||
let channel_conn = Connection::from_source(source, alpn.as_bytes().to_vec());
|
||||
let raw_task = open_handler(input, plan, channel_conn, auth.clone());
|
||||
let raw_task = open_handler(input, plan, channel_conn, per_call_auth);
|
||||
|
||||
let teardown_manager = manager.clone();
|
||||
let teardown_policy = Arc::clone(policy);
|
||||
@@ -948,8 +989,6 @@ fn make_open_handler_once(
|
||||
let alpn = alpn.clone();
|
||||
Box::pin(async move {
|
||||
let request_id = ctx.request_id.clone();
|
||||
let opener_identity = opener_identity_from_context(&ctx);
|
||||
let opener_id = opener_identity.id.clone();
|
||||
run_open_wrapper(
|
||||
&manager,
|
||||
&policy,
|
||||
@@ -958,8 +997,7 @@ fn make_open_handler_once(
|
||||
&auth,
|
||||
&alpn,
|
||||
input,
|
||||
opener_id,
|
||||
opener_identity,
|
||||
ctx.identity.clone(),
|
||||
request_id,
|
||||
ctx.deadline,
|
||||
)
|
||||
@@ -993,8 +1031,6 @@ fn make_open_handler_stream(
|
||||
let alpn = alpn.clone();
|
||||
Box::pin(futures::stream::once(async move {
|
||||
let request_id = ctx.request_id.clone();
|
||||
let opener_identity = opener_identity_from_context(&ctx);
|
||||
let opener_id = opener_identity.id.clone();
|
||||
run_open_wrapper(
|
||||
&manager,
|
||||
&policy,
|
||||
@@ -1003,8 +1039,7 @@ fn make_open_handler_stream(
|
||||
&auth,
|
||||
&alpn,
|
||||
input,
|
||||
opener_id,
|
||||
opener_identity,
|
||||
ctx.identity.clone(),
|
||||
request_id,
|
||||
ctx.deadline,
|
||||
)
|
||||
@@ -1357,17 +1392,15 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opener_identity_from_context_returns_identity_when_present() {
|
||||
fn opener_identity_or_anonymous_returns_identity_when_present() {
|
||||
let ctx = test_context("opener-1");
|
||||
let id = opener_identity_from_context(&ctx);
|
||||
let id = opener_identity_or_anonymous(ctx.identity.clone());
|
||||
assert_eq!(id.id, "alice");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opener_identity_from_context_falls_back_to_anonymous() {
|
||||
let mut ctx = test_context("opener-2");
|
||||
ctx.identity = None;
|
||||
let id = opener_identity_from_context(&ctx);
|
||||
fn opener_identity_or_anonymous_falls_back_to_anonymous() {
|
||||
let id = opener_identity_or_anonymous(None);
|
||||
assert_eq!(id.id, "anonymous");
|
||||
}
|
||||
|
||||
@@ -1415,6 +1448,106 @@ mod tests {
|
||||
assert!(spawned.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
/// CF-005 corollary — the establisher and the pump handler see the
|
||||
/// **per-call** opener identity (the identity the ACL gate and cap
|
||||
/// check saw), not the install-time snapshot. The install-time
|
||||
/// context here carries a stale identity (`stale-install`); the
|
||||
/// dispatch context resolves `alice`. Both hooks must report
|
||||
/// `alice` while `alpn` stays the install-time transport value.
|
||||
#[tokio::test]
|
||||
async fn open_wrapper_overlays_per_call_identity_on_install_time_auth() {
|
||||
let manager = make_manager().await;
|
||||
let policy = super::super::policy::default_policy();
|
||||
let seen_establisher = Arc::new(parking_lot::Mutex::new(None::<String>));
|
||||
let seen_handler = Arc::new(parking_lot::Mutex::new(None::<String>));
|
||||
let seen_establisher_cl = Arc::clone(&seen_establisher);
|
||||
let seen_handler_cl = Arc::clone(&seen_handler);
|
||||
|
||||
let establisher: OpenEstablisher = Arc::new(move |_input, auth| {
|
||||
let seen = Arc::clone(&seen_establisher_cl);
|
||||
Box::pin(async move {
|
||||
*seen.lock() = auth.identity.as_ref().map(|i| i.id.clone());
|
||||
Ok::<_, EstablishmentError>(Establishment::default())
|
||||
})
|
||||
});
|
||||
let open_handler: OpenHandler = Arc::new(move |_input, _plan, _conn, auth| {
|
||||
*seen_handler_cl.lock() = auth.identity.as_ref().map(|i| i.id.clone());
|
||||
tokio::spawn(async {})
|
||||
});
|
||||
|
||||
let mut install_auth = AuthContext::anonymous(b"alk/tty");
|
||||
install_auth.identity = Some(Identity {
|
||||
id: "stale-install".to_string(),
|
||||
scopes: vec![],
|
||||
resources: HashMap::new(),
|
||||
});
|
||||
let handler = make_open_handler_once(
|
||||
manager.clone(),
|
||||
policy,
|
||||
Some(EstablisherHook {
|
||||
establisher,
|
||||
timeout: None,
|
||||
}),
|
||||
open_handler,
|
||||
install_auth,
|
||||
"alk/tty".to_string(),
|
||||
);
|
||||
let env = handler(json!({}), test_context("open-per-call-1")).await;
|
||||
env.result.expect("open should succeed");
|
||||
|
||||
assert_eq!(
|
||||
seen_establisher.lock().as_deref(),
|
||||
Some("alice"),
|
||||
"establisher saw the per-call opener identity"
|
||||
);
|
||||
assert_eq!(
|
||||
seen_handler.lock().as_deref(),
|
||||
Some("alice"),
|
||||
"pump handler saw the per-call opener identity"
|
||||
);
|
||||
}
|
||||
|
||||
/// CF-005 corollary (fallback leg) — when the call has no resolved
|
||||
/// identity, the anonymous opener keeps the install-time identity
|
||||
/// (no overlay). The install-time identity here is `bob`; the
|
||||
/// handler must see `bob`, not the synthetic `anonymous`.
|
||||
#[tokio::test]
|
||||
async fn open_wrapper_keeps_install_time_identity_when_call_identityless() {
|
||||
let manager = make_manager().await;
|
||||
let policy = super::super::policy::default_policy();
|
||||
let seen_handler = Arc::new(parking_lot::Mutex::new(None::<String>));
|
||||
let seen_handler_cl = Arc::clone(&seen_handler);
|
||||
let open_handler: OpenHandler = Arc::new(move |_input, _plan, _conn, auth| {
|
||||
*seen_handler_cl.lock() = auth.identity.as_ref().map(|i| i.id.clone());
|
||||
tokio::spawn(async {})
|
||||
});
|
||||
|
||||
let mut install_auth = AuthContext::anonymous(b"alk/tty");
|
||||
install_auth.identity = Some(Identity {
|
||||
id: "bob".to_string(),
|
||||
scopes: vec![],
|
||||
resources: HashMap::new(),
|
||||
});
|
||||
let handler = make_open_handler_once(
|
||||
manager.clone(),
|
||||
policy,
|
||||
None,
|
||||
open_handler,
|
||||
install_auth,
|
||||
"alk/tty".to_string(),
|
||||
);
|
||||
let mut ctx = test_context("open-per-call-2");
|
||||
ctx.identity = None;
|
||||
let env = handler(json!({}), ctx).await;
|
||||
env.result.expect("open should succeed");
|
||||
|
||||
assert_eq!(
|
||||
seen_handler.lock().as_deref(),
|
||||
Some("bob"),
|
||||
"identity-less call keeps the install-time identity"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn make_open_handler_stream_success_path() {
|
||||
let manager = make_manager().await;
|
||||
@@ -1628,11 +1761,11 @@ mod tests {
|
||||
let open_handler: OpenHandler =
|
||||
Arc::new(|_input, _plan, _conn, _auth| tokio::spawn(async {}));
|
||||
let auth = AuthContext::anonymous(b"alk/call");
|
||||
let opener_id = Identity {
|
||||
let opener = Some(Identity {
|
||||
id: "alice".to_string(),
|
||||
scopes: vec![],
|
||||
resources: HashMap::new(),
|
||||
};
|
||||
});
|
||||
let env = run_open_wrapper(
|
||||
&manager,
|
||||
&policy,
|
||||
@@ -1641,8 +1774,7 @@ mod tests {
|
||||
&auth,
|
||||
"alk/tty",
|
||||
json!({}),
|
||||
opener_id.id.clone(),
|
||||
opener_id,
|
||||
opener,
|
||||
"req-cap-deny".to_string(),
|
||||
None,
|
||||
)
|
||||
@@ -1705,8 +1837,7 @@ mod tests {
|
||||
&auth,
|
||||
"alk/tty",
|
||||
json!({}),
|
||||
"alice".to_string(),
|
||||
identity("alice"),
|
||||
Some(identity("alice")),
|
||||
"req-est-fail".to_string(),
|
||||
None,
|
||||
)
|
||||
@@ -1764,8 +1895,7 @@ mod tests {
|
||||
&auth,
|
||||
"alk/tty",
|
||||
json!({}),
|
||||
"alice".to_string(),
|
||||
identity("alice"),
|
||||
Some(identity("alice")),
|
||||
"req-est-timeout".to_string(),
|
||||
None,
|
||||
)
|
||||
@@ -1809,8 +1939,7 @@ mod tests {
|
||||
&auth,
|
||||
"alk/tty",
|
||||
json!({}),
|
||||
"alice".to_string(),
|
||||
identity("alice"),
|
||||
Some(identity("alice")),
|
||||
"req-est-deadline".to_string(),
|
||||
Some(Instant::now() + Duration::from_millis(50)),
|
||||
)
|
||||
@@ -1844,8 +1973,7 @@ mod tests {
|
||||
&auth,
|
||||
"alk/tty",
|
||||
json!({}),
|
||||
"alice".to_string(),
|
||||
identity("alice"),
|
||||
Some(identity("alice")),
|
||||
"req-est-ok".to_string(),
|
||||
None,
|
||||
)
|
||||
@@ -1884,8 +2012,7 @@ mod tests {
|
||||
&auth,
|
||||
"alk/tty",
|
||||
json!({}),
|
||||
"alice".to_string(),
|
||||
identity("alice"),
|
||||
Some(identity("alice")),
|
||||
"req-accepted".to_string(),
|
||||
None,
|
||||
)
|
||||
@@ -1916,8 +2043,7 @@ mod tests {
|
||||
&auth,
|
||||
"alk/tty",
|
||||
json!({}),
|
||||
"alice".to_string(),
|
||||
identity("alice"),
|
||||
Some(identity("alice")),
|
||||
"req-not-accepted".to_string(),
|
||||
None,
|
||||
)
|
||||
@@ -2129,8 +2255,7 @@ mod tests {
|
||||
&auth,
|
||||
"alk/tty",
|
||||
json!({}),
|
||||
"alice".to_string(),
|
||||
identity("alice"),
|
||||
Some(identity("alice")),
|
||||
"req-plan-flow".to_string(),
|
||||
None,
|
||||
)
|
||||
@@ -2183,8 +2308,7 @@ mod tests {
|
||||
&auth,
|
||||
"alk/tty",
|
||||
json!({ "resource": "same" }),
|
||||
"alice".to_string(),
|
||||
identity("alice"),
|
||||
Some(identity("alice")),
|
||||
"req-race-a".to_string(),
|
||||
None,
|
||||
),
|
||||
@@ -2196,8 +2320,7 @@ mod tests {
|
||||
&auth,
|
||||
"alk/tty",
|
||||
json!({ "resource": "same" }),
|
||||
"alice".to_string(),
|
||||
identity("alice"),
|
||||
Some(identity("alice")),
|
||||
"req-race-b".to_string(),
|
||||
None,
|
||||
)
|
||||
@@ -2233,8 +2356,7 @@ mod tests {
|
||||
&auth,
|
||||
"alk/tty",
|
||||
json!({}),
|
||||
"alice".to_string(),
|
||||
identity("alice"),
|
||||
Some(identity("alice")),
|
||||
"req-no-plan".to_string(),
|
||||
None,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user