# 文件管理：自动提权、归属显示、权限修改 — 设计文档

| 项 | 内容 |
|----|------|
| 日期 | 2026-06-01 |
| 状态 | 已实现 Phase 1–5 + 二期递归 chmod/不可读提示（2026-06-01） |
| 范围 | `#/files` 远程文件管理（浏览、写入、压缩、chmod 等） |
| 关联 | `sync_v2.py` · `asyncssh_pool.py` · `remote_archive.py` · `FilesPage.vue` |

---

## 1. 背景与目标

### 1.1 背景

当前文件管理已具备浏览、编辑、上传、压缩、右键 chmod 等能力，但生产环境普遍存在：

- SSH 登录用户为 **deploy/www** 等非 root 账号；
- 站点目录与文件属主为 **root** 或 **www**，普通用户 **读得到、写不进**；
- 列表展示 `drwxr-xr-x` 等原始 `ls` 权限串，运维难以一眼看出「这是谁家的文件」；
- chmod 仅支持八进制输入，无预设、无 chown，且无 sudo 回退，与保存/压缩已做的提权能力不一致。

用户诉求（原文）：

1. **自动获取 root 权限**（文件操作应能自动提权，而不是每次失败再手工改服务器）；
2. **列表显示权限**，且 **简化为用户维度**（直接显示 `www` / `root` 等，而非冗长权限串）；
3. **右键修改权限**，弹出 **结构化对话框**（而非仅一个 chmod 输入框）。

### 1.2 目标

| 目标 | 可验收标准 |
|------|------------|
| G1 自动提权 | 写、删、压缩、chmod/chown 等在配置允许时自动 `sudo -n` 重试，成功率与 root 登录接近 |
| G2 归属可读 | 列表默认列显示 **属主**（如 `www`），悬停可看完整 `权限 + 用户:组` |
| G3 权限可改 | 右键「修改权限」打开对话框：预设 + 自定义八进制 + 可选改属主/属组 |
| G4 安全可审计 | 提权范围可配置、命令白名单、所有 chmod/chown 记审计日志 |
| G5 不破坏架构 | API → Service/SSH 基础设施层，不在路由里散落 sudo 逻辑 |

### 1.3 非目标（本期不做）

- 不要求 Nexus 在目标机上 **永久改写 sudoers**（类似 Agent 安装的临时注入）作为默认路径——仅文档 + 可选一键脚本；
- 不实现交互式 `sudo` 密码输入（违反无人值守运维）；
- 不把文件管理改成「必须 root SSH 才能用」。

---

## 2. 现状摘要

| 能力 | 现状 | 缺口 |
|------|------|------|
| 浏览 | `ls -la`，解析 `perms`，**未返回 owner/group** | 前端无法显示 www/root |
| 保存/上传 | SFTP → SSH tee/mv，`sudo -n` 部分已做 | 未统一、chmod 未接入 |
| 压缩 | `remote_archive.py` 已 sudo 回退 | — |
| chmod | 仅 `chmod mode path`，无 sudo | 非 root 常失败 |
| 提权模型 | Agent 安装用 `_sudo_wrap` 临时 sudoers | 文件管理未复用同一策略 |

后端 `browse` 已解析 `parts[2]`（owner）、`parts[3]`（group），但未写入 API 响应（见 `sync_v2.py` 约 270–302 行）。

---

## 3. 方案对比

### 3.1 自动提权

| 方案 | 说明 | 优点 | 缺点 |
|------|------|------|------|
| **A. 仅 SSH 用户** | 不 sudo，失败即报错 | 最简单 | 无法满足用户「自动 root」 |
| **B. 统一 sudo -n 回退** | 先普通执行，Permission denied 再 `sudo -n bash -c` | 与现网 partial 实现一致；可审计 | 需目标机配置 NOPASSWD 白名单 |
| **C. 每次临时写 sudoers** | 复用 `servers._sudo_wrap` | 无预先配置 | 安全风险高、慢、需 root 写 sudoers |
| **D. 文件管理强制 root SSH** | 凭据层只用 root | 权限足 | 违背最小权限；很多站点不给 root |

**选定：B（主） + 安装向导/文档配置 NOPASSWD（辅）**

