Open edX 课程创作 AuthZ 迁移的双轨权限路径设计:以 ADR 0027 为例
Open edX 课程创作 AuthZ 迁移的双轨权限路径设计以 ADR 0027 为例【免费下载链接】openedx-platformThe Open edX LMS Studio, powering education sites around the world!项目地址: https://gitcode.com/GitHub_Trending/ed/openedx-platform本文围绕 Open edX 的架构决策记录docs/decisions/0027-dual-path-course-authoring-migration.rst展开讲清AUTHZ_COURSE_AUTHORING_FLAG门控的从 legacyCourseAccessRole迁移到 openedx-authz过程中如何引入角色感知的双路径判断。读完后你能掌握enable_authz_course_authoring(course_key, role)的设计动机与行为细节、course_creator_group等未迁移角色如何安全回退到 legacy 路径以及调用方何时必须传入role参数以避免复发 500/403 故障。背景迁移中的角色覆盖不全问题Open edX 正在把课程创作权限的授权引擎从 legacy 的CourseAccessRole表切换到 openedx-authz。这个切换由 waffle 开关AUTHZ_COURSE_AUTHORING_FLAGauthz.enable_course_authoring整体门控定义在 toggles.py 中# .. toggle_description: This toggle will enable the new openedx-authz authorization engine for course authoring. # .. toggle_use_cases: temporary # .. toggle_creation_date: 2026-02-05 # .. toggle_target_removal_date: 2027-06-09 AUTHZ_COURSE_AUTHORING_FLAG CourseWaffleFlag(authz.enable_course_authoring, __name__)注意几个关键属性toggle_use_cases: temporary这是一个临时开关迁移完成后会移除toggle_target_removal_date: 2027-06-09计划移除时间即本 ADR 所说的stopgapCourseWaffleFlag按course_key粒度启用不是全局开关。问题在于迁移尚未完成只有部分 legacy 角色在 openedx-authz 中有对应角色由 openedx-authz 的LEGACY_COURSE_ROLE_EQUIVALENCES定义而course_creator_group和org_course_creator_group尚无对应角色。最初的实现只判断 waffle 开关从不检查正在处理的这个角色是否已经迁移于是打开开关后出现两个故障均记录在 openedx-authz issue #353、#354通过 Django admin 授予 course creator 权限时报 500已有 legacy course creator 授权的用户在创建课程时收到 403。两者的共同根因是同一个代码试图对一个在 openedx-authz 中尚无对应角色的 legacy 角色使用 authz 路径导致授权解析失败异常或权限被拒。决策给enable_authz_course_authoring增加可选role参数ADR 的核心决定是让enable_authz_course_authoring接受一个可选的role参数以便基于具体角色做决策。在 roles.py 中的实现如下def enable_authz_course_authoring(course_key: CourseKey | None None, role: str | None None) - bool: True only if the authz.enable_course_authoring waffle flag is enabled and, when role is given, that role has a migrated authz equivalent. ... if not AUTHZ_COURSE_AUTHORING_FLAG.is_enabled(course_key): return False if role is not None and get_authz_role_from_legacy_role(role) is None: return False return True行为规则完全对应 ADR 的三条决策点条件返回值说明开关关闭对给定course_keyFalse函数提前返回从不评估role零额外开销开关开启 未传roleTrue保持旧行为跟随开关通用权限检查调用方适用开关开启 传入role且该角色在LEGACY_COURSE_ROLE_EQUIVALENCES中无对应 authz 角色False回退 legacy 路径避免 500/403开关开启 传入role且存在对应 authz 角色True走 authz 路径其中角色映射函数同样在 roles.pydef get_authz_role_from_legacy_role(legacy_role: str) - str: return authz_roles.LEGACY_COURSE_ROLE_EQUIVALENCES.get(legacy_role, None) def get_legacy_role_from_authz_role(authz_role: str) - str: return next((k for k, v in authz_roles.LEGACY_COURSE_ROLE_EQUIVALENCES.items() if v authz_role), None)LEGACY_COURSE_ROLE_EQUIVALENCES来自openedx_authz.constants.roles外部依赖 openedx-authz 包本仓库只负责查表。从源码结构看role参数只有在开关已开启时才被求值第一行if not AUTHZ_COURSE_AUTHORING_FLAG.is_enabled(course_key): return False这印证了 ADR role check only runs when the flag is on 的表述。调用方约定操作具体角色时必须传roleADR 的调用方规则很明确操作具体角色的调用方授予/撤销应把该角色传入不关心具体角色的调用方通用权限检查可省略。在源码中RoleBase的所有双路径入口都遵循了这个约定例如 roles.pydef add_users(self, *users): if enable_authz_course_authoring(self.course_key, roleself._role_name): self._authz_add_users(users) else: self._legacy_add_users(users) def remove_users(self, *users): if enable_authz_course_authoring(self.course_key, roleself._role_name): self._authz_remove_users(users) else: self._legacy_remove_users(users)类似的调用点还包括users_with_role、get_orgs_for_user、has_org_for_user均在RoleBase上以及CourseRole.course_group_already_exists注意这个方法不传role因为它检查的是该课程是否已存在任何角色授权这种通用判断符合不关心具体角色则省略的规则。相反CourseCreatorRoleROLE course_creator_group与OrgContentCreatorRoleROLE org_course_creator_group目前没有 authz 等价角色。ADR 中提到的is_content_creator检查正是因此完全跳过开关判断# common/djangoapps/student/auth.py def is_content_creator(user, org): Neither CourseCreatorRole nor OrgContentCreatorRole has a migrated AuthZ equivalent yet (see ADR 0027), so this always checks the legacy role-based permission system. Once either role gets a migrated equivalent, this should also check AuthZ, gated on that role. return _has_legacy_content_creator_access(user, org) def _has_legacy_content_creator_access(user, org): return (user_has_role(user, CourseCreatorRole()) or user_has_role(user, OrgContentCreatorRole(orgorg)))见 auth.py。而 Studio 的课程创建入口views/course.py 中的create_course正是用is_content_creator(request.user, org)来放行has_course_creator_role is_content_creator(request.user, org) if not has_course_creator_role: raise PermissionDenied()这条链路解释了故障 #354 的成因与修复方式由于is_content_creator始终走 legacy 判断已有 legacycourse_creator_group授权的用户即使在开关开启后依然能通过创建权限校验。未迁移角色的读取路径RoleCache 双源合并除了写路径grant/revokeADR 隐含的第二个风险是读路径开关开启后若只从 openedx-authz 读角色legacy 表中的未迁移授权会被漏掉。仓库中的实际解法是 roles.py 中的AuthzCompatCourseAccessRole兼容数据类与RoleCachedataclass(frozenTrue) class AuthzCompatCourseAccessRole: Generic data class for storing CourseAccessRole-compatible data to be used inside BulkRoleCache and RoleCache. This allows the cache to store both legacy and openedx-authz compatible roles user_id: int username: str org: str course_id: str | None role: strRoleCache.__init__在缓存未命中时会同时加载两个来源openedx-authz 兼容角色get_authz_compat_course_access_roles_for_user(user)把用户在 authz 中的课程/组织/平台级授权转换为 legacy 兼容记录无 legacy 映射的 authz 角色被跳过legacy 角色CourseAccessRole.objects.filter(useruser)直接查表。BulkRoleCache.prefetch批量预取roles.py同样合并两个来源。这意味着RoleCache.has_role对未迁移角色如course_creator_group的判定始终基于 legacy 表数据与is_content_creator的始终走 legacy策略一致避免了写走 legacy、读走 authz导致读不到的不一致。影响与后续清理计划ConsequencesADR 的Consequences一节列出了四条影响结合源码可逐条对应验证未迁移角色在开关开启后继续正常工作RoleBase.add_users/remove_users/users_with_role传入role后自动回退 legacyis_content_creator始终走 legacy——对应修复 #353Django admin 授予 500与 #354已有授权用户 403。不传role的调用方零成本role是可选参数缺省时函数只在开关开启时直接返回True无任何额外查表。主要风险是未来调用方忘传roleADR 明确指出若某调用方操作具体角色却忘记传入会静默地复现本决策修复的同一个 bug。这是给后续开发者的硬性约束——凡是对某个具体 legacy 角色做 grant/revoke/list 的操作调用enable_authz_course_authoring时必须带上roleself._role_name。这是过渡方案不是最终形态ADR 强调stopgap, not a permanent shape。当所有 legacy 角色都有 authz 等价角色后角色检查以及开关本身都会移除——与 toggles.py 中toggle_target_removal_date: 2027-06-09的标注一致。届时is_content_creator的 docstring 也已预埋了待办Once either role gets a migrated equivalent, this should also check AuthZ, gated on that role.适用前提与限制本 ADR 描述的行为适用于当前仓库版本AUTHZ_COURSE_AUTHORING_FLAG按course key 粒度生效CourseWaffleFlag同一平台内可以部分课程开启、部分关闭角色映射表LEGACY_COURSE_ROLE_EQUIVALENCES维护在外部依赖 openedx-authz 中见 openedx-authz ADR 0011 的角色映射表本仓库 roles.py 只是查表入口开关的toggle_warning提示启用该开关会触发在 legacy 与 openedx-authz 之间的角色授权数据迁移生产环境开启前需评估数据迁移影响文中提到的 openedx-authz issue #353 / #354 属于外部仓库openedx-authz本仓库内无法直接验证其细节事实依据以 ADR 原文陈述为准。关键文件索引内容路径本 ADR 原文0027-dual-path-course-authoring-migration.rst开关定义toggles.pyenable_authz_course_authoring/ 角色映射 / 双路径角色类roles.pyis_content_creator始终走 legacyauth.pyStudio 课程创建权限入口views/course.py基于开关的权限装饰器不关心具体角色的调用方示例decorators.py【免费下载链接】openedx-platformThe Open edX LMS Studio, powering education sites around the world!项目地址: https://gitcode.com/GitHub_Trending/ed/openedx-platform创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考