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:
1 parent
de536b82e1
commit
73724b8a1d
28 files changed
+1000
-29
No files matched your search
@@ -14,6 +14,7 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use alknet_core::auth::{AuthContext, IdentityProvider};
|
||||
use alknet_core::ownership::OwnershipProvider;
|
||||
use alknet_core::types::{Connection, HandlerError, ProtocolHandler};
|
||||
use async_trait::async_trait;
|
||||
|
||||
@@ -63,6 +64,11 @@ impl CallAdapter {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_ownership_provider(mut self, provider: Arc<dyn OwnershipProvider>) -> Self {
|
||||
self.dispatcher = self.dispatcher.with_ownership_provider(provider);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn registry(&self) -> &Arc<OperationRegistry> {
|
||||
&self.dispatcher.registry
|
||||
}
|
||||
@@ -224,6 +230,7 @@ mod tests {
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
acl,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -237,6 +244,7 @@ mod tests {
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
AccessControl::default(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -257,6 +265,7 @@ mod tests {
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
acl,
|
||||
None,
|
||||
),
|
||||
HandlerKind::Once(handler),
|
||||
OperationProvenance::Local,
|
||||
@@ -548,6 +557,7 @@ mod tests {
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
AccessControl::default(),
|
||||
None,
|
||||
),
|
||||
HandlerKind::Once(echo_handler()),
|
||||
OperationProvenance::FromCall,
|
||||
@@ -583,6 +593,7 @@ mod tests {
|
||||
abort_policy: AbortPolicy::default(),
|
||||
deadline: context.deadline,
|
||||
internal: false,
|
||||
ownership: None,
|
||||
};
|
||||
let response = context
|
||||
.env
|
||||
@@ -615,6 +626,7 @@ mod tests {
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
AccessControl::default(),
|
||||
None,
|
||||
),
|
||||
HandlerKind::Once(echo_handler()),
|
||||
OperationProvenance::FromCall,
|
||||
@@ -641,6 +653,7 @@ mod tests {
|
||||
abort_policy: AbortPolicy::default(),
|
||||
deadline: context.deadline,
|
||||
internal: false,
|
||||
ownership: None,
|
||||
};
|
||||
let response = context
|
||||
.env
|
||||
|
||||
@@ -326,6 +326,7 @@ impl OperationEnv for OverlayOperationEnv {
|
||||
let composition_authority;
|
||||
let scoped_env;
|
||||
let access_control;
|
||||
let resource_id_path;
|
||||
{
|
||||
let overlay = self.overlay.read();
|
||||
let Some(registration) = overlay.get(&name) else {
|
||||
@@ -338,6 +339,7 @@ impl OperationEnv for OverlayOperationEnv {
|
||||
.clone()
|
||||
.unwrap_or_else(ScopedPeerEnv::empty);
|
||||
access_control = registration.spec.access_control.clone();
|
||||
resource_id_path = registration.spec.resource_id_path.clone();
|
||||
}
|
||||
|
||||
let caller_identity = if parent.internal {
|
||||
@@ -348,7 +350,14 @@ impl OperationEnv for OverlayOperationEnv {
|
||||
} else {
|
||||
parent.identity.clone()
|
||||
};
|
||||
if let AccessResult::Forbidden(message) = access_control.check(caller_identity.as_ref()) {
|
||||
let resource_id = resource_id_path
|
||||
.as_ref()
|
||||
.and_then(|path| crate::registry::registration::extract_json_pointer(&input, path));
|
||||
if let AccessResult::Forbidden(message) = access_control.check(
|
||||
caller_identity.as_ref(),
|
||||
resource_id.as_deref(),
|
||||
parent.ownership.as_deref(),
|
||||
) {
|
||||
return ResponseEnvelope::forbidden(parent.request_id.clone(), message);
|
||||
}
|
||||
|
||||
@@ -368,6 +377,7 @@ impl OperationEnv for OverlayOperationEnv {
|
||||
scoped_env,
|
||||
env: parent.env.clone(),
|
||||
internal: true,
|
||||
ownership: parent.ownership.clone(),
|
||||
};
|
||||
|
||||
match handler {
|
||||
@@ -487,6 +497,7 @@ mod tests {
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
AccessControl::default(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -525,6 +536,7 @@ mod tests {
|
||||
abort_policy: AbortPolicy::default(),
|
||||
deadline: Some(Instant::now() + Duration::from_secs(30)),
|
||||
internal: true,
|
||||
ownership: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1002,6 +1014,7 @@ mod tests {
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
AccessControl::default(),
|
||||
None,
|
||||
),
|
||||
HandlerKind::Stream(streaming_handler),
|
||||
OperationProvenance::FromCall,
|
||||
|
||||
@@ -16,6 +16,7 @@ use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use alknet_core::auth::{AuthToken, Identity, IdentityProvider};
|
||||
use alknet_core::ownership::OwnershipProvider;
|
||||
use alknet_core::types::StreamError;
|
||||
use futures::stream::StreamExt;
|
||||
use serde_json::Value;
|
||||
@@ -70,6 +71,7 @@ pub struct Dispatcher {
|
||||
pub registry: Arc<OperationRegistry>,
|
||||
pub identity_provider: Arc<dyn IdentityProvider>,
|
||||
pub session_source: Option<Arc<dyn SessionOverlaySource + Send + Sync>>,
|
||||
pub ownership_provider: Option<Arc<dyn OwnershipProvider>>,
|
||||
pub default_timeout: Duration,
|
||||
}
|
||||
|
||||
@@ -82,6 +84,7 @@ impl Dispatcher {
|
||||
registry,
|
||||
identity_provider,
|
||||
session_source: None,
|
||||
ownership_provider: None,
|
||||
default_timeout: DEFAULT_TIMEOUT,
|
||||
}
|
||||
}
|
||||
@@ -99,6 +102,11 @@ impl Dispatcher {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_ownership_provider(mut self, provider: Arc<dyn OwnershipProvider>) -> Self {
|
||||
self.ownership_provider = Some(provider);
|
||||
self
|
||||
}
|
||||
|
||||
fn strip_leading_slash(operation_id: &str) -> &str {
|
||||
operation_id.strip_prefix('/').unwrap_or(operation_id)
|
||||
}
|
||||
@@ -182,6 +190,7 @@ impl Dispatcher {
|
||||
env: stub_env,
|
||||
abort_policy: AbortPolicy::default(),
|
||||
internal: false,
|
||||
ownership: self.ownership_provider.clone(),
|
||||
};
|
||||
context.env = self.compose_root_env(connection, &context);
|
||||
context
|
||||
@@ -432,6 +441,7 @@ impl Clone for Dispatcher {
|
||||
registry: Arc::clone(&self.registry),
|
||||
identity_provider: Arc::clone(&self.identity_provider),
|
||||
session_source: self.session_source.clone(),
|
||||
ownership_provider: self.ownership_provider.clone(),
|
||||
default_timeout: self.default_timeout,
|
||||
}
|
||||
}
|
||||
@@ -524,6 +534,7 @@ mod tests {
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
acl,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -539,6 +550,7 @@ mod tests {
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
acl,
|
||||
None,
|
||||
),
|
||||
HandlerKind::Once(make_handler(|input, context| async move {
|
||||
ResponseEnvelope::ok(context.request_id, input)
|
||||
@@ -1001,6 +1013,7 @@ mod tests {
|
||||
serde_json::json!({}),
|
||||
vec![],
|
||||
acl,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user