- 业务命令统一走 `exec_ssh_command_with_fallback()`（从 `remote_archive` 提升到 `remote_shell.py`）。
- 对 **非 root** SSH 用户：失败且 stderr 含 permission denied → 自动 `sudo -n bash -c '...'`。
- **root** SSH 用户：不包 sudo。
- 提供 **服务器级开关**（见 §5.2），默认 `auto_sudo`。

不推荐 C 作为默认：与 Agent 安装场景不同，文件管理高频、面过大。

### 3.2 权限展示

| 方案 | 列表主列 | 说明 |
|------|----------|------|
| **S1** | `drwxr-xr-x` | 现状 |
| **S2** | `www` | 仅属主 |
| **S3** | `www · 755` | 属主 + 八进制简写（从 perms 解析） |
| **S4** | Chip `www` + tooltip 全文 | **推荐**：主信息属主，详情悬停 |

**选定：S4**

- 列名：**归属**（`owner` 字段，展示 `owner` 或 `owner:group` 若不同）。
- 辅助解析 `mode_octal`：`755` / `644` 供排序或二级展示（可选列，默认隐藏或放在 tooltip）。
- `symlink` 仍显示 `→ target`，归属列显示 `-` 或链接目标属主。

### 3.3 修改权限对话框

| 方案 | 内容 |
|------|------|
| **P1** | 仅八进制 chmod（现状） |
| **P2** | 预设按钮 + 八进制 + chown（**推荐**） |
| **P3** | 完整 Unix 权限矩阵（rwx 勾选） |

**选定：P2** — 满足运维习惯，实现成本低于 P3。

---

## 4. 选定方案详设

### 4.1 统一远程 Shell 执行层

新增模块（建议路径）：

```
server/infrastructure/ssh/remote_shell.py
```

职责：

```python
# 概念接口（实现时写完整类型）

class FilesElevation(str, Enum):
    OFF = "off"           # 从不 sudo
    AUTO = "auto_sudo"    # 默认：失败且权限错误时 sudo -n
    ALWAYS = "always_sudo"  # 所有命令直接 sudo -n（可选，仅排障）

async def exec_remote_command(
    server: Server,
    command: str,
    *,
    timeout: int = 30,
    elevation: FilesElevation | None = None,  # None → 读服务器/全局配置
) -> CommandResult: ...

def wrap_sudo(command: str) -> str:
    return f"sudo -n bash -c {shlex.quote(command)}"
```

**接入范围（须全部走统一层）**

| 操作 | 当前入口 | 改造 |
|------|----------|------|
| browse | `exec_ssh_command(ls)` | 保持直接执行（只读，一般不需 sudo） |
| write-file / upload | `remote_write_bytes` | 已有多级回退，并入 elevation 策略 |
| delete / mkdir / rename | `file-ops` | 加 fallback |
| compress / decompress | `remote_archive` | 已有 fallback，迁到 remote_shell |
| chmod / chown | `chmod` 端点 | 扩展 payload + fallback |
| read-file | `cat` | 一般只读；可选对不可读文件 sudo cat（二期） |

**提权判定**

```text
exit_code != 0
且 (
  "permission denied" in stderr.lower()
  或 "operation not permitted" in stderr.lower()
)
→ 若 elevation == AUTO，重试 wrap_sudo(command)
```

### 4.2 目标机 sudo 配置（运维一次性）

在 `docs/deploy/` 增加模板 **`nexus-files.sudoers`**（示例）：

```sudoers
# /etc/sudoers.d/nexus-files — 由运维审阅后安装
deploy ALL=(ALL) NOPASSWD: /bin/chmod, /bin/chown, /usr/bin/chown,
    /bin/mv, /bin/cp, /bin/mkdir, /bin/rm, /bin/tar, /usr/bin/zip,
    /usr/bin/unzip, /usr/bin/tee, /usr/bin/cat
```

说明：

- Nexus **不会**默认自动创建该文件（避免未授权扩权）；
- 安装向导或「服务器详情 → 文件管理」页提供 **检测** API：`GET /api/servers/{id}/files-capability`  
  - 检测项：`ssh_user`、`can_sudo_nopasswd`、`whitelist_ok`；
- 检测失败时，前端 Toast 链到文档锚点。

若 SSH 用户已是 **root**：`can_sudo_nopasswd=true`，无需 sudoers。

### 4.3 浏览 API 扩展

**`POST /api/sync/browse` 响应项扩展**

