OpenProject 15.1.1 版本发布详解:五项关键缺陷修复的技术剖析与升级指南

OpenProject 15.1.1 版本发布详解:五项关键缺陷修复的技术剖析与升级指南 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/openprojectOpenProject 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 没有引入新的用户可见功能全部变更集中在稳定性和安全性修复上共包含五项缺陷修复编号类型问题摘要#59985Bugfix层次结构自定义字段可绕过企业版限制#60171Bugfix无法删除收藏了项目的用户#60172Bugfix删除创建过授权提供方SAML、OIDC的用户时静默失败#60479Bugfix关系标签页不应展示用户无读取权限的工作包关系#60512Bugfix关系标签页中必须阻止为里程碑创建子关系说明发布说明中的原始 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.rbclass 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.rbdef 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::DeleteJobapp/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.rbSAML 与 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/childTYPE_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),仅供参考