282 lines
13 KiB
Markdown
282 lines
13 KiB
Markdown
# AList API 集成说明(源码级验证)
|
||
|
||
> 规格文档 §11/§47 要求:不猜 API,不发明 endpoint。本文所有端点均已从
|
||
> AList 上游源码(`AlistGo/alist`,`v3.63.0`)验证,而非仅凭文档目录猜测。
|
||
|
||
## 1. 版本
|
||
|
||
- 目标版本:`xhofe/alist:v3.63.0`(2026-08 最新稳定 release)
|
||
- Role-scoped path aggregation + Check path limit 自 v3.53.0 起支持(规格 §48 要求已满足)。
|
||
|
||
## 2. 认证
|
||
|
||
```
|
||
POST /api/auth/login
|
||
Body: { "username": "admin", "password": "***" }
|
||
响应: { "code": 200, "data": { "token": "<JWT>" } }
|
||
```
|
||
|
||
后续所有 Admin 请求带 header:
|
||
|
||
```
|
||
Authorization: <token>
|
||
```
|
||
|
||
## 3. 已验证的 Admin API
|
||
|
||
源码位置:`server/router.go`(`admin(g *gin.RouterGroup)`)、`server/handles/user.go`、`server/handles/role.go`。
|
||
|
||
### 3.1 User API
|
||
|
||
| 端点 | 方法 | 说明 |
|
||
|---|---|---|
|
||
| `/api/admin/user/list` | GET | 分页列出用户(`?page=&per_page=`) |
|
||
| `/api/admin/user/get` | GET | `?id=` 取单个用户 |
|
||
| `/api/admin/user/create` | POST | 建用户 |
|
||
| `/api/admin/user/update` | POST | 改用户 |
|
||
| `/api/admin/user/delete` | POST | `?id=` 删用户 |
|
||
| `/api/admin/user/cancel_2fa` | POST | 撤销某用户 2FA |
|
||
|
||
### 3.2 Role API(关键 —— 完整 CRUD 存在)
|
||
|
||
| 端点 | 方法 | 说明 |
|
||
|---|---|---|
|
||
| `/api/admin/role/list` | GET | 分页列出 role |
|
||
| `/api/admin/role/get` | GET | `?id=` 取单个 role |
|
||
| `/api/admin/role/create` | POST | 建 role |
|
||
| `/api/admin/role/update` | POST | 改 role(含 `permission_scopes`) |
|
||
| `/api/admin/role/delete` | POST | `?id=` 删 role |
|
||
|
||
> ✅ 结论:Role scope 的增删改**可通过官方 API 完整实现**,无需 fork、无需
|
||
> integration service(规格 §12 的 Option C 不需要走)。
|
||
|
||
## 4. 数据结构(源码 `internal/model/`)
|
||
|
||
### 4.1 Role
|
||
|
||
```go
|
||
type Role struct {
|
||
ID uint `json:"id"`
|
||
Name string `json:"name"` // 唯一
|
||
Description string `json:"description"`
|
||
Default bool `json:"default"`
|
||
PermissionScopes []PermissionEntry `json:"permission_scopes"`
|
||
}
|
||
|
||
type PermissionEntry struct {
|
||
Path string `json:"path"` // 路径前缀,如 "/Class/A/1"
|
||
Permission int32 `json:"permission"` // bitmask
|
||
}
|
||
```
|
||
|
||
JSON 示例:
|
||
|
||
```json
|
||
{
|
||
"id": 5,
|
||
"name": "customer-alice",
|
||
"description": "Alice customer role",
|
||
"default": false,
|
||
"permission_scopes": [
|
||
{ "path": "/Class/A/1", "permission": 0 },
|
||
{ "path": "/Class/A/2", "permission": 0 }
|
||
]
|
||
}
|
||
```
|
||
|
||
### 4.2 User
|
||
|
||
```go
|
||
type User struct {
|
||
ID uint `json:"id"`
|
||
Username string `json:"username"`
|
||
Password string `json:"password"` // create/update 时传入明文
|
||
BasePath string `json:"base_path"`
|
||
Role Roles `json:"role"` // role-ID 数组
|
||
Permission int32 `json:"permission"` // bitmask
|
||
Disabled bool `json:"disabled"`
|
||
}
|
||
```
|
||
|
||
JSON 示例(只读客户):
|
||
|
||
```json
|
||
{
|
||
"id": 10,
|
||
"username": "alice",
|
||
"password": "***",
|
||
"base_path": "/Class",
|
||
"role": [5],
|
||
"permission": 16384,
|
||
"disabled": false
|
||
}
|
||
```
|
||
|
||
## 5. Permission bitmask(源码 `internal/model/user.go`)
|
||
|
||
| bit | 含义 | 对应 |
|
||
|---|---|---|
|
||
| 0 | 查看隐藏文件 | 1 |
|
||
| 1 | 免密访问 | 2 |
|
||
| 2 | 离线下载任务 | 4 |
|
||
| 3 | mkdir / upload | 8 |
|
||
| 4 | rename | 16 |
|
||
| 5 | move | 32 |
|
||
| 6 | copy | 64 |
|
||
| 7 | remove / delete | 128 |
|
||
| 8 | WebDAV read | 256 |
|
||
| 9 | WebDAV write | 512 |
|
||
| 10 | FTP login/read | 1024 |
|
||
| 11 | FTP write | 2048 |
|
||
| 12 | 读压缩包 | 4096 |
|
||
| 13 | 解压 | 8192 |
|
||
| **14** | **Check path limit** | **16384** |
|
||
| 15 | MCP read | 32768 |
|
||
| 16 | MCP write | 65536 |
|
||
|
||
**本项目默认:**
|
||
- `read-only` = bitmask **`0`**(不设 bit 3–7/9/11/13 等任何写权限)
|
||
- `Check path limit ON` = 设 bit 14 → **`16384`**
|
||
|
||
即客户 user 的 `permission` 字段 = `16384`(只读 + 路径限制),所有写位为 0。
|
||
|
||
## 6. Role scope + Check path limit 的权限判定逻辑
|
||
|
||
源码 `JoinPath()`(`internal/model/user.go`)证实:
|
||
|
||
1. 用户 base path 只是登录后的默认落点,**不是 ACL**(规格 §2/§30)。
|
||
2. 若用户 `CheckPathLimit()` 为真(bit 14 置位),请求路径必须落在该用户**任一 role
|
||
的任一 permission scope 的 path 之内**(前缀匹配),否则返回 `PermissionDenied`。
|
||
3. 因此「Alice 只能进 /Class/A/1 与 /Class/A/2,进不了 /Class/A/3 与 /Class/B」
|
||
的正确实现 = Alice 专属 role 的 `permission_scopes` 只含这两个 path,且 user
|
||
`permission` bit 14 置位。
|
||
|
||
## 7. 保护性约束(源码确认)
|
||
|
||
- `admin` / `guest` role 受保护:`update`(改 admin 名/默认属性)与 `delete` 会被
|
||
`ErrChangeDefaultRole` 拒绝。客户 role 不受此限。
|
||
- `CreateUser` 时若 `role` 空数组,会回填默认 role ID(`op.GetDefaultRoleID()`)。
|
||
**因此建客户时必须显式传其专属 role ID,不能留空。**
|
||
- `UpdateUser` 若试图把用户 role 改成 admin/guest role,会被拒绝。
|
||
- 至少保留 1 个 enabled admin,否则禁用最后一个 admin 会失败。
|
||
|
||
## 8. 给 n8n 的实现要点(幂等,规格 §10/§28)
|
||
|
||
- 建用户:先 `GET /api/admin/user/list` 按 `username` 查重,存在则 `update`,否则 `create`。
|
||
- 建 role:先 `GET /api/admin/role/list` 按 `name` 查重,存在则 `update`(重写 `permission_scopes`),否则 `create`。
|
||
- 更新 scope:`POST /api/admin/role/update`,整体重写该 role 的 `permission_scopes`
|
||
(desired state = NocoDB 当前有效 grants 的并集),天然幂等。
|
||
- 到期撤销:从 desired scopes 里移除过期 path,再整体 `update` 该 role。
|
||
- 禁用客户:`POST /api/admin/user/update` 设 `disabled: true`。
|
||
- 删除客户:soft delete(NocoDB 侧 `status=deleted`),AList 侧禁用用户 + 清空 role scopes,不物理删除(保留审计)。
|
||
|
||
## 9. Phase 3 实测验证(2026-08-22,对真实 v3.63.0 实例)
|
||
|
||
> 以下为对 DSM 上运行中的 `alist-serve-resource`(v3.63.0,端口 5244)实际 curl 验证结果,
|
||
> 修正/确认了上文的若干推断。
|
||
|
||
### 9.1 认证 —— 登录带 2FA 时的字段
|
||
|
||
源码 `server/handles/auth.go` 的 `LoginReq` 结构:
|
||
|
||
```go
|
||
type LoginReq struct {
|
||
Username string `json:"username" binding:"required"`
|
||
Password string `json:"password"`
|
||
OtpCode string `json:"otp_code"` // 2FA 开启时必填(TOTP 6 位)
|
||
}
|
||
```
|
||
|
||
- 账户开启 2FA(`otp_secret` 非空)时,单次 `POST /api/auth/login` 不带 `otp_code`
|
||
直接返回 `{"code":402,"message":"Invalid 2FA code"}`。**是一步登录**,不是两步 challenge。
|
||
- 登录成功返回:`{"code":200,"data":{"token":"<JWT>","device_key":"<md5>"}}`。
|
||
- `GET /api/admin/user/list` 返回的员工 `password` 字段恒为 `""`(脱敏),`otp` 状态不在此端点,
|
||
需 `GET /api/me` 才见 `otp: true/false`。
|
||
|
||
### 9.2 Role CRUD 实测返回值(修正 §3)
|
||
|
||
| 端点 | 实测返回 | 备注 |
|
||
|---|---|---|
|
||
| `role/list` | `{"code":200,"data":{"content":[...],"total":N}}` | `PageResp` 包裹,非顶层数组 |
|
||
| `role/create` | `{"code":200,"data":null}` | **不返回新建 role 对象**;需再 `role/list` 按 `name` 取 id |
|
||
| `role/update` | `{"code":200,"data":null}` | 成功即 null;`role/get?id=` 可回读确认 |
|
||
| `role/get?id=` | `{"code":200,"data":{...}}` | 返回完整 role 含 `permission_scopes` |
|
||
| `role/delete` | **`POST /api/admin/role/delete?id=4`(id 走 query param)** | 用 JSON body 传 id 会 `400 strconv.Atoi: parsing "": invalid syntax` |
|
||
|
||
- `role/delete` 与 `user/delete` **都必须 `?id=` query param**,不是 body。
|
||
- `user/create`、`user/delete` 同样实测成功(返回 `data:null`)。
|
||
|
||
### 9.3 admin 账户(当前实例)
|
||
|
||
- 用户名 `hoelee`,角色 `admin`(role id 2,`permission 65535`,`permission_scopes` = `[{"/", 65535}]`)。
|
||
- `guest` role(id 1)`disabled`,`permission 0`。
|
||
- admin 登录曾开 2FA(已由用户关闭);本系统若需 n8n 无人值守,**建议 admin 账户保持 2FA 关闭
|
||
或用独立 service token**——否则 n8n 每次登录都要算 TOTP。
|
||
|
||
### 9.4 网络可达性(关键部署事实)
|
||
|
||
- AList 容器名 `alist-serve-resource` 仅 Docker **网络内部**可解析。
|
||
**DSM host shell 无法解析该名字**(`Could not resolve host`),须用 `localhost:5244`。
|
||
- n8n 容器在 `bridge_hoelee` 网络内,访问 AList 用 **`http://alist-serve-resource:5244`**(✅)。
|
||
- 旧 `alist` 容器(v3.54.0)现在映射 host 端口 5243,新容器占 5244,无冲突。
|
||
|
||
### 9.5 ⚠ JWT secret 不一致(待修复,否则 token 会失效)
|
||
|
||
- 容器 env `JWT_SECRET=<redacted>=`(.env 传入),但 AList 初始化时写入 `data/config.json`
|
||
的 `jwt_secret` 是另一个值 `<redacted>`(首次启动自动生成)。
|
||
- **env `JWT_SECRET` 只影响首次初始化**;服务运行后以 `config.json` 为准(当前生效的是
|
||
`<redacted>`)。两者不一致是正常的(env 在初次 init 时可能被 config 覆盖)。
|
||
- 影响:若将来重建数据目录(清空 `data/`),.env 里的 `JWT_SECRET` 会生效;否则一直用 config.json 上的值。
|
||
**n8n 不关心这个**(每次 `login` 换 token),但需知悉「改 env 不会改已运行实例的 JWT」。
|
||
- 若要统一:改 `data/config.json` 的 `jwt_secret` 并重启,或接受 config.json 为准。
|
||
|
||
### 9.6 n8n 集成结论(Phase 3 通过 ✅)
|
||
|
||
- admin 登录 token 可用,Role/User 全 CRUD 可用,scope 读写可用。
|
||
- n8n workflow 实现要点(§8)全部成立,唯一补充:**delete 走 `?id=`**,**create/update 返回 null
|
||
需回查**(用 `list` 按 `name` 定位 id)。
|
||
|
||
### 9.7 ⚠ `user/create` 空 role 会失败(Phase 4/W1 debug 实测,源码级确认)
|
||
|
||
- **症状**:`POST /api/admin/user/create` 传 `"role": []` 时返回
|
||
`{"code":400,"message":"admin or guest user can not be created"}`,且与 username 无关(任意名字都失败)。
|
||
- **根因**(`server/handles/user.go` `CreateUser`):
|
||
1. `if len(req.Role) == 0 { req.Role = model.Roles{op.GetDefaultRoleID()} }` —— 空 role 被自动填为
|
||
**默认 role(guest,id=1)**。
|
||
2. 紧接着 `if req.IsAdmin() || req.IsGuest() { ... 400 ... }`,其中
|
||
`IsGuest()` = `Role.Contains(GUEST)`(`GUEST=1`)、`IsAdmin()` = `Role.Contains(ADMIN)`(`ADMIN=2`)。
|
||
role 现在是 `[1]`(guest)→ 判定为 guest → 拒绝。
|
||
- **关键认知**:`IsGuest/IsAdmin` 判断的是 **role ID**,不是 username。错误信息里的 "admin or guest
|
||
user" 会误导向「用户名保留了」,实际是「role 里含 guest/admin ID」。
|
||
- **正确顺序(必须)**:
|
||
1. `role/create`(name=`customer-<username>`,空 `permission_scopes`)→ 返回 200 但 `data:null`;
|
||
2. `role/list` 按 name 找到新建 role 的自增 id(3、4、5…);
|
||
3. `user/create` 传 `"role": [<该 role id>]`(**非空、非 1/2**)→ 成功,`base_path` 正确。
|
||
- 实测:`role:[5]` 创建用户 `code 200`,`base_path:/Class`、`permission 16384` 均落地。
|
||
- **适用**:W1 的「新建用户」路径必须先建 role;更新路径 `user/update` 里 `role:[]` 同样会触发
|
||
`GetDefaultRoleID` 回填(见 `UpdateUser` 同款逻辑),若客户已有 role id 应显式传入。
|
||
- 相关常量:`GENERAL=0, GUEST=1, ADMIN=2, NEWGENERAL=3`(`internal/model/user.go`);`permission`
|
||
bitmask 定义同文件注释(bit 14 = "check path limit" = 16384)。
|
||
|
||
### 9.8 ✅ 正确结论:role `permission_scopes` + `base_path="/"` = 多文件夹授权(已实测确认)
|
||
|
||
**最终确认的多文件夹授权正确做法**(2026-08-22 实测 mama 账号验证通过):
|
||
|
||
- **组合**:`base_path = "/"`(根)+ role `permission_scopes` 用**绝对路径**(`/Class/Forex`、`/Class/Health`)。
|
||
- **实测结果**(mama, base_path=`/`, role scopes=`[/Class/Forex, /Class/Health]`):
|
||
- `fs list /` → 只显示被授权的挂载点(`Class`),**不泄露** `/Book`/`/Comic` 等未授权挂载点 ✅
|
||
- `fs list /Class/Forex` → 200 ✅、`fs list /Class/Health` → 200 ✅(**第二个 scope 也生效**)
|
||
- `fs list /Class/Other` → 403 ✅、`fs list /Book` → 403 ✅(未授权正确拒绝)
|
||
- **关键坑(之前失败的原因)**:`base_path` 若设成 `/Class`(而非 `/`),scope 绝对路径 `/Class/Forex`
|
||
会被 `JoinPath` 拼成 `/Class/Class/Forex` → 全部 403。**必须 `/`**。
|
||
- 注意:`CreateUser` 里有 `u.BasePath = scopes[0].Path` 的覆盖逻辑,但仅当 role **有 scope** 时触发;
|
||
若创建时先建空 role(无 scope),base_path 不会被覆盖。W1 顺序:先建空 role → 建 user
|
||
(base_path=`/`)→ W2 再填 scope。实测当 base_path 被显式设为 `/` 且 role 后续填 scope 时,
|
||
重新 login/访问正常。
|
||
- **不需要 Alias storage**(§9.9 曾是备选,现已确认 role scope 路线够用且更简单)。
|
||
|
||
### 9.9(备选,未采用)Alias storage —— 一用户看多分散文件夹的另一种方式
|
||
|
||
(保留作为参考:`drivers/alias/driver.go`,`paths` 换行 `别名:目标`,`writable:false` 只读。
|
||
但既然 role scope + base_path=`/` 已能满足,除非有「合并路径语义/去重/隔离」特殊需求,否则不必上 Alias。)
|