#[cfg(test)]
mod enforcement_tests;
mod handlers;
#[cfg(test)]
mod state_tests;
#[cfg(test)]
mod state_tests_agent_modify;
#[cfg(test)]
mod tests;
use std::sync::Arc;
use astrid_audit::{AuditOutcome, AuthorizationProof};
use astrid_core::principal::PrincipalId;
use astrid_events::ipc::IpcPayload;
use astrid_events::kernel_api::{
AdminKernelRequest, AdminKernelResponse, AdminRequestKind, AdminResponseBody,
};
use tracing::warn;
use super::{
AdminAuditEntry, AuthorityScope, authorize_request, publish_response, record_admin_audit,
resolve_caller,
};
const ADMIN_TOPIC_PREFIX: &str = "astrid.v1.admin.";
const ADMIN_RESPONSE_PREFIX: &str = "astrid.v1.admin.response.";
pub(crate) fn spawn_admin_router(kernel: Arc<crate::Kernel>) -> tokio::task::JoinHandle<()> {
let mut receiver = kernel.event_bus.subscribe_topic("astrid.v1.admin.*");
tokio::spawn(async move {
while let Some(event) = receiver.recv().await {
let astrid_events::AstridEvent::Ipc { message, .. } = &*event else {
continue;
};
if message.topic.starts_with(ADMIN_RESPONSE_PREFIX) {
continue;
}
let IpcPayload::RawJson(val) = &message.payload else {
continue;
};
match serde_json::from_value::<AdminKernelRequest>(val.clone()) {
Ok(req) => {
let caller = resolve_caller(message);
handle_admin_request(&kernel, message.topic.clone(), caller, req).await;
},
Err(e) => {
warn!(
error = %e,
topic = %message.topic,
"Failed to parse AdminKernelRequest from IPC"
);
},
}
}
})
}
fn admin_response_topic(input_topic: &str) -> String {
input_topic.strip_prefix(ADMIN_TOPIC_PREFIX).map_or_else(
|| input_topic.to_string(),
|suffix| format!("{ADMIN_RESPONSE_PREFIX}{suffix}"),
)
}
#[must_use]
pub fn resolve_admin_scope(req: &AdminRequestKind, caller: &PrincipalId) -> AuthorityScope {
match req {
AdminRequestKind::QuotaGet { principal } | AdminRequestKind::QuotaSet { principal, .. } => {
if principal == caller {
AuthorityScope::Self_
} else {
AuthorityScope::Global
}
},
AdminRequestKind::AgentList | AdminRequestKind::GroupList => AuthorityScope::Self_,
AdminRequestKind::AgentCreate { .. }
| AdminRequestKind::AgentDelete { .. }
| AdminRequestKind::AgentEnable { .. }
| AdminRequestKind::AgentDisable { .. }
| AdminRequestKind::AgentModify { .. }
| AdminRequestKind::GroupCreate { .. }
| AdminRequestKind::GroupDelete { .. }
| AdminRequestKind::GroupModify { .. }
| AdminRequestKind::CapsGrant { .. }
| AdminRequestKind::CapsRevoke { .. } => AuthorityScope::Global,
}
}
#[must_use]
pub fn required_capability_for_admin_request(
req: &AdminRequestKind,
scope: AuthorityScope,
) -> &'static str {
match (req, scope) {
(AdminRequestKind::AgentCreate { .. }, _) => "agent:create",
(AdminRequestKind::AgentDelete { .. }, _) => "agent:delete",
(AdminRequestKind::AgentEnable { .. }, _) => "agent:enable",
(AdminRequestKind::AgentDisable { .. }, _) => "agent:disable",
(AdminRequestKind::AgentModify { .. }, _) => "agent:modify",
(AdminRequestKind::AgentList, AuthorityScope::Self_) => "self:agent:list",
(AdminRequestKind::AgentList, AuthorityScope::Global) => "agent:list",
(AdminRequestKind::QuotaSet { .. }, AuthorityScope::Self_) => "self:quota:set",
(AdminRequestKind::QuotaSet { .. }, AuthorityScope::Global) => "quota:set",
(AdminRequestKind::QuotaGet { .. }, AuthorityScope::Self_) => "self:quota:get",
(AdminRequestKind::QuotaGet { .. }, AuthorityScope::Global) => "quota:get",
(AdminRequestKind::GroupCreate { .. }, _) => "group:create",
(AdminRequestKind::GroupDelete { .. }, _) => "group:delete",
(AdminRequestKind::GroupModify { .. }, _) => "group:modify",
(AdminRequestKind::GroupList, AuthorityScope::Self_) => "self:group:list",
(AdminRequestKind::GroupList, AuthorityScope::Global) => "group:list",
(AdminRequestKind::CapsGrant { .. }, _) => "caps:grant",
(AdminRequestKind::CapsRevoke { .. }, _) => "caps:revoke",
}
}
#[must_use]
pub fn admin_request_method(req: &AdminRequestKind) -> &'static str {
match req {
AdminRequestKind::AgentCreate { .. } => "admin.agent.create",
AdminRequestKind::AgentDelete { .. } => "admin.agent.delete",
AdminRequestKind::AgentEnable { .. } => "admin.agent.enable",
AdminRequestKind::AgentDisable { .. } => "admin.agent.disable",
AdminRequestKind::AgentModify { .. } => "admin.agent.modify",
AdminRequestKind::AgentList => "admin.agent.list",
AdminRequestKind::QuotaSet { .. } => "admin.quota.set",
AdminRequestKind::QuotaGet { .. } => "admin.quota.get",
AdminRequestKind::GroupCreate { .. } => "admin.group.create",
AdminRequestKind::GroupDelete { .. } => "admin.group.delete",
AdminRequestKind::GroupModify { .. } => "admin.group.modify",
AdminRequestKind::GroupList => "admin.group.list",
AdminRequestKind::CapsGrant { .. } => "admin.caps.grant",
AdminRequestKind::CapsRevoke { .. } => "admin.caps.revoke",
}
}
#[must_use]
pub fn admin_target_principal(req: &AdminRequestKind) -> Option<&PrincipalId> {
match req {
AdminRequestKind::AgentDelete { principal }
| AdminRequestKind::AgentEnable { principal }
| AdminRequestKind::AgentDisable { principal }
| AdminRequestKind::AgentModify { principal, .. }
| AdminRequestKind::QuotaSet { principal, .. }
| AdminRequestKind::QuotaGet { principal }
| AdminRequestKind::CapsGrant { principal, .. }
| AdminRequestKind::CapsRevoke { principal, .. } => Some(principal),
AdminRequestKind::AgentCreate { .. }
| AdminRequestKind::AgentList
| AdminRequestKind::GroupCreate { .. }
| AdminRequestKind::GroupDelete { .. }
| AdminRequestKind::GroupModify { .. }
| AdminRequestKind::GroupList => None,
}
}
async fn handle_admin_request(
kernel: &Arc<crate::Kernel>,
topic: String,
caller: PrincipalId,
req: AdminKernelRequest,
) {
let response_topic = admin_response_topic(&topic);
let request_id = req.request_id.clone();
let method = admin_request_method(&req.kind);
let scope = resolve_admin_scope(&req.kind, &caller);
let required_cap = required_capability_for_admin_request(&req.kind, scope);
let target = admin_target_principal(&req.kind).cloned();
let audit_params = serde_json::to_value(&req.kind).ok();
match authorize_request(kernel, &caller, required_cap) {
Ok(()) => {
record_admin_audit(
kernel,
AdminAuditEntry {
caller: &caller,
method,
required_cap,
target_principal: target.clone(),
params: audit_params.clone(),
authorization: AuthorizationProof::System {
reason: format!("policy allow: {caller} holds {required_cap}"),
},
outcome: AuditOutcome::success(),
},
);
},
Err(e) => {
warn!(
security_event = true,
method = method,
principal = %caller,
required = required_cap,
error = %e,
"Permission check denied admin request"
);
record_admin_audit(
kernel,
AdminAuditEntry {
caller: &caller,
method,
required_cap,
target_principal: target,
params: audit_params,
authorization: AuthorizationProof::Denied {
reason: e.to_string(),
},
outcome: AuditOutcome::failure(e.to_string()),
},
);
publish_response(
kernel,
response_topic,
AdminKernelResponse::for_request(
request_id,
AdminResponseBody::Error(e.to_string()),
),
);
return;
},
}
let body = handlers::dispatch(kernel, req.kind).await;
publish_response(
kernel,
response_topic,
AdminKernelResponse::for_request(request_id, body),
);
}