项目页面因名称查找失败显示“Forbidden”

问题描述

某些 Harbor 项目在访问仓库或其他项目详情时,Web UI 会显示 forbidden 错误。API 返回 403:

{"errors":[{"code":"FORBIDDEN","message":"forbidden"}]}

其他项目可以正常工作。该问题可能在 Harbor 升级后出现。

根本原因

数据库 project 表中存储的项目名称已损坏(例如存在不可见字符或编码问题)。当 Harbor 通过 ORM 按名称查询项目时:

SELECT * FROM project WHERE name = '<project_name>' AND deleted = false;

由于相等性检查失败,查询不会返回任何行,尽管该行实际存在,并且可以通过 project_id 找到。由于项目查找失败,权限检查返回 false,API 因此返回 403 Forbidden,而不是实际数据。

相关上游问题:https://github.com/goharbor/harbor/issues/15620

故障排查

步骤 1:检查 Harbor Core 日志

在 harbor-core 日志中查找以下错误模式:

[ERROR] [/server/v2.0/handler/base.go:87]: failed to get project <project_name>: project <project_name> not found

完整的日志链通常如下所示:

[DEBUG] get project <project_name> from cache error: key not found:redis: nil, will query from database.
[ERROR] [/server/v2.0/handler/base.go:87]: failed to get project <project_name>: project <project_name> not found
[DEBUG] {"errors":[{"code":"FORBIDDEN","message":"forbidden"}]}

步骤 2:在数据库中验证

连接到 Harbor 数据库(registry):

kubectl exec -it <harbor-database-pod> -n <namespace> -- psql -d registry

执行以下查询:

-- 按 project_id 查询 — 应返回该行
SELECT project_id, name, deleted FROM project WHERE project_id = <id>;

-- 按名称查询 — 如果存在该问题,则返回 0 行
SELECT project_id, name, deleted FROM project WHERE name = '<project_name>';

如果第一条查询返回了项目,但第二条返回 0 行,则可以确认该问题。

解决方案

在 Harbor 数据库(registry)中,使用 project_id 重写 name 字段:

-- 修复损坏的名称
UPDATE project SET name = '<project_name>' WHERE project_id = <id>;

-- 重建索引以确保一致性(请参见下方风险说明)
REINDEX TABLE project;

验证修复结果:

SELECT project_id FROM project WHERE name = '<project_name>' AND deleted = false;
-- 应返回 1 行

刷新 Harbor Web UI,确认 403 错误已解决。

REINDEX 风险说明

REINDEX TABLE project 会重建 project 表上的所有索引。它是一项非破坏性操作(不会修改任何数据行),但需要注意以下风险:

项目影响
写锁在重建索引期间,表会获取 ACCESS EXCLUSIVE 锁,这会在操作完成之前阻塞该表上的所有读写操作(SELECT/INSERT/UPDATE/DELETE)。
持续时间对于较小的表(例如只有几十行),通常只需几毫秒即可完成。对于较大的表,锁定持续时间也会相应增加。
连接影响如果其他 Harbor 组件(core、jobservice 等)在重建索引期间尝试查询 project 表,这些请求会进入排队,可能表现为短暂卡顿或超时。
失败安全性如果重建索引被中断(例如 Pod 重启、连接断开),PostgreSQL 会自动回滚。原有索引将保持完整且可用,不会发生数据丢失或损坏。
磁盘空间重建索引在删除旧索引之前,需要临时额外的磁盘空间来构建新索引。请确保数据库卷具有足够的可用空间。

建议:

  • 在生产环境中,请在低流量维护窗口期间执行,以尽量减小写锁带来的影响。
  • 对于 project 表本身而言,数据量通常很小(几十到几百行),因此锁定持续时间通常可以忽略不计。
  • 如果需要零停机,PostgreSQL 12+ 支持 REINDEX TABLE CONCURRENTLY project,它可以在不持有独占锁的情况下构建新索引。不过,该方式耗时更长,并且需要更多磁盘空间。