Files
alknet/tasks/call/registry/access-control-ownership-check.md
glm-5.2 de536b82e1 tasks: add ADR-050 implementation tasks — ownership store, resource_id_path, check signature, dispatch wiring
Four tasks forming a DAG for the dynamic resource ownership model (ADR-050):

1. core/ownership-store-trait (no deps) — OwnershipProvider (sync read) +
   OwnershipStore (async write) traits + InMemoryOwnershipStore + OwnershipError
   in alknet-core; fourth instance of the repo/adapter pattern (ADR-033)
2. call/registry/operation-spec-resource-id-path (no deps) — add
   resource_id_path: Option<String> to OperationSpec (JSON pointer into
   input for resource ID extraction)
3. call/registry/access-control-ownership-check (depends on 1) — update
   AccessControl::check signature to accept resource_id + OwnershipProvider;
   backward compatible (ownership=None falls back to static Identity.resources)
4. call/registry/dispatch-resource-id-extraction (depends on 2, 3) — wire
   dispatch path to extract resource_id from input via spec.resource_id_path
   and thread OwnershipProvider to check(); OperationContext gains ownership
   field

Tasks 1 and 2 can run in parallel (different crates, no deps). Task 3
depends on 1. Task 4 depends on 2 and 3. Validated: no cycles, 90 tasks total.
2026-07-05 11:12:44 +00:00

153 lines
6.9 KiB
Markdown

---
id: call/registry/access-control-ownership-check
name: Update AccessControl::check to consult OwnershipProvider for dynamic resource ownership (ADR-050 §2)
status: pending
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.