综合 GoatFlow 0.10.0:accept[:16] 越界 panic、PostgreSQL 卡在 migration 5、token 无 session 无法注销

2026-10-06 20:01:47

GoatFlow 0.10.0:accept[:16] 越界 panic、PostgreSQL 卡在 migration 5、token 无 session 无法注销

2026 年 10 月 4 日

项目信息

0.9.0 解决的是能装起来;0.10.0 处理的是运行后状态是否真实。93 commits,263 条 changelog entries。

登录与自助服务

LDAP / Active Directory 登录可用。ldap provider 以前返回 not yet implemented。现在会搜索目录、以用户身份 bind 检查密码,并在 StartTLS/LDAPS 上验证服务器证书。

代理和客户的忘记密码重置,以及客户自助注册。重置链接和密码重试有速率限制。

Passkey 管理与恢复码。

现在每次登录都有 session。OIDC、SAML 和 POST /api/v1/auth/login 以前发出的 token 背后没有 session,所以 “log out” 和 Admin → Sessions → kill 不会撤销任何东西。Token 现在带 session id(sid),session 一消失就被拒绝。

login -> JWT login -> session row -> JWT{sid}
logout -> cookie cleared; logout -> session deleted; JWT still valid old -> JWT{sid} refused on every route

这个变化有一个代价:升级后所有人需要重新登录一次。0.9.0 的 token 没有 sid,会被拒绝。

PostgreSQL,真的能用了

0.9.0 在 PostgreSQL 上根本装不上:golang-migrate 停在 migration 5,报 duplicate migration file。0.10.0 在 MariaDB 和 PostgreSQL 上跑完整套件,SQL 可移植性 lint(cmd/gk-lint)现在在 pre-commit 里用两个数据库的 schema 检查每条查询。

  • Fresh PostgreSQL install: 0.9.0 fails at migration 5 -> 0.10.0 works
  • MySQL-only SQL in Go code: caught in review -> caught by gk-lint
  • Unknown table/column in a query: runtime error -> lint error
  • Upserts: hand-written per driver -> database.ConvertUpsert

如果你试过在 PostgreSQL 上跑 0.9.0,安装 0.10.0 时要用全新安装。没有 0.9.0 的 PostgreSQL 数据库可以升级。

兼容 OTRS 的存储

附件和原始 article body 现在可以放到 OTRS 保存它们的位置,goatflow-storage 是一个可用的 CLI,用于在 database 和 filesystem storage 之间移动。

插件平台开始校验输入

权限被强制执行。每个 HostAPI 调用和插件运行的每条 SQL 语句,都会对照它声明的 resources.permissions 检查。gRPC 插件以前会绕过沙箱;现在也走沙箱。

Org 作用域生效。以前插件查询只要提到 org_id,就可能跳过组织过滤。

插件以调用者身份运行。Routes、widgets、插件 UI 和 MCP 调用会携带用户的 identity、organisation 和 language。

新 HostAPI:CreateArticle、article attachments、RenderMarkdownToPdf with branding、共享 Tiptap editor partial。

能记住版本的市场

Marketplace index 以前每个插件只描述一个版本。现在每个条目可以带 release history:

{"name":"goat-kb","latest_version":"0.2.0","min_host_version":"0.10.0","versions":[{"version":"0.2.0","min_host_version":"0.10.0"},{"version":"0.1.1","min_host_version":"0.8.0"},{"version":"0.1.0","min_host_version":"0.8.0"}]}

GoatFlow 会选择它能实际运行的最新版本。

补了三个缺口:update 现在检查 min_host_version;更新失败不再删除插件(下载并在 staging 验证签名,然后替换);插件现在拿到真实 host 版本,而不是硬编码 0.6.4。

两个插件发布:goat-kb 0.2.0 和新的 goat-kanban 0.1.0。