```json
{
  "name": "index.php",
  "type": "file",
  "size": 1234,
  "modified": "Jun 1 12:00",
  "permissions": "rw-r--r--",
  "mode_octal": "644",
  "owner": "www",
  "group": "www",
  "link_target": null
}
```

| 字段 | 来源 | 说明 |
|------|------|------|
| `permissions` | `ls` 第 1 列 | 保留，供 tooltip / 高级用户 |
| `mode_octal` | 从 perms 解析 | `rwxr-xr-x` → `755`（含 setuid 等用 4 位，少见则原样） |
| `owner` | `ls` 第 3 列 | **列表主展示** |
| `group` | `ls` 第 4 列 | 与 owner 相同时 UI 只显示 owner |

解析工具：`frontend/src/utils/fileMode.ts` + 后端 `server/utils/unix_ls.py`（避免前后端不一致，**以后端为准**）。

**browse 可选增强（二期）**：`ls -la --time-style=long-iso` 统一时间格式。

### 4.4 前端列表 UI

**列调整（`FilesPage.vue`）**

| 列 | 默认 | 展示 |
|----|------|------|
| 名称 | 显示 | 不变 |
| 归属 | **显示**（新） | `v-chip`：`www` / `root`；`www:nogroup` 简化为 `www` |
| 大小 | 显示 | 不变 |
| 修改时间 | 显示 | 不变 |
| 权限 | **隐藏**（可选展开） | tooltip：`drwxr-xr-x · 755` |

**样式约定**

- `owner === 'root'` → chip 颜色 `warning`（提醒系统文件）
- 当前 SSH 用户无写权限且非 root 属主 → 行尾可选 icon `mdi-shield-lock`（仅提示，不阻断只读）

### 4.5 右键「修改权限」对话框

**入口**：已有右键 `chmod` 菜单项，改为打开新组件 `FilePermissionDialog.vue`。

**对话框结构**

```
┌─ 修改权限 ─────────────────────────────┐
│ 文件：index.php                         │
│ 当前：www · 644  (rw-r--r--)            │
├─────────────────────────────────────────┤
│ 常用预设（按 type 过滤）                 │
│  [ 644 文件 ] [ 755 目录 ] [ 600 私密 ]   │
│  [ 775 协作目录 ] [ 自定义… ]            │
├─────────────────────────────────────────┤
│ 八进制权限  [ 644        ]  (验证 3–4 位) │
│ 属主      [ www        ]  (可选)         │
│ 属组      [ www        ]  (可选)         │
│ □ 同时修改子项（仅目录，二期）             │
├─────────────────────────────────────────┤
│ 提权：将使用 sudo 执行（SSH 用户 deploy）  │
│        [ 检测提权能力 ] 链接文档           │
├─────────────────────────────────────────┤
│           [ 取消 ]  [ 应用 ]             │
└─────────────────────────────────────────┘
```

**预设表（前端常量，后端可校验）**

| 标签 | mode | 适用 |
|------|------|------|
| 文件默认 | 644 | file |
| 目录默认 | 755 | directory |
| 仅本人 | 600 | file |
| 协作目录 | 775 | directory |
| 可执行 | 755 | file（脚本） |

**API：`POST /api/sync/chmod` 扩展**

```python
class FileChmod(BaseModel):
    server_id: int
    path: str = Field(..., min_length=1, max_length=4096)
    mode: str = Field(..., pattern=r"^[0-7]{3,4}$")
    owner: str | None = Field(None, max_length=64)   # 可选 chown
    group: str | None = Field(None, max_length=64)
    recursive: bool = False  # 二期：chmod -R
```

**远程命令（经 `exec_remote_command` + AUTO elevation）**

```bash
# 单文件
chmod 644 '/path/file' && chown www:www '/path/file'

# 仅 chmod
chmod 644 '/path/file'
```

顺序：先 `chmod` 再 `chown`（或反之，实现时固定一种并写测试）。

**响应**

```json
{
  "success": true,
  "path": "/www/.../index.php",
  "mode": "644",
  "owner": "www",
  "group": "www",
  "elevated": true
}
```

前端成功后 `browseCurrent()` 刷新列表。

### 4.6 服务器级配置（三写之一扩展）

在 **服务器编辑** 或 **settings** 增加（MySQL `servers` 表或 `settings` JSON）：

