Harden auth/vault path sanitization and clean WeKnora docs
This commit is contained in:
@@ -255,7 +255,8 @@ impl CommandContext {
|
||||
editor_dirty: false,
|
||||
editor_dirty_state: Some("clean".to_string()),
|
||||
editor_has_selection: false,
|
||||
ai_can_write: true,
|
||||
// 最小权限:默认禁止 AI 写;调用方确认需要时再 with_ai_can_write(true)
|
||||
ai_can_write: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,11 +352,15 @@ impl CommandContext {
|
||||
self
|
||||
}
|
||||
|
||||
/// 用 builder 模式设置 editor dirty。
|
||||
/// 用 builder 模式设置 editor dirty;与 `editor_dirty_state` 双向同步。
|
||||
pub fn with_editor_dirty(mut self, dirty: bool) -> Self {
|
||||
self.editor_dirty = dirty;
|
||||
if dirty && self.editor_dirty_state.as_deref() == Some("clean") {
|
||||
if dirty {
|
||||
// dirty=true 时始终落到 dirty 状态(含从 clean/其它状态同步)
|
||||
self.editor_dirty_state = Some("dirty".to_string());
|
||||
} else {
|
||||
// dirty=false 时重置为 clean,避免两字段语义分裂
|
||||
self.editor_dirty_state = Some("clean".to_string());
|
||||
}
|
||||
self
|
||||
}
|
||||
@@ -700,10 +705,11 @@ pub fn check_when(ctx: &CommandContext, when_expr: &str) -> Result<bool, String>
|
||||
|
||||
/// 检查一个命令在指定上下文中是否应 enabled。
|
||||
/// 如果 when 表达式为 None 或空,默认返回 true。
|
||||
/// when 解析失败时 fail-closed:返回 false(禁止因坏表达式默认放开命令)。
|
||||
pub fn is_command_enabled(ctx: &CommandContext, when: Option<&str>) -> bool {
|
||||
match when {
|
||||
None | Some("") => true,
|
||||
Some(expr) => check_when(ctx, expr).unwrap_or(true),
|
||||
Some(expr) => check_when(ctx, expr).unwrap_or(false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -905,8 +911,8 @@ mod command_context_tests {
|
||||
#[test]
|
||||
fn command_context_parse_error_returns_default() {
|
||||
let ctx = CommandContext::new();
|
||||
// 解析错误 → is_command_enabled 返回 true(默认允许)
|
||||
assert!(is_command_enabled(&ctx, Some("invalid syntax &&&")));
|
||||
// 解析错误 → fail-closed:is_command_enabled 返回 false(禁止默认放开)
|
||||
assert!(!is_command_enabled(&ctx, Some("invalid syntax &&&")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -60,7 +60,14 @@ impl JobTicket {
|
||||
}
|
||||
|
||||
pub fn decide_access(context: &AccessContext) -> AccessDecision {
|
||||
if context.tenant_id.is_none() {
|
||||
// None 与空串/纯空白均视为缺 tenant,避免 `Some("")` 绕过鉴权。
|
||||
let tenant_missing = context
|
||||
.tenant_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.is_none();
|
||||
if tenant_missing {
|
||||
return AccessDecision::Deny(AccessDenyReason::MissingTenant);
|
||||
}
|
||||
if context.workspace_id.trim().is_empty() {
|
||||
@@ -100,6 +107,21 @@ mod tests {
|
||||
assert_eq!(decide_access(&context), AccessDecision::Allow);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn access_denies_empty_tenant_string() {
|
||||
let context = AccessContext {
|
||||
tenant_id: Some(" ".to_string()),
|
||||
workspace_id: "ws-1".to_string(),
|
||||
actor_id: "user-1".to_string(),
|
||||
actor_type: "human".to_string(),
|
||||
source: "react-next".to_string(),
|
||||
};
|
||||
assert_eq!(
|
||||
decide_access(&context),
|
||||
AccessDecision::Deny(AccessDenyReason::MissingTenant)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn job_ticket_keeps_trace_fields() {
|
||||
let ticket = JobTicket::new(
|
||||
|
||||
@@ -246,7 +246,13 @@ impl DocumentBuffer {
|
||||
|
||||
pub fn mark_dirty(&mut self, content_hash: String) {
|
||||
self.current_content_hash = Some(content_hash);
|
||||
self.dirty_state = DocBufferDirtyState::Dirty;
|
||||
// Stale / Deleted 不可被普通编辑覆盖:保留外部冲突与删除语义。
|
||||
match self.dirty_state {
|
||||
DocBufferDirtyState::Stale | DocBufferDirtyState::Deleted => {}
|
||||
_ => {
|
||||
self.dirty_state = DocBufferDirtyState::Dirty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_saved(&mut self, file_version: String, content_hash: String) {
|
||||
@@ -298,6 +304,14 @@ impl DocumentBuffer {
|
||||
pub fn mark_deleted(&mut self) {
|
||||
self.dirty_state = DocBufferDirtyState::Deleted;
|
||||
}
|
||||
|
||||
/// 从回收站 restore 后清除 deleted 标记,使已打开 buffer 可再次编辑。
|
||||
/// 恢复为 Clean:文件已回到磁盘,当前 buffer 内容仍视为与 base 一致直至下一次外部变更检测。
|
||||
pub fn clear_deleted(&mut self) {
|
||||
if self.dirty_state == DocBufferDirtyState::Deleted {
|
||||
self.dirty_state = DocBufferDirtyState::Clean;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@@ -1109,6 +1123,11 @@ mod tests {
|
||||
assert_eq!(buf.dirty_state, DocBufferDirtyState::Stale);
|
||||
assert_eq!(buf.external_actor.as_deref(), Some("external-editor"));
|
||||
|
||||
// Stale 时继续编辑只更新 content_hash,不得降级回 Dirty。
|
||||
buf.mark_dirty("sha256:stale-edit".into());
|
||||
assert_eq!(buf.dirty_state, DocBufferDirtyState::Stale);
|
||||
assert_eq!(buf.current_content_hash.as_deref(), Some("sha256:stale-edit"));
|
||||
|
||||
buf.mark_saved("v2".into(), "sha256:saved".into());
|
||||
assert!(!buf.is_dirty());
|
||||
assert_eq!(buf.dirty_state, DocBufferDirtyState::Clean);
|
||||
@@ -1119,5 +1138,9 @@ mod tests {
|
||||
|
||||
buf.mark_deleted();
|
||||
assert_eq!(buf.dirty_state, DocBufferDirtyState::Deleted);
|
||||
buf.mark_dirty("sha256:after-delete".into());
|
||||
assert_eq!(buf.dirty_state, DocBufferDirtyState::Deleted);
|
||||
buf.clear_deleted();
|
||||
assert_eq!(buf.dirty_state, DocBufferDirtyState::Clean);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,8 @@ pub struct MindmapKernelProjection {
|
||||
pub source: MindmapProjectionSource,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
// style/refs 含 serde_json::Value(可能含 f64),不实现 Eq;仅保留 PartialEq。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MindmapKernelNode {
|
||||
pub node_id: String,
|
||||
|
||||
Reference in New Issue
Block a user