astrid_kernel/kernel_router/admin/
mod.rs1#[cfg(test)]
25mod enforcement_tests;
26mod handlers;
27#[cfg(test)]
28mod state_tests;
29#[cfg(test)]
30mod state_tests_agent_modify;
31#[cfg(test)]
32mod tests;
33
34use std::sync::Arc;
35
36use astrid_audit::{AuditOutcome, AuthorizationProof};
37use astrid_core::principal::PrincipalId;
38use astrid_events::ipc::IpcPayload;
39use astrid_events::kernel_api::{
40 AdminKernelRequest, AdminKernelResponse, AdminRequestKind, AdminResponseBody,
41};
42use tracing::warn;
43
44use super::{
45 AdminAuditEntry, AuthorityScope, authorize_request, publish_response, record_admin_audit,
46 resolve_caller,
47};
48
49const ADMIN_TOPIC_PREFIX: &str = "astrid.v1.admin.";
51const ADMIN_RESPONSE_PREFIX: &str = "astrid.v1.admin.response.";
53
54pub(crate) fn spawn_admin_router(kernel: Arc<crate::Kernel>) -> tokio::task::JoinHandle<()> {
58 let mut receiver = kernel.event_bus.subscribe_topic("astrid.v1.admin.*");
59
60 tokio::spawn(async move {
61 while let Some(event) = receiver.recv().await {
62 let astrid_events::AstridEvent::Ipc { message, .. } = &*event else {
63 continue;
64 };
65
66 if message.topic.starts_with(ADMIN_RESPONSE_PREFIX) {
68 continue;
69 }
70
71 let IpcPayload::RawJson(val) = &message.payload else {
72 continue;
73 };
74
75 match serde_json::from_value::<AdminKernelRequest>(val.clone()) {
76 Ok(req) => {
77 let caller = resolve_caller(message);
78 handle_admin_request(&kernel, message.topic.clone(), caller, req).await;
79 },
80 Err(e) => {
81 warn!(
82 error = %e,
83 topic = %message.topic,
84 "Failed to parse AdminKernelRequest from IPC"
85 );
86 },
87 }
88 }
89 })
90}
91
92fn admin_response_topic(input_topic: &str) -> String {
94 input_topic.strip_prefix(ADMIN_TOPIC_PREFIX).map_or_else(
95 || input_topic.to_string(),
96 |suffix| format!("{ADMIN_RESPONSE_PREFIX}{suffix}"),
97 )
98}
99
100#[must_use]
108pub fn resolve_admin_scope(req: &AdminRequestKind, caller: &PrincipalId) -> AuthorityScope {
109 match req {
110 AdminRequestKind::QuotaGet { principal } | AdminRequestKind::QuotaSet { principal, .. } => {
111 if principal == caller {
112 AuthorityScope::Self_
113 } else {
114 AuthorityScope::Global
115 }
116 },
117 AdminRequestKind::AgentList | AdminRequestKind::GroupList => AuthorityScope::Self_,
130 AdminRequestKind::AgentCreate { .. }
131 | AdminRequestKind::AgentDelete { .. }
132 | AdminRequestKind::AgentEnable { .. }
133 | AdminRequestKind::AgentDisable { .. }
134 | AdminRequestKind::AgentModify { .. }
135 | AdminRequestKind::GroupCreate { .. }
136 | AdminRequestKind::GroupDelete { .. }
137 | AdminRequestKind::GroupModify { .. }
138 | AdminRequestKind::CapsGrant { .. }
139 | AdminRequestKind::CapsRevoke { .. } => AuthorityScope::Global,
140 }
141}
142
143#[must_use]
153pub fn required_capability_for_admin_request(
154 req: &AdminRequestKind,
155 scope: AuthorityScope,
156) -> &'static str {
157 match (req, scope) {
158 (AdminRequestKind::AgentCreate { .. }, _) => "agent:create",
159 (AdminRequestKind::AgentDelete { .. }, _) => "agent:delete",
160 (AdminRequestKind::AgentEnable { .. }, _) => "agent:enable",
161 (AdminRequestKind::AgentDisable { .. }, _) => "agent:disable",
162 (AdminRequestKind::AgentModify { .. }, _) => "agent:modify",
163 (AdminRequestKind::AgentList, AuthorityScope::Self_) => "self:agent:list",
164 (AdminRequestKind::AgentList, AuthorityScope::Global) => "agent:list",
165 (AdminRequestKind::QuotaSet { .. }, AuthorityScope::Self_) => "self:quota:set",
166 (AdminRequestKind::QuotaSet { .. }, AuthorityScope::Global) => "quota:set",
167 (AdminRequestKind::QuotaGet { .. }, AuthorityScope::Self_) => "self:quota:get",
168 (AdminRequestKind::QuotaGet { .. }, AuthorityScope::Global) => "quota:get",
169 (AdminRequestKind::GroupCreate { .. }, _) => "group:create",
170 (AdminRequestKind::GroupDelete { .. }, _) => "group:delete",
171 (AdminRequestKind::GroupModify { .. }, _) => "group:modify",
172 (AdminRequestKind::GroupList, AuthorityScope::Self_) => "self:group:list",
173 (AdminRequestKind::GroupList, AuthorityScope::Global) => "group:list",
174 (AdminRequestKind::CapsGrant { .. }, _) => "caps:grant",
175 (AdminRequestKind::CapsRevoke { .. }, _) => "caps:revoke",
176 }
177}
178
179#[must_use]
182pub fn admin_request_method(req: &AdminRequestKind) -> &'static str {
183 match req {
184 AdminRequestKind::AgentCreate { .. } => "admin.agent.create",
185 AdminRequestKind::AgentDelete { .. } => "admin.agent.delete",
186 AdminRequestKind::AgentEnable { .. } => "admin.agent.enable",
187 AdminRequestKind::AgentDisable { .. } => "admin.agent.disable",
188 AdminRequestKind::AgentModify { .. } => "admin.agent.modify",
189 AdminRequestKind::AgentList => "admin.agent.list",
190 AdminRequestKind::QuotaSet { .. } => "admin.quota.set",
191 AdminRequestKind::QuotaGet { .. } => "admin.quota.get",
192 AdminRequestKind::GroupCreate { .. } => "admin.group.create",
193 AdminRequestKind::GroupDelete { .. } => "admin.group.delete",
194 AdminRequestKind::GroupModify { .. } => "admin.group.modify",
195 AdminRequestKind::GroupList => "admin.group.list",
196 AdminRequestKind::CapsGrant { .. } => "admin.caps.grant",
197 AdminRequestKind::CapsRevoke { .. } => "admin.caps.revoke",
198 }
199}
200
201#[must_use]
204pub fn admin_target_principal(req: &AdminRequestKind) -> Option<&PrincipalId> {
205 match req {
206 AdminRequestKind::AgentDelete { principal }
207 | AdminRequestKind::AgentEnable { principal }
208 | AdminRequestKind::AgentDisable { principal }
209 | AdminRequestKind::AgentModify { principal, .. }
210 | AdminRequestKind::QuotaSet { principal, .. }
211 | AdminRequestKind::QuotaGet { principal }
212 | AdminRequestKind::CapsGrant { principal, .. }
213 | AdminRequestKind::CapsRevoke { principal, .. } => Some(principal),
214 AdminRequestKind::AgentCreate { .. }
215 | AdminRequestKind::AgentList
216 | AdminRequestKind::GroupCreate { .. }
217 | AdminRequestKind::GroupDelete { .. }
218 | AdminRequestKind::GroupModify { .. }
219 | AdminRequestKind::GroupList => None,
220 }
221}
222
223async fn handle_admin_request(
224 kernel: &Arc<crate::Kernel>,
225 topic: String,
226 caller: PrincipalId,
227 req: AdminKernelRequest,
228) {
229 let response_topic = admin_response_topic(&topic);
230 let request_id = req.request_id.clone();
231 let method = admin_request_method(&req.kind);
232 let scope = resolve_admin_scope(&req.kind, &caller);
233 let required_cap = required_capability_for_admin_request(&req.kind, scope);
234 let target = admin_target_principal(&req.kind).cloned();
235 let audit_params = serde_json::to_value(&req.kind).ok();
239
240 match authorize_request(kernel, &caller, required_cap) {
241 Ok(()) => {
242 record_admin_audit(
243 kernel,
244 AdminAuditEntry {
245 caller: &caller,
246 method,
247 required_cap,
248 target_principal: target.clone(),
249 params: audit_params.clone(),
250 authorization: AuthorizationProof::System {
251 reason: format!("policy allow: {caller} holds {required_cap}"),
252 },
253 outcome: AuditOutcome::success(),
254 },
255 );
256 },
257 Err(e) => {
258 warn!(
259 security_event = true,
260 method = method,
261 principal = %caller,
262 required = required_cap,
263 error = %e,
264 "Permission check denied admin request"
265 );
266 record_admin_audit(
267 kernel,
268 AdminAuditEntry {
269 caller: &caller,
270 method,
271 required_cap,
272 target_principal: target,
273 params: audit_params,
274 authorization: AuthorizationProof::Denied {
275 reason: e.to_string(),
276 },
277 outcome: AuditOutcome::failure(e.to_string()),
278 },
279 );
280 publish_response(
281 kernel,
282 response_topic,
283 AdminKernelResponse::for_request(
284 request_id,
285 AdminResponseBody::Error(e.to_string()),
286 ),
287 );
288 return;
289 },
290 }
291
292 let body = handlers::dispatch(kernel, req.kind).await;
293 publish_response(
294 kernel,
295 response_topic,
296 AdminKernelResponse::for_request(request_id, body),
297 );
298}