Skip to content

Commit 710ae0b

Browse files
author
developerworks
committed
Tighten control command audit validation
- Reject empty audit fields in public control entrypoints and runtime loops - Add control command audit tests and update manual guidance
1 parent 012a736 commit 710ae0b

8 files changed

Lines changed: 82 additions & 2 deletions

File tree

README.zh.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
- 使用 `OneForOne`(一对一), `OneForAll`(一对全部) 和 `RestForOne`(后续一组) supervision strategy(监督策略).
3131
- 从 typed failure(类型化失败), backoff(退避), jitter(抖动), fuse rule(熔断规则) 和 policy engine(策略引擎) 生成 `RestartDecision`(重启决策).
3232
- 通过 `SupervisorHandle`(监督器句柄) 控制运行中的树, 包括 `add_child`, `remove_child`, `restart_child`, `pause_child`, `resume_child`, `quarantine_child`, `shutdown_tree`, `current_state``subscribe_events`.
33+
- 控制命令必须携带非空 `requested_by`(请求者) 和 `reason`(原因), 公共控制入口和 runtime control loop(运行时控制循环) 都会在执行前校验审计字段.
3334
-`examples/config/supervisor.yaml` 加载主 YAML(数据序列化格式) 配置.
3435
- 复用 `rust_supervisor::config::configurable::SupervisorConfig` 完成 YAML(数据序列化格式) 加载, template generation(模板生成) 和 JSON Schema(JSON 结构模式) 生成.
3536
- 发出 structured log(结构化日志), tracing span(追踪跨度), metrics(指标), audit event(审计事件), event journal entry(事件日志条目) 和 `RunSummary`(运行摘要) diagnostics(诊断信息).

manual/en/dashboard.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ The screenshot below shows the dashboard client view for target lists, topology,
1010

1111
## Three-End Responsibilities
1212

13-
- `rust-supervisor`: The target process reads `SupervisorConfig`, opens a Unix domain socket when `ipc.enabled=true`, and produces snapshots, event records, log records, command results, and registration heartbeats.
13+
- `rust-supervisor`: The target process reads `SupervisorConfig`, opens a Unix domain socket when `ipc.enabled=true`, and produces state, event records, log records, command results, and registration heartbeats.
1414
- `rust-supervisor-relay`: The relay listens on the registration socket, stores the target registry, exposes external `wss://` dashboard sessions, validates mTLS and allowed IPC path prefixes, and forwards session commands to the target process.
1515
- `rust-supervisor-ui`: The dashboard client connects to the relay through `wss://` and displays the target list, topology, state, event stream, log tail, and command audit.
1616

manual/zh/dashboard.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ dashboard(看板) 功能由三个仓库共同完成. `rust-supervisor` 只负责
1010

1111
## 三端职责
1212

13-
- `rust-supervisor`: target process(目标进程) 读取 `SupervisorConfig`(监督器配置), 在 `ipc.enabled=true` 时打开 Unix domain socket(Unix 域套接字), 并生成 snapshot(快照), event record(事件记录), log record(日志记录), command result(命令结果) 和 registration heartbeat(注册心跳).
13+
- `rust-supervisor`: target process(目标进程) 读取 `SupervisorConfig`(监督器配置), 在 `ipc.enabled=true` 时打开 Unix domain socket(Unix 域套接字), 并生成 state(状态), event record(事件记录), log record(日志记录), command result(命令结果) 和 registration heartbeat(注册心跳).
1414
- `rust-supervisor-relay`: relay(中继) 监听 registration socket(注册套接字), 保存 target registry(目标注册表), 对外提供 `wss://` dashboard session(看板会话), 校验 mTLS(双向传输层安全协议认证) 和 allowed IPC path prefix(允许的进程间通信路径前缀), 并把会话命令转发到 target process(目标进程).
1515
- `rust-supervisor-ui`: dashboard client(看板客户端) 通过 `wss://` 连接 relay(中继), 显示 target list(目标列表), topology(拓扑), state(状态), event stream(事件流), log tail(日志尾部) 和 command audit(命令审计).
1616

manual/zh/runtime-control.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,5 @@
2929
## 审计数据
3030

3131
每个控制命令都带有 `requested_by`(请求者), `reason`(原因), `target_path`(目标路径), `accepted_at`(接受时间)和 `command_id`(命令标识). 这些字段用于 audit event(审计事件)和问题追踪.
32+
33+
`requested_by`(请求者) 和 `reason`(原因) 必须提供非空文本. `SupervisorHandle`(监督器句柄) 会在命令进入 channel(通道) 前拒绝空值, runtime control loop(运行时控制循环) 也会在执行命令前再次校验. 这样做可以保证人工操作, dashboard IPC(看板进程间通信) 转发和内部控制调用都留下可追踪的审计来源.

