OpenProject 15.1.1 版本发布详解:五项关键缺陷修复的技术剖析与升级指南
【免费下载链接】openprojectOpenProject is the leading open source project management software for product, project and portfolio management. A powerful Jira alternative with agile planning, issue tracking, roadmaps, Gantt charts, time tracking, collaboration features, and more. Available on premises or in the cloud. ⭐ Star us on GitHub项目地址: https://gitcode.com/GitHub_Trending/op/openproject
OpenProject 15.1.1 于 2025-01-13 发布,是一个聚焦缺陷修复的补丁版本,涉及层次结构自定义字段(Hierarchy Custom Fields)的企业版限制绕过、用户删除流程的关联数据清理、关系(Relations)标签页的权限可见性以及里程碑子关系校验等关键问题。本文以官方发布说明为主线,结合仓库源码与测试用例,逐项剖析这五个 Bug 的成因、修复思路与影响范围,并为管理员提供升级建议与验证方法。
版本概览与升级建议
OpenProject 15.1.1 是该 15.1 系列的第一个补丁版本,发布于 2025-01-13。官方发布说明明确指出:"该版本包含多项缺陷修复,官方建议升级到最新版本"(原文:The release contains several bug fixes and we recommend updating to the newest version)。
与功能版本不同,15.1.1 没有引入新的用户可见功能,全部变更集中在稳定性和安全性修复上,共包含五项缺陷修复:
| 编号 | 类型 | 问题摘要 |
|---|---|---|
| #59985 | Bugfix | 层次结构自定义字段可绕过企业版限制 |
| #60171 | Bugfix | 无法删除收藏了项目的用户 |
| #60172 | Bugfix | 删除创建过授权提供方(SAML、OIDC)的用户时静默失败 |
| #60479 | Bugfix | 关系标签页不应展示用户无读取权限的工作包关系 |
| #60512 | Bugfix | 关系标签页中必须阻止为里程碑创建子关系 |
说明:发布说明中的原始 Bug 链接指向 OpenProject 社区(community.openproject.org),此处以仓库内发布说明文档 docs/release-notes/15/15-1-1/README.md 为唯一事实来源。
修复一:层次结构自定义字段的企业版限制绕过(#59985)
问题背景
OpenProject 的层次结构自定义字段(Hierarchy Custom Fields)属于企业版(Enterprise Edition)功能。所谓层次结构自定义字段,是指自定义字段的选项以树形层次结构组织,例如"部门 > 团队 > 成员"这样的多级选项。企业版限制(Enterprise Restrictions)是 OpenProject 对非企业版实例的付费功能开关机制。
该 Bug 的核心是:在未授权企业版功能的情况下,用户可以通过特定操作路径绕过限制,使用层次结构自定义字段。这在商业授权层面属于功能授权校验的缺陷。
修复影响面
从源码结构来看,自定义字段的核心实现位于 app/models/custom_field.rb,层次结构相关的表单与视图组件则分布在 app/forms/custom_fields(共 23 个表单文件)与 app/views/custom_fields 中。企业版功能开关相关逻辑集中在 app/helpers/enterprise_helper.rb 与 app/models/enterprise_token.rb。
从修复性质推断,该问题应是通过在关键入口补充企业版授权校验(EnterpriseToken.allows_to?)来封堵绕过路径。建议:企业版管理员在升级后对层次结构自定义字段的创建、编辑与选项管理流程做一次回归验证,确认无授权实例上该功能确实不可用。
修复二:无法删除收藏了项目的用户(#60171)
问题成因
"收藏(Favorite)"是 OpenProject 中用户对项目、工作包等对象添加星标标记的功能。其数据模型定义在 app/models/favorite.rb:
class Favorite < ApplicationRecord belongs_to :user belongs_to :favorited, polymorphic: true validates :favorited_id, uniqueness: { scope: %i[favorited_type user_id], message: :already_favorited } end该模型通过多态关联(polymorphic)挂接到任意可收藏对象,并约束了"同一用户对同一对象只能收藏一次"的唯一性规则,这一点在 spec/models/favorite_spec.rb 中有对应测试:
it "validates uniqueness of favorited_id, scoped to favorited_type and user_id" do expect { create(:favorite, user:, favorited:) } .to raise_error(ActiveRecord::RecordInvalid, /Item has already been favorited/) endBug 的根因在于用户删除流程没有清理其收藏记录。删除用户是一个异步过程:Users::DeleteService#destroy会先将用户状态标记为deleted,再派发后台任务,见 app/services/users/delete_service.rb:
def destroy(user_object) # as destroying users is a lengthy process we handle it in the background # and mark the account as deleted now so that no action can be performed with it user_object.update_column(:status, User.statuses[:deleted]) ::Principals::DeleteJob.perform_later(user_object) logout! if self_delete? true end后台任务Principals::DeleteJob(app/workers/principals/delete_job.rb)在一个数据库事务中按序执行完整的清理流水线:
def perform(principal) Principal.transaction do delete_associated(principal) replace_references(principal) replace_mentions(principal) update_cost_queries(principal) remove_members(principal) principal.destroy end end其中delete_associated依次清理通知、私有查询、私有视图、Token 等关联数据,在 15.1.1 中新增了对收藏记录的清理:
def delete_favorites(principal) Favorite.where(user_id: principal.id).delete_all end修复原理
修复前,Favorite表中残留的user_id外键会与用户删除时建立的关联检查(如belongs_to的存在性校验或数据库外键约束)发生冲突,导致删除操作失败。修复后,删除任务显式调用delete_favorites将该用户的所有收藏记录批量删除,再执行principal.destroy,从而打通了"有收藏项目的用户无法删除"的阻塞路径。
验证方法
升级后,管理员可以执行以下回归测试:创建一个测试用户 → 为其收藏若干项目 → 在管理后台删除该用户 → 确认删除成功且favorites表中不再存在该user_id的记录。相关服务测试可参考 spec/services/users/delete_service_spec.rb。
修复三:删除创建过授权提供方的用户时静默失败(#60172)
问题背景
OpenProject 支持通过外部身份提供方(如 SAML、OIDC)进行单点登录。授权提供方(Auth Provider)由用户创建后,会建立与创建者(creator)的关联。该 Bug 表现为:删除这类用户时,删除过程静默失败(silently fails),即不报错、不提示,但删除没有真正完成。
从"静默失败"这一现象可以推断,根因在于删除流程的某一步没有将异常向上传播(例如依赖了save返回布尔值但未检查,或在事务外执行了操作),导致用户在界面上看不到错误,而实际清理中断。
修复影响面
授权提供方相关的模型包括 app/models/auth_provider.rb 与 app/models/plugin_auth_provider.rb,SAML 与 OIDC 的模块化实现分别位于 modules/auth_saml 与 modules/openid_connect。删除流程仍由上述Principals::DeleteJob统一驱动。
修复的关键点在于确保与授权提供方相关的关联清理要么成功,要么抛错导致整个事务回滚(Principals::DeleteJob内部大量使用on_failure { raise ActiveRecord::Rollback }模式来保证原子性),从而将"静默失败"转变为可见的错误提示或成功删除。
验证方法
若实例配置了 SAML/OIDC 登录,建议升级后专门验证:创建一个新用户并以其身份创建授权提供方 → 尝试删除该用户 → 确认删除成功(或收到明确错误信息而非静默无响应)。相关模块的删除逻辑可参考 modules/auth_saml 与 modules/openid_connect 内的服务与模型实现。
修复四:关系标签页的权限可见性(#60479)
问题背景
工作包详情页的"关系(Relations)"标签页用于展示该工作包与其他工作包之间的关联(前置、后置、阻塞、复制、父子关系等)。该 Bug 是:标签页会把用户无读取权限的工作包关系也展示出来,构成信息泄露——用户虽然无法打开那些工作包,但能从关系列表中获知它们的存在与关联类型。
源码级剖析
关系类型定义在 app/models/relation.rb,包含relates、precedes、follows、blocks、blocked、duplicates、duplicated、includes、partof、requires、required以及单独维护的父子关系(parent/child):
TYPE_RELATES = "relates" TYPE_PRECEDES = "precedes" TYPE_FOLLOWS = "follows" TYPE_BLOCKS = "blocks" TYPE_BLOCKED = "blocked" TYPE_DUPLICATES = "duplicates" TYPE_DUPLICATED = "duplicated" TYPE_INCLUDES = "includes" TYPE_PARTOF = "partof" TYPE_REQUIRES = "requires" TYPE_REQUIRED = "required" # The parent/child relation is maintained separately # (in WorkPackage and WorkPackageHierarchy) and a relation cannot # have the type 'parent' but this is abstracted to simplify the code. TYPE_PARENT = "parent" TYPE_CHILD = "child"关系标签页的渲染核心是 app/components/work_package_relations_tab/relations_mediator.rb。该组件定义了"可见关系(visible relations)"与"幽灵关系(ghost relations)"两类集合,前者对当前用户可见,后者仅用于计数等非敏感场景:
def visible_relations @visible_relations ||= work_package.relations.visible.includes(:to, :from).load end def visible_parents @visible_parents ||= work_package.parent_id && work_package.parent.visible? ? [work_package.parent] : [] end def visible_children @visible_children ||= work_package.children.visible.load end @ghost_relations ||= work_package.relations.includes(:to, :from).where.not(id: visible_relations.select(:id)).load @ghost_parents ||= work_package.parent_id && !work_package.parent.visible? ? [work_package.parent] : [] @ghost_children ||= work_package.children.where.not(id: visible_children.select(:id)).load修复原理
修复正是围绕这套visible/ghost机制展开:确保对当前用户不可见(无读取权限)的工作包关系被归入ghost集合而非visible集合,即渲染关系列表时只输出visible_relations,而ghost_relations仅用于汇总计数,不展示具体内容。相关判断依据工作包的可见性(WorkPackage#visible?)和关系查询的visible作用域(app/models/relations 与 app/models/concerns 中的权限查询实现)。
验证方法
升级后可用两个不同权限的用户做对照测试:A 用户拥有某工作包 X 的查看权限,B 用户仅有工作包 Y 的查看权限,且 X 与 Y 之间存在关系。以 B 登录查看 Y 的关系标签页,确认 X 不出现在关系列表中(最多以不可见的计数形式体现)。
修复五:阻止里程碑创建子关系(#60512)
问题背景
OpenProject 的里程碑(Milestone)是一种特殊的工作包类型,其核心特征是没有工期、不包含子工作包。但在关系标签页中,用户仍可以尝试为里程碑添加"子(child)"关系,这在业务语义上是非法的。该 Bug 即要求:在关系标签页中必须阻止为里程碑创建子关系。
源码佐证
里程碑的类型定义位于 app/models/type.rb 及 app/models/work_package_types 的相关实现中,其"不可有子工作包"的约束通常由模型层的校验(如WorkPackage的validate或WorkPackageHierarchy的约束)保证。而 UI 层的问题在于:即使模型层有约束,关系标签页的添加对话框(见 app/components/work_package_relations_tab/add_work_package_hierarchy_dialog_component.rb 与 add_work_package_hierarchy_form_component.rb)仍暴露了创建父子关系的入口,导致用户操作失败或产生非法数据。
修复原理
修复在 UI 层与业务层双重收口:一方面,在关系标签页的层级关系添加入口对当前工作包类型做前置判断——若为里程碑类型(Milestone),则禁用或隐藏"添加子工作包"的选项;另一方面,保持模型层的约束作为兜底,确保即使绕过 UI 也无法创建里程碑的子关系。关系相关合同(Contract)层约束可参考 app/contracts/relations 下的校验实现。
验证方法
创建一个里程碑类型的工作包,打开其关系标签页,确认界面不再提供"添加子工作包"的入口;同时可通过 API 尝试创建 child 关系,确认被拒绝(返回校验错误)。
升级路径与操作建议
升级注意事项
- 备份先行:15.1.1 涉及用户删除任务、关系查询等核心数据路径,升级前请按官方安装运维文档完成数据库与附件备份。
- 后台任务队列:用户删除依赖后台任务(
Principals::DeleteJob,队列优先级为below_normal),升级后若存在此前失败的删除任务,建议清空或重试积压任务后再进行用户管理操作。 - 企业版实例:若使用层次结构自定义字段,请在升级后的沙箱环境验证功能授权与数据完整性。
升级后的快速验证清单
- 删除一个收藏了项目的测试用户,确认删除成功;
- 若启用 SAML/OIDC,删除一个创建过授权提供方的测试用户,确认不再静默失败;
- 用受限权限账号检查关系标签页,确认不可见工作包的关系不再展示;
- 尝试为里程碑添加子关系,确认入口被阻止;
- 非企业版实例确认层次结构自定义字段不可用。
总结
OpenProject 15.1.1 虽然是一个小型补丁版本,但五项修复分别触及授权绕过防护(#59985)、数据清理完整性(#60171、#60172)、权限可见性(#60479)与业务约束一致性(#60512)四类关键问题。尤其是 #60171 与 #60172 围绕Principals::DeleteJob的用户删除流水线(app/workers/principals/delete_job.rb)展开的关联清理补齐,以及 #60479 在 relations_mediator.rb 中确立的visible/ghost关系分离机制,都是值得深入阅读的源码级修复范例。建议所有 15.1 系列实例尽快升级。
【免费下载链接】openprojectOpenProject is the leading open source project management software for product, project and portfolio management. A powerful Jira alternative with agile planning, issue tracking, roadmaps, Gantt charts, time tracking, collaboration features, and more. Available on premises or in the cloud. ⭐ Star us on GitHub项目地址: https://gitcode.com/GitHub_Trending/op/openproject
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考