| 字段 | 类型 | 默认 | 说明 |
|------|------|------|------|
| `files_elevation` | enum | `auto_sudo` | `off` / `auto_sudo` / `always_sudo` |

不在安装向导强制，避免与 Agent sudoers 混淆；在服务器表单「高级 → 文件管理」折叠区配置。

---

## 5. 安全与性能

### 5.1 安全

- **最小权限**：`sudo -n` 仅允许白名单命令，禁止 `ALL=(ALL) NOPASSWD: ALL`；
- **禁止**在 API 中拼接未校验的 owner/group（仅 `[a-zA-Z0-9_-.]+`）；
- **路径**继续 `normalize_remote_abs_path`，禁止 `..`；
- **审计**：`file_chmod` / `file_chown` 合并为 `file_permission_change`，记录 mode、owner、group、elevated；
- **失败信息**不泄露 sudoers 路径内容。

### 5.2 性能

- browse 仍单次 `ls -la`，不增加 SSH 往返；
- sudo 回退最多 **2 倍** 命令耗时（仅失败路径），可接受；
- 批量 chmod（二期 `-R`）需限制路径深度或要求确认框。

---

## 6. 实现步骤（供技术文档拆分）

| 阶段 | 内容 | 预估 |
|------|------|------|
| **Phase 1** | `remote_shell.py` + 迁移 write/compress/chmod/file-ops | 1d |
| **Phase 2** | browse 返回 owner/group/mode_octal；前端归属列 | 0.5d |
| **Phase 3** | `FilePermissionDialog` + chmod API 扩展 chown | 1d |
| **Phase 4** | `files-capability` 检测 + 文档 + 设置项 `files_elevation` | 0.5d |
| **Phase 5** | 测试 + changelog + 审计 | 0.5d |

依赖顺序：Phase 1 → 2/3 可并行 → 4 → 5。

---

## 7. 验收标准

### L1 提权

- [ ] SSH 用户 `deploy` + 已配置 `nexus-files.sudoers`：编辑、保存、压缩、chmod 均成功，`elevated: true` 出现在审计（若执行了 sudo）。
- [ ] SSH 用户 `root`：不调用 sudo，功能正常。
- [ ] 无 sudo 且非 root：操作失败，提示含「配置免密 sudo」链接，不返回误导性「SSH 连接失败」。

### L2 展示

- [ ] 列表「归属」列显示 `www` / `root`，悬停可见完整权限串。
- [ ] 不再默认展示 `drwxr-xr-x` 宽列（可列设置打开）。

### L3 修改权限

- [ ] 右键单文件/目录 → 对话框预设 644/755 一键填充。
- [ ] 修改属主为 `www` 后，`ls` 刷新显示更新。
- [ ] 非法 mode/owner 前端校验 + 后端 400。

### L4 回归

- [ ] 文件树、编辑器、剪贴板粘贴、下载不受回归影响。

---

## 8. 风险与对策

| 风险 | 对策 |
|------|------|
| 用户以为「自动 root」= Nexus 破解权限 | UI 文案写清：**在服务器已授权 sudo 前提下自动提权** |
| sudo 白名单遗漏命令 | capability 检测 + 失败时记录完整 stderr |
| `ls` 解析在不同 OS 上列错位 | 优先 GNU coreutils；异常行跳过并记日志 |
| chown 误操作 | 对话框展示当前值；危险路径（/etc、/usr）二次确认（二期） |

---

## 9. 相关文档（实现后补齐）

- 技术实施：`docs/design/plans/2026-06-01-files-root-ownership-ui.md`（待写）
- Changelog：`docs/changelog/2026-06-01-files-root-ownership-ui.md`（待写）
- 运维：`docs/deploy/nexus-files-sudoers.example`

---

## 10. 决策记录

| 决策 | 选择 | 理由 |
|------|------|------|
| 自动 root 实现 | `sudo -n` 回退，非临时写 sudoers | 安全、可审计、与现有 partial 实现一致 |
| 列表权限 | 主显 **属主**，权限进 tooltip | 符合用户「www/root」认知 |
| chmod UI | 预设 + 八进制 + 可选 chown | 平衡能力与复杂度 |
| browse 是否 sudo | 否（默认） | 列目录极少需要 root；减少噪音 |

---

**请评审确认后进入 Phase 1 实现。** 若需「始终 sudo」或「强制 root SSH」，可在服务器级 `files_elevation` 中开启 `always_sudo`。