src/control/command.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! This module owns auditable command inputs and command results. Runtime code
44
//! executes these commands and records state changes.
55
6+
use crate::error::types::SupervisorError;
67
use crate::id::types::{ChildId, SupervisorPath};
78
use crate::shutdown::coordinator::ShutdownResult;
89
use serde::{Deserialize, Serialize};
@@ -75,6 +76,20 @@ impl CommandMeta {
7576
reason: reason.into(),
7677
}
7778
}
79+
80+
/// Validates audit metadata before command dispatch.
81+
///
82+
/// # Arguments
83+
///
84+
/// This function has no arguments.
85+
///
86+
/// # Returns
87+
///
88+
/// Returns `Ok(())` when actor and reason fields are non-empty.
89+
pub(crate) fn validate(&self) -> Result<(), SupervisorError> {
90+
validate_required_text(&self.requested_by, "requested_by")?;
91+
validate_required_text(&self.reason, "reason")
92+
}
7893
}
7994

8095
/// Runtime command sent to the control loop.
@@ -158,6 +173,38 @@ impl ControlCommand {
158173
| Self::CurrentState { meta } => meta,
159174
}
160175
}
176+
177+
/// Validates audit metadata attached to this command.
178+
///
179+
/// # Arguments
180+
///
181+
/// This function has no arguments.
182+
///
183+
/// # Returns
184+
///
185+
/// Returns `Ok(())` when the command carries auditable metadata.
186+
pub(crate) fn validate_audit_metadata(&self) -> Result<(), SupervisorError> {
187+
self.meta().validate()
188+
}
189+
}
190+
191+
/// Validates one required text field.
192+
///
193+
/// # Arguments
194+
///
195+
/// - `value`: Text value supplied by the command caller.
196+
/// - `field`: Field name used in the diagnostic message.
197+
///
198+
/// # Returns
199+
///
200+
/// Returns `Ok(())` when the value is not blank.
201+
fn validate_required_text(value: &str, field: &str) -> Result<(), SupervisorError> {
202+
if value.trim().is_empty() {
203+
return Err(SupervisorError::InvalidTransition {
204+
message: format!("control command {field} must not be empty"),
205+
});
206+
}
207+
Ok(())
161208
}
162209

163210
/// State assigned to a managed child by the control loop.

src/control/handle.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ impl SupervisorHandle {
263263
///
264264
/// Returns a command result or a supervisor error when the runtime is gone.
265265
async fn send(&self, command: ControlCommand) -> Result<CommandResult, SupervisorError> {
266+
command.validate_audit_metadata()?;
266267
let (reply_sender, reply_receiver) = oneshot::channel();
267268
self.command_sender
268269
.send(RuntimeCommand::Control {

src/runtime/control_loop.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ impl RuntimeControlState {
133133
&mut self,
134134
command: ControlCommand,
135135
) -> Result<CommandResult, SupervisorError> {
136+
command.validate_audit_metadata()?;
136137
match command {
137138
ControlCommand::AddChild { child_manifest, .. } => {
138139
self.ensure_dynamic_child_allowed()?;

src/tests/supervisor_control_test.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! These tests verify idempotent command handling through the public handle.
44
55
use rust_supervisor::control::command::{CommandResult, ManagedChildState};
6+
use rust_supervisor::error::types::SupervisorError;
67
use rust_supervisor::id::types::{ChildId, SupervisorPath};
78
use rust_supervisor::runtime::supervisor::Supervisor;
89
use rust_supervisor::spec::supervisor::SupervisorSpec;
@@ -50,3 +51,30 @@ async fn control_commands_update_child_state() {
5051
}
5152
));
5253
}
54+
55+
/// Verifies that control commands require auditable metadata.
56+
#[tokio::test]
57+
async fn control_commands_reject_empty_audit_metadata() {
58+
let handle = Supervisor::start(SupervisorSpec::root(Vec::new()))
59+
.await
60+
.expect("start supervisor");
61+
let child_id = ChildId::new("worker");
62+
63+
let missing_actor = handle
64+
.pause_child(child_id, " ", "maintenance window")
65+
.await;
66+
assert_invalid_transition(missing_actor, "requested_by");
67+
68+
let missing_reason = handle.shutdown_tree("operator", "\t").await;
69+
assert_invalid_transition(missing_reason, "reason");
70+
}
71+
72+
/// Asserts that a command returned the expected invalid transition field.
73+
fn assert_invalid_transition(result: Result<CommandResult, SupervisorError>, expected_field: &str) {
74+
match result {
75+
Err(SupervisorError::InvalidTransition { message }) => {
76+
assert!(message.contains(expected_field), "{message}");
77+
}
78+
other => panic!("unexpected command result: {other:?}"),
79+
}
80+
}

0 commit comments

Comments
 (0)