Skip to content

[Bug] File Management: “The current user does not have permission.” #13912

Description

@lalasou

Contact Information

No response

1Panel Version

2.3.1

Problem Description

1Panel v2.3.1 文件管理"提示当前用户无权限"排查报告

排查时间:2026-09-23 / 复核 2026-09-24 00:00
环境:Armbian 25.11(aarch64, kernel 6.12.85-ophub)· 1Panel v2.3.1(已是最新发布版,2026-09-18 由 v2.2.5 升级而来)· SystemStatus=Free(社区版)· 面板直连 http://192.168.10.3:1990(SSL 关闭、无前置反代)· core/agent 均以 root 运行

结论:不是权限问题,改账号名也没用。
真正失败的是 读取文件内容 /files/content:文件大小在 (2KB, 5MB) 区间时,agent 返回 gzip 压缩响应,而 core 代理请求时把浏览器的 Accept-Encoding 原样转发,导致 Go 的 http.Transport 关闭了自动解压 → core 拿 gzip 二进制当 JSON 解析失败 → 前端把"没有 message 的错误"兜底显示成 "当前用户无权限"。


一、现象

在文件管理中打开/编辑文本文件,面板提示"当前用户无权限"。

关键修正(本次复核新增): 操作日志显示——

接口 含义 Success Failed
/files/content 读取/打开文件内容 137 120
/files/save 保存文件内容 131 0

也就是说:保存接口从来没有失败过。真正被拦的是"打开文件"这一步。用户感知到的"修改后保存无权限",是打开时读取失败弹出的同一句兜底文案。

二、结论先行(两个被证伪的假设)

1. 改面板账号名为 admin —— 无效

社区版服务端把角色写死为常量,与用户名无关(core/app/auth/auth.go → GetCurrentUserInfo()):

info.Name = settingMap["UserName"]   // 用户名(本机为 x)
info.Role = "ADMIN"                  // 硬编码常量,与 Name 无关
info.Permissions = []string{}        // 免费版恒为空
info.NodeRoles = []dto.CurrentUserNodeRole{}  // 免费版恒为空

前端 isAdmin 判定为 data.role === 'ADMIN',因此登录后 isAdmin 恒为 true,保存守卫 hasManagePermissionAccess(undefined, {nodeAdmin:true}) 必然放行。改名只改登录名,不改 role,也不影响本次故障。

2. "当前用户无权限"是误导性兜底文案

部署版前端里该文案只有 2 个来源,其中一个是"无 message 错误"的默认值:

mh = () => t('commons.res.forbidden')        // 默认文案 = "当前用户无权限"
_h = (e, t = {}) => {
  let n = e?.message || mh();                // 后端错误没带 message → 直接显示这句话
  switch (e?.code) { case ERR_RBAC: ...; ... }
}

即:任何没有携带错误信息的后端报错,都会被显示成"当前用户无权限"。这次的 gzip 解析错误就是"没有 message"的那种。

三、真正根因(已实测 100% 复现)

链路

浏览器 ──(Accept-Encoding: gzip, deflate, br, zstd)──> 1panel-core:1990
   │
   ├─ core 转发请求到本地 agent(unix socket /etc/1panel/agent.sock)
   │   core/utils/req_helper/proxy_local/req_to_local.go · RequestWithContext:
   │       if ctx != nil {
   │           for key, values := range ctx.Request.Header { req.Header.Add(key, value) }
   │       }
   │   ↑ 把浏览器的 Accept-Encoding 原样转发 → Go 的 http.Transport 认为"调用方自己会处理",
   │     于是不再自动解压响应体
   │
   ├─ agent 端 agent/app/api/v2/file.go · GetContent:
   │       if info.Size > 2*1024 && info.Size < 5*1024*1024 {
   │           helper.SuccessWithDataGzipped(c, info)   // 2KB~5MB 的文件 → gzip 响应
   │       } else { helper.SuccessWithData(c, info) }
   │
   │   而 agent 端 gzip 的触发条件是"客户端声明支持 gzip"
   │   (agent/app/api/v2/helper/helper.go):
   │       func SuccessWithDataGzipped(ctx *gin.Context, data interface{}) {
   │           if !strings.Contains(ctx.GetHeader("Accept-Encoding"), "gzip") {
   │               SuccessWithData(ctx, data); return      // 不支持就直接发明文
   │           }
   │           ...
   │           ctx.Header("Content-Encoding", "gzip")
   │       }
   │
   └─ core 拿到响应后:
           bodyByte, _ := io.ReadAll(resp.Body)          // ← 读到的仍是 gzip 字节
           var respJson dto.Response
           if err := json.Unmarshal(bodyByte, &respJson); err != nil {
               return nil, fmt.Errorf("json umarshal resp data failed, err: %v", err)  // ← 必然失败
           }

