--- id: call/registry/access-control-ownership-check name: Update AccessControl::check to consult OwnershipProvider for dynamic resource ownership (ADR-050 §2) status: done depends_on: [core/ownership-store-trait] scope: moderate risk: medium impact: component level: implementation --- ## Description Update `AccessControl::check` to accept `resource_id` and an optional `OwnershipProvider` reference, consulting the provider for runtime-spawned resource ownership checks. Per ADR-050 §2. ### The signature change Current (in `crates/alknet-call/src/registry/spec.rs`): ```rust pub fn check(&self, identity: Option<&Identity>) -> AccessResult ``` Updated: ```rust pub fn check( &self, identity: Option<&Identity>, resource_id: Option<&str>, ownership: Option<&dyn OwnershipProvider>, ) -> AccessResult ``` ### Check logic ``` 1. Scope check (unchanged): identity.scopes ⊇ required_scopes. If identity is None and scopes are required, deny here. (Same as current — no change to the scope-check path.) 2. Resource check (only if self.resource_type is Some): a. resource_id Some + ownership Some: → identity must be Some (owns takes &Identity, not Option); if identity is None, deny. → p.owns(identity, resource_type, resource_id, resource_action) → if false, Forbidden("not owner of resource") b. resource_id None + ownership Some (the `list` case, ADR-050 §4a): → if identity is None, deny. → p.owns_any(identity, resource_type) [scope-gate] → if false, Forbidden("no owned resources of type") c. ownership None → fall back to static → identity.resources[resource_type] ∋ resource_action (backward compat for non-runtime resources — the current logic) ``` ### Backward compatibility When `ownership` is `None`, `check` falls back to the static `Identity.resources` path — the existing resource-check logic (lines 83-105 of `spec.rs`) runs unchanged. This means operations with static resource sets work without wiring an ownership provider. The ownership provider is an additional check, not a replacement. When `resource_id` is `None` and `ownership` is `None`, the existing logic also runs unchanged (the `resource_action` without a specific resource ID path, lines 97-105). This covers operations that have `resource_action` but no `resource_type` — those stay on the static path. ### Call sites to update There are **6 call sites** for `check()` that need the signature update: 1. `registry/registration.rs:142` — `acl.check(identity.as_ref())` in `invoke()` 2. `registry/registration.rs:194` — `acl.check(identity.as_ref())` in `invoke_streaming()` 3. `protocol/connection.rs:351` — `access_control.check(caller_identity.as_ref())` in `invoke_with_policy()` 4. `registry/discovery.rs:224` — `spec.access_control.check(calling_identity)` in `services/list` 5. `registry/discovery.rs:247` — `spec.access_control.check(calling_identity)` in `services/schema` 6. `registry/discovery.rs:269` — `reg.spec.access_control.check(calling_identity)` in `services/list-peers` For sites 1-3 (dispatch path): pass `resource_id: None` and `ownership: None` for now — the dispatch-path wiring (extracting `resource_id` from input via `spec.resource_id_path` and threading the ownership provider) is `call/registry/dispatch-resource-id-extraction`, which depends on this task. This task changes the signature and the check logic; the dispatch task wires the new parameters. For sites 4-6 (service discovery): these are `services/list` / `services/schema` / `services/list-peers` filtering — they check whether a calling peer can see an operation at all. Pass `resource_id: None` and `ownership: None` — service discovery doesn't do per-resource ownership checks (it's scope-gating only). The `list` result-filter (ADR-050 §4a) is a handler-level concern, not a service-discovery concern. ### What this task does NOT do - Does NOT extract `resource_id` from the input or thread the ownership provider into the dispatch path — that's `call/registry/dispatch-resource-id-extraction`, which depends on this task. This task changes the signature and the check logic; all call sites pass `None, None` and behave exactly as before. - Does NOT add `resource_id_path` to `OperationSpec` — that's `call/registry/operation-spec-resource-id-path`, which is independent (no dependency). ### Tests The existing tests in `spec.rs` (lines 157-327) all call `check(identity)` with one argument. They need to be updated to `check(identity, None, None)` — the behavior should be identical (backward compat). Add new tests for the ownership-provider path: - `resource_id Some + ownership Some + provider says owns → Allowed` - `resource_id Some + ownership Some + provider says not owns → Forbidden` - `resource_id Some + ownership Some + identity None → Forbidden` - `resource_id None + ownership Some + provider says owns_any → Allowed` (the `list` scope-gate) - `resource_id None + ownership Some + provider says not owns_any → Forbidden` - `ownership None + resource_type Some → static fallback (existing behavior)` - A mock `OwnershipProvider` impl for testing ## Acceptance Criteria - [ ] `AccessControl::check` signature updated: `(identity, resource_id, ownership) -> AccessResult` - [ ] Scope check path unchanged (required_scopes, required_scopes_any) - [ ] Resource check path: when `ownership` is Some, consults the provider - [ ] Resource check path: when `ownership` is None, falls back to static `Identity.resources` (backward compat) - [ ] `resource_id None + ownership Some` → calls `owns_any` (the `list` scope-gate) - [ ] `resource_id Some + ownership Some` → calls `owns` - [ ] `identity None + ownership Some + resource_type Some` → Forbidden (owns needs &Identity) - [ ] All 6 existing call sites updated to pass `(None, None)` — behavior unchanged - [ ] All existing tests updated to `check(identity, None, None)` — behavior unchanged - [ ] New tests with a mock `OwnershipProvider` for the dynamic path - [ ] `cargo test -p alknet-call` succeeds - [ ] `cargo clippy -p alknet-call` succeeds with no warnings ## References - docs/architecture/crates/call/operation-registry.md — AccessControl (updated with the new check signature) - docs/architecture/decisions/050-dynamic-resource-ownership-for-runtime-spawned-resources.md — ADR-050 §2, §4a - crates/alknet-call/src/registry/spec.rs — current AccessControl::check implementation - tasks/core/ownership-store-trait.md — the OwnershipProvider trait (dependency) ## Notes > This is the one-way door — the `check` signature change touches every > call site and test. The key design property: backward compatibility when > `ownership` is `None`. All existing call sites pass `(None, None)` in > this task and behave exactly as before. The dispatch-path wiring > (extracting `resource_id` from input and threading a real ownership > provider) is the next task. This task is the signature + logic change; > the next task is the wiring.