Files
Nexus/docs/design/specs/2026-06-01-files-root-ownership-ui-design.md
T

404 lines
15 KiB
Markdown
Raw Normal View History

# 文件管理:自动提权、归属显示、权限修改 — 设计文档
| 项 | 内容 |
|----|------|
| 日期 | 2026-06-01 |
| 状态 | 已实现 Phase 15 + 二期递归 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` 约 270302 行)。
---
## 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 ] (验证 34 位) │
│ 属主 [ 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`