触发条件 = 文件大小 > 2048 字节 且 请求头带 Accept-Encoding: gzip。 浏览器一定会带,所以对用户来说就是:文件一旦超过 2KB,面板里就再也打不开了(这也解释了"以前不这样"——以前编辑的都是小文件)。

实测证据(直连 agent:curl --unix-socket /etc/1panel/agent.sock)

测试文件 请求头 响应 Content-Encoding 响应体开头
1024 B 带 Accept-Encoding: gzip 无 7b 22({",正常 JSON)
2151 B 带 Accept-Encoding: gzip gzip 1f 8b 08(gzip 魔数)
2151 B 不带 Accept-Encoding 无 7b 22(正常 JSON)
3072 B 带 Accept-Encoding: gzip gzip 1f 8b
6291456 B 带 Accept-Encoding: gzip 无 7b 22(>5MB 走明文分支)

下例为 2151 B 文件的响应头实测(可见 Content-Encoding: gzip、Content-Length: 1351 为压缩后长度):

HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Type: application/json; charset=utf-8
Content-Length: 1351

现场文件与操作日志(core.db.operation_logs)完全对应:

文件 大小 结果
package.json 522 B ✅ Success
VinMa.js 1,821 B ✅ Success
Data.js 2,151 B ❌ Failed
Adapter.js 10,072 B ❌ Failed
gerAdapter.js 13,026 B ❌ Failed(今天失败 3 次)

分界线精确落在 2048 字节:137 次成功全部 < 2KB,120 次失败全部 > 2KB。

附加影响:目录树接口同样中招

agent/app/api/v2/file.go 的 GetFileTree 无条件调用 helper.SuccessWithDataGzipped(c, tree)(没有大小判断)。所以当某个目录的文件树 JSON 变大时,文件列表/目录树也会刷新失败。

不是 v2.3.1 引入的回归

  • req_to_local.go 与 file.go 这两段逻辑在 v2.2.5 中完全相同。
  • 历史日志显示早期就有同样失败:2026-09-10 /etc/mihomo/config.yaml(2,162 B)→ Failed;更早 2025-12-24、2026-02、2026-03 均有 Failed 记录,且同一天 Success/Failed 混存(= 文件大小差异)。
  • 属于长期存在的缺陷,只是最近才开始编辑 >2KB 的文件,才被暴露出来。

四、修复建议(给 1Panel)

在 core/utils/req_helper/proxy_local/req_to_local.go 的 RequestWithContext 中转发请求头时排除 Accept-Encoding,让 Go 的 http.Transport 自行协商并透明解压:

if ctx != nil {
    for key, values := range ctx.Request.Header {
        if strings.EqualFold(key, "Accept-Encoding") {
            continue        // 交给 Transport 自行处理 gzip,保证响应可被 JSON 解析
        }
        for _, value := range values {
            req.Header.Add(key, value)
        }
    }
}

或者更稳妥:读取响应体前判断 resp.Header.Get("Content-Encoding") == "gzip" 并手动解压(同时清掉该响应头)。

五、用户侧可用的方案

  1. 【立即可用·零风险】大文件改用其他方式编辑:面板内置终端(vi/nano)、SSH、或 SMB 共享(本机 445 端口 smbd 在跑)挂到 Windows 上用 VS Code 编辑。面板编辑器对 <2KB 文件是正常的。
  2. 【一劳永逸·不改面板源码】在面板前加一层反向代理,并清除 Accept-Encoding:
    本机已安装 Lucky(/mnt/sda1/apps/lucky),或可用面板自带的 OpenResty 建站点,反代到 127.0.0.1:1990 并删除请求头 Accept-Encoding。原理:core 侧不再收到该头 → Go Transport 自己加 Accept-Encoding: gzip 并自动解压 → json.Unmarshal 成功。
    location / {
        proxy_pass http://127.0.0.1:1990;
        proxy_set_header Accept-Encoding "";   # 关键
        proxy_set_header Host $host;
        proxy_http_version 1.1;
    }
    ⚠️ 必须通过这个反代入口访问面板才有效;继续直连 1990 依旧会坏。
  3. 向官方提 issue(v2.3.1 已是最新版,dev 分支已重构为 backend/,暂时没有"升级即可修复"的版本)。现成英文 issue 文本见同目录 1Panel-issue-EN.md。
  4. 自编译补丁:改动仅 5 行(见第四节)。若日后愿意,也可以只替换 core 二进制。

Steps to Reproduce

1Panel v2.3.1 文件管理"提示当前用户无权限"排查报告

排查时间:2026-09-23 / 复核 2026-09-24 00:00
环境:Armbian 25.11(aarch64, kernel 6.12.85-ophub)· 1Panel v2.3.1(已是最新发布版,2026-09-18 由 v2.2.5 升级而来)· SystemStatus=Free(社区版)· 面板直连 http://192.168.10.3:1990(SSL 关闭、无前置反代)· core/agent 均以 root 运行

结论:不是权限问题,改账号名也没用。
真正失败的是 读取文件内容 /files/content:文件大小在 (2KB, 5MB) 区间时,agent 返回 gzip 压缩响应,而 core 代理请求时把浏览器的 Accept-Encoding 原样转发,导致 Go 的 http.Transport 关闭了自动解压 → core 拿 gzip 二进制当 JSON 解析失败 → 前端把"没有 message 的错误"兜底显示成 "当前用户无权限"。


一、现象

在文件管理中打开/编辑文本文件,面板提示"当前用户无权限"。

关键修正(本次复核新增): 操作日志显示——

接口 含义 Success Failed
/files/content 读取/打开文件内容 137 120
/files/save 保存文件内容 131 0

也就是说:保存接口从来没有失败过。真正被拦的是"打开文件"这一步。用户感知到的"修改后保存无权限",是打开时读取失败弹出的同一句兜底文案。

二、结论先行(两个被证伪的假设)

1. 改面板账号名为 admin —— 无效

社区版服务端把角色写死为常量,与用户名无关(core/app/auth/auth.go → GetCurrentUserInfo()):

info.Name = settingMap["UserName"]   // 用户名(本机为 x)
info.Role = "ADMIN"                  // 硬编码常量,与 Name 无关
info.Permissions = []string{}        // 免费版恒为空
info.NodeRoles = []dto.CurrentUserNodeRole{}  // 免费版恒为空

前端 isAdmin 判定为 data.role === 'ADMIN',因此登录后 isAdmin 恒为 true,保存守卫 hasManagePermissionAccess(undefined, {nodeAdmin:true}) 必然放行。改名只改登录名,不改 role,也不影响本次故障。

2. "当前用户无权限"是误导性兜底文案

部署版前端里该文案只有 2 个来源,其中一个是"无 message 错误"的默认值:

mh = () => t('commons.res.forbidden')        // 默认文案 = "当前用户无权限"
_h = (e, t = {}) => {
  let n = e?.message || mh();                // 后端错误没带 message → 直接显示这句话
  switch (e?.code) { case ERR_RBAC: ...; ... }
}

即:任何没有携带错误信息的后端报错,都会被显示成"当前用户无权限"。这次的 gzip 解析错误就是"没有 message"的那种。

三、真正根因(已实测 100% 复现)

链路

浏览器 ──(Accept-Encoding: gzip, deflate, br, zstd)──> 1panel-core:1990
   │
   ├─ core 转发请求到本地 agent(unix socket /etc/1panel/agent.sock)
   │   core/utils/req_helper/proxy_local/req_to_local.go · RequestWithContext:
   │       if ctx != nil {
   │           for key, values := range ctx.Request.Header { req.Header.Add(key, value) }
   │       }
   │   ↑ 把浏览器的 Accept-Encoding 原样转发 → Go 的 http.Transport 认为"调用方自己会处理",
   │     于是不再自动解压响应体
   │
   ├─ agent 端 agent/app/api/v2/file.go · GetContent:
   │       if info.Size > 2*1024 && info.Size < 5*1024*1024 {
   │           helper.SuccessWithDataGzipped(c, info)   // 2KB~5MB 的文件 → gzip 响应
   │       } else { helper.SuccessWithData(c, info) }
   │
   │   而 agent 端 gzip 的触发条件是"客户端声明支持 gzip"
   │   (agent/app/api/v2/helper/helper.go):
   │       func SuccessWithDataGzipped(ctx *gin.Context, data interface{}) {
   │           if !strings.Contains(ctx.GetHeader("Accept-Encoding"), "gzip") {
   │               SuccessWithData(ctx, data); return      // 不支持就直接发明文
   │           }
   │           ...
   │           ctx.Header("Content-Encoding", "gzip")
   │       }
   │
   └─ core 拿到响应后:
           bodyByte, _ := io.ReadAll(resp.Body)          // ← 读到的仍是 gzip 字节
           var respJson dto.Response
           if err := json.Unmarshal(bodyByte, &respJson); err != nil {
               return nil, fmt.Errorf("json umarshal resp data failed, err: %v", err)  // ← 必然失败
           }

触发条件 = 文件大小 > 2048 字节 且 请求头带 Accept-Encoding: gzip。 浏览器一定会带,所以对用户来说就是:文件一旦超过 2KB,面板里就再也打不开了(这也解释了"以前不这样"——以前编辑的都是小文件)。

实测证据(直连 agent:curl --unix-socket /etc/1panel/agent.sock)

测试文件 请求头 响应 Content-Encoding 响应体开头
1024 B 带 Accept-Encoding: gzip 无 7b 22({",正常 JSON)
2151 B 带 Accept-Encoding: gzip gzip 1f 8b 08(gzip 魔数)
2151 B 不带 Accept-Encoding 无 7b 22(正常 JSON)
3072 B 带 Accept-Encoding: gzip gzip 1f 8b
6291456 B 带 Accept-Encoding: gzip 无 7b 22(>5MB 走明文分支)

下例为 2151 B 文件的响应头实测(可见 Content-Encoding: gzip、Content-Length: 1351 为压缩后长度):

HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Type: application/json; charset=utf-8
Content-Length: 1351

现场文件与操作日志(core.db.operation_logs)完全对应:

文件 大小 结果
package.json 522 B ✅ Success
VinMa.js 1,821 B ✅ Success
Data.js 2,151 B ❌ Failed
Adapter.js 10,072 B ❌ Failed
gerAdapter.js 13,026 B ❌ Failed(今天失败 3 次)

分界线精确落在 2048 字节:137 次成功全部 < 2KB,120 次失败全部 > 2KB。

附加影响:目录树接口同样中招

agent/app/api/v2/file.go 的 GetFileTree 无条件调用 helper.SuccessWithDataGzipped(c, tree)(没有大小判断)。所以当某个目录的文件树 JSON 变大时,文件列表/目录树也会刷新失败。

不是 v2.3.1 引入的回归

  • req_to_local.go 与 file.go 这两段逻辑在 v2.2.5 中完全相同。
  • 历史日志显示早期就有同样失败:2026-09-10 /etc/mihomo/config.yaml(2,162 B)→ Failed;更早 2025-12-24、2026-02、2026-03 均有 Failed 记录,且同一天 Success/Failed 混存(= 文件大小差异)。
  • 属于长期存在的缺陷,只是最近才开始编辑 >2KB 的文件,才被暴露出来。

四、修复建议(给 1Panel)

在 core/utils/req_helper/proxy_local/req_to_local.go 的 RequestWithContext 中转发请求头时排除 Accept-Encoding,让 Go 的 http.Transport 自行协商并透明解压:

if ctx != nil {
    for key, values := range ctx.Request.Header {
        if strings.EqualFold(key, "Accept-Encoding") {
            continue        // 交给 Transport 自行处理 gzip,保证响应可被 JSON 解析
        }
        for _, value := range values {
            req.Header.Add(key, value)
        }
    }
}

或者更稳妥:读取响应体前判断 resp.Header.Get("Content-Encoding") == "gzip" 并手动解压(同时清掉该响应头)。

五、用户侧可用的方案

  1. 【立即可用·零风险】大文件改用其他方式编辑:面板内置终端(vi/nano)、SSH、或 SMB 共享(本机 445 端口 smbd 在跑)挂到 Windows 上用 VS Code 编辑。面板编辑器对 <2KB 文件是正常的。
  2. 【一劳永逸·不改面板源码】在面板前加一层反向代理,并清除 Accept-Encoding:
    本机已安装 Lucky(/mnt/sda1/apps/lucky),或可用面板自带的 OpenResty 建站点,反代到 127.0.0.1:1990 并删除请求头 Accept-Encoding。原理:core 侧不再收到该头 → Go Transport 自己加 Accept-Encoding: gzip 并自动解压 → json.Unmarshal 成功。
    location / {
        proxy_pass http://127.0.0.1:1990;
        proxy_set_header Accept-Encoding "";   # 关键
        proxy_set_header Host $host;
        proxy_http_version 1.1;
    }
    ⚠️ 必须通过这个反代入口访问面板才有效;继续直连 1990 依旧会坏。
  3. 向官方提 issue(v2.3.1 已是最新版,dev 分支已重构为 backend/,暂时没有"升级即可修复"的版本)。现成英文 issue 文本见同目录 1Panel-issue-EN.md。
  4. 自编译补丁:改动仅 5 行(见第四节)。若日后愿意,也可以只替换 core 二进制。

The expected correct result

No response

Related log output

Additional Information

No response

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions