feat(core,call): implement ADR-050 dynamic resource ownership — ownership store, resource_id_path, check signature, dispatch wiring
Implements the 4-task DAG for runtime-spawned resource ownership so AccessControl::check can answer "does this identity own this specific container/TTY/process" instead of relying only on static Identity.resources grants. 1. core/ownership-store-trait: OwnershipProvider (sync read) + OwnershipStore (async write) traits + InMemoryOwnershipStore + OwnershipError in alknet-core. Fourth instance of the repo/adapter pattern (ADR-033), mirroring CredentialStore/store.rs. 2. call/registry/operation-spec-resource-id-path: resource_id_path field on OperationSpec — JSON pointer into input for resource ID extraction. Single field addition, all ~40 construction sites across alknet-call + alknet-http updated to pass None (no semantic change). 3. call/registry/access-control-ownership-check: check() signature gains resource_id + Option<&dyn OwnershipProvider>. 3-case decision tree: ownership Some + resource_id Some -> owns(); ownership Some + resource_id None -> owns_any() (list scope-gate); ownership None -> static Identity.resources fallback (backward compat). 7 call sites updated to (None, None) — including a 7th in alknet-http/gateway_routes not listed in the original spec. 4. call/registry/dispatch-resource-id-extraction: wire the dispatch path. OperationContext gains ownership field; Dispatcher/CallAdapter gain with_ownership_provider builders; invoke/invoke_streaming/ invoke_with_policy extract resource_id via spec.resource_id_path and thread context.ownership to check(). extract_json_pointer helper handles $.field syntax (graceful None on missing/non-string). 20 OperationContext literals updated across both crates. Backward compatibility is load-bearing throughout: ownership=None falls back to the existing static resource-check path. Deployments without runtime-spawned resources wire nothing and behave identically. 821 tests pass workspace-wide (was ~770); clippy clean; fmt clean. Task specs marked done.
This commit is contained in:
@@ -180,6 +180,7 @@ pub(crate) fn build_spec(tool: &Tool, namespace: &str) -> OperationSpec {
|
||||
output_schema,
|
||||
error_schemas,
|
||||
AccessControl::default(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -427,6 +427,7 @@ impl FromOpenAPI {
|
||||
output_schema,
|
||||
error_schemas,
|
||||
AccessControl::default(),
|
||||
None,
|
||||
);
|
||||
|
||||
let path_template = path.to_string();
|
||||
@@ -967,6 +968,7 @@ mod tests {
|
||||
abort_policy: alknet_call::registry::context::AbortPolicy::default(),
|
||||
deadline: Some(std::time::Instant::now() + Duration::from_secs(30)),
|
||||
internal: true,
|
||||
ownership: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -488,6 +488,7 @@ mod tests {
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
acl,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -560,6 +560,7 @@ mod tests {
|
||||
json!({}),
|
||||
errors,
|
||||
AccessControl::default(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1015,6 +1016,7 @@ mod tests {
|
||||
json!({}),
|
||||
vec![error("INTERNAL_ERROR", Some(418))],
|
||||
AccessControl::default(),
|
||||
None,
|
||||
),
|
||||
HandlerKind::Once(noop_handler()),
|
||||
OperationProvenance::Local,
|
||||
|
||||
@@ -135,6 +135,7 @@ impl GatewayDispatch {
|
||||
env,
|
||||
abort_policy: AbortPolicy::default(),
|
||||
internal: false,
|
||||
ownership: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +205,7 @@ mod tests {
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
acl,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -216,6 +218,7 @@ mod tests {
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
acl,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -231,6 +234,7 @@ mod tests {
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
acl,
|
||||
None,
|
||||
),
|
||||
HandlerKind::Once(make_handler(|input, context| async move {
|
||||
ResponseEnvelope::ok(context.request_id, input)
|
||||
@@ -278,6 +282,7 @@ mod tests {
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
acl,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -261,7 +261,7 @@ fn access_check_for_op(
|
||||
) -> Option<String> {
|
||||
let name = operation.strip_prefix('/').unwrap_or(operation);
|
||||
let reg = registry.registration(name)?;
|
||||
if let AccessResult::Forbidden(message) = reg.spec.access_control.check(identity) {
|
||||
if let AccessResult::Forbidden(message) = reg.spec.access_control.check(identity, None, None) {
|
||||
return Some(message);
|
||||
}
|
||||
None
|
||||
@@ -349,6 +349,7 @@ mod tests {
|
||||
json!({}),
|
||||
vec![],
|
||||
acl,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -361,6 +362,7 @@ mod tests {
|
||||
json!({}),
|
||||
vec![],
|
||||
AccessControl::default(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -428,6 +430,7 @@ mod tests {
|
||||
json!({}),
|
||||
vec![],
|
||||
acl,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ mod tests {
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
AccessControl::default(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ mod tests {
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
acl,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -103,6 +104,7 @@ mod tests {
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
AccessControl::default(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -170,6 +172,7 @@ mod tests {
|
||||
abort_policy: AbortPolicy::default(),
|
||||
deadline: Some(Instant::now() + Duration::from_secs(30)),
|
||||
internal: true,
|
||||
ownership: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -313,6 +313,7 @@ mod tests {
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
acl,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -325,6 +326,7 @@ mod tests {
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
AccessControl::default(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -568,6 +570,7 @@ mod tests {
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
AccessControl::default(),
|
||||
None,
|
||||
),
|
||||
HandlerKind::Once(make_handler(|input, ctx| async move {
|
||||
ResponseEnvelope::ok(ctx.request_id, input)
|
||||
@@ -928,6 +931,7 @@ mod tests {
|
||||
abort_policy: AbortPolicy::default(),
|
||||
deadline: Some(Instant::now() + Duration::from_secs(30)),
|
||||
internal: false,
|
||||
ownership: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -948,6 +952,7 @@ mod tests {
|
||||
abort_policy: AbortPolicy::default(),
|
||||
deadline: Some(Instant::now() + Duration::from_secs(30)),
|
||||
internal: true,
|
||||
ownership: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ fn test_context(request_id: &str, caps: Capabilities) -> OperationContext {
|
||||
abort_policy: AbortPolicy::default(),
|
||||
deadline: Some(Instant::now() + Duration::from_secs(30)),
|
||||
internal: true,
|
||||
ownership: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user