以前只会说 Yes 的接口

  • 工单通知:保存了,从不发送 -> 发送,收件人遵循 OTRS 规则
  • SLA escalation:时间从不计算 -> 首次响应 / 更新 / 解决事件被触发
  • PUT /api/tickets/:id:返回 200,什么都没保存 -> 保存,记录历史
  • Zinc / Elasticsearch:reindex 返回 501 -> 可用,遵守 queue 权限
  • Dashboard widgets:出错时显示 0 -> 显示数据或说明无法显示
  • make test-unit:几乎没跑也能通过 -> 某个 package 加载失败就失败

运维

GET /health 执行真实检查;/metrics 暴露 Prometheus 指标;结构化日志由 LOG_FORMAT / LOG_OUTPUT 控制。

优雅关闭带 connection draining,大小适配 stop grace period。

Scheduled jobs 和 startup migrations 在多个副本间只跑一次,带锁。失败的 migration 不再被标记为已应用。

Helm chart 开箱即用:runner Deployment、持久化文件存储、可用 probes、Valkey 和 Ingress。air-gapped bundle 包含 chart 需要的所有镜像。

升级会安装 bundled plugins 的新版本,同时保留管理员改过的文件。

Compose 用户:deploy/docker-compose.yml 现在要求在 .env 中设置 GOATFLOW_SECURE_KEY(64 hex characters,openssl rand -hex 32)和 SMTP_HOST。key 必须在 app、customer portal 和 runner 中一致,所以 compose 缺它会拒绝启动。

安全清理

  • 客户工单列表 order 参数中的 SQL 注入。
  • 工单视图 description card 中的存储 XSS。
  • POST /api/v1/auth/login 忽略第二因子。
  • SSO 登录 CSRF:OIDC state 和 SAML RelayState 现在绑定到发起登录的浏览器。
  • Identity-provider 和 webhook URL 可能访问内部地址。
  • Identity-provider secrets 和 webhook headers 静态加密。
  • 数据库错误文本不再返回客户端。

版本与统计

93 commits;263 条 changelog entries(30 added, 22 changed, 154 fixed, 32 security, 22 removed);33 个 migrations(0.9.0 是 26 个),在 MariaDB 和 PostgreSQL 上一致;Go 1.25.12,grpc v1.83.2,goldmark v1.7.17,golang-jwt v5.3.1。

附加:Accept 的十六个字符

每个 release 总有一个值得复述的 bug。0.10.0 的这个由 demo deploy smoke test 发现,而且就在保护 demo 的代码里。

func wantsJSON(c *gin.Context) bool {
  accept := c.GetHeader("Accept")
  return accept == "application/json" ||
    c.GetHeader("X-Requested-With") == "XMLHttpRequest" ||
    c.ContentType() == "application/json" ||
    len(accept) > 0 && accept[:16] == "application/json"
}

len(accept) > 0 本来想守住 slice。它什么都没守住:accept[:16] 需要 16 字节,不是 1 字节。curl 发送 Accept: */*,三个字节。每个打到受保护页面的 curl 请求都会 panic,报 slice bounds out of range [:16] with length 3,并以 500 返回。

浏览器从没触发它,因为浏览器的 Accept header 很长。所以 demo 对浏览器用户看起来正常,却被第一个礼貌请求的脚本打崩。

修复:

func wantsJSON(c *gin.Context) bool {
  return strings.HasPrefix(c.GetHeader("Accept"), "application/json") ||
    c.GetHeader("X-Requested-With") == "XMLHttpRequest" ||
    c.ContentType() == "application/json"
}

同一个 smoke test 发现这个 guard 的第二个问题:没有 Referer 时,浏览器重定向把页面指回它自己。现在回退到 dashboard。两处都有 regression tests,能在旧代码上失败。

升级注意:0.9.0 的 token 没有 sid,升级后会被拒绝,所有人要重新登录一次;PostgreSQL 用户需要全新安装 0.10.0,没有 0.9.0 的 PostgreSQL 数据库可升级。

复制全文 生成海报 Go 开源工单系统 PostgreSQL ITSM Wails

推荐文章

程序员茄子在线接单