ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Open edX 课程权限体系重构:用 bridgekeeper 取代 has_access 的决策与实现剖析

Open edX 课程权限体系重构:用 bridgekeeper 取代 has_access 的决策与实现剖析 Open edX 课程权限体系重构用 bridgekeeper 取代 has_access 的决策与实现剖析【免费下载链接】openedx-platformThe Open edX LMS Studio, powering education sites around the world!项目地址: https://gitcode.com/GitHub_Trending/ed/openedx-platform导读本文围绕 Open edXedx-platform课程权限体系的一次关键架构决策展开即 ADR 0003Use bridgekeeper for Permissions and Tracks。该决策提出将平台沿用多年的自研权限 APIhas_access与 CourseMode 选课模式检查逐步迁移到基于 Djangouser.has_perm的具名权限named permissions体系并借助bridgekeeper库把权限规则进一步表达为可下推到数据库的 Django QuerySet 过滤器从而解决课程列表视图因逐对象鉴权引发的性能问题。读完本文你将理解这套迁移的完整背景、四步行动计划、bridgekeeper规则Rule的check/query双接口机制以及它在当前仓库 courseware、instructor、certificates、content_libraries 等模块中的落地形态。背景为什么弃用自研权限 APIOEP-9 与 django-rules 的首次尝试edx-platform 的权限检查长期由自研 APIhas_access承担见 lms/djangoapps/courseware/access.py。它的工作方式是调用方传入user、action和obj由has_access根据对象类型分派到对应的检查函数课程、xblock、ErrorBlock、CourseKey、UsageKey、字符串等并在内部通过一系列角色判断GlobalStaff、CourseStaffRole、CourseInstructorRole、OrgStaffRole、OrgInstructorRole 等来推断用户能做什么。这种角色即权限的隐式模型存在明显问题权限与角色深度耦合难以给新角色复用既有权限也难以把权限条件独立出来复用和测试。为此OEP-9 强制要求在 edx 应用中使用django-rules提供灵活的权限实现先前的 ADR 0002Use django-rules for Permissions and Tracks 已制定了基于django-rules的迁移计划该 ADR 状态为 Accepted但此后没有实际推进完成转换。性能痛点列表视图的逐对象鉴权在等待迁移期间团队观察到两个高流量列表视图存在显著性能问题course_api的 CourseListView课程列表接口学生仪表盘 dashboard 视图。两者的共同点是在CourseOverview从数据库加载完成后才逐个对象执行访问检查导致产生大量额外的数据库查询N1 查询问题。这正是引入bridgekeeper的直接动机——它不仅提供与django-rules极其相似的接口还额外支持把权限规则表达为Django query filters从而可以在数据库层面直接过滤 QuerySet把查完再筛变成筛完再查。此外通过源码审查可以确认bridgekeeper支持 Python 权限检查返回非布尔值。这一点对 edx-platform 至关重要has_access返回的是携带丰富错误信息的AccessResponse对象如VisibilityError、MilestoneAccessError、IncorrectPartitionGroupError等定义于 lms/djangoapps/courseware/access_response.py而非纯布尔值。django-rules 的Predicate会强制把谓词结果转成布尔这正是 0002 号 ADR 需要子类化Predicate的原因而 bridgekeeper 天然不要求布尔返回值迁移成本更低。决策全面转向 bridgekeeper 与具名权限ADR 0003 的决策很明确不在 edx-platform 中使用 django-rules而是把所有权限检查统一迁移到 bridgekeeper并同步转换所有has_access调用与所有 CourseMode 成员检查为具名权限。转换路径沿用 0002 号 ADR 的计划大纲。两个转换目标分别是has_access调用点把隐式权限由角色推断转换为显式具名权限通过谓词predicates绑定到角色CourseMode 成员检查把用户是否注册在某个 track选课模式的散落判断收敛为user.has_perm检查为未来数据驱动的 CourseMode 权限铺路。现状解剖has_access 到底做了什么在深入行动计划之前有必要看清has_access的真实结构。当前仓库中has_access定义于 lms/djangoapps/courseware/access.pyfunction_trace(has_access) def has_access(user, action, obj, course_keyNone): if not user: user AnonymousUser() # delegate the work to type-specific functions. if isinstance(obj, CourseBlock): return _has_access_course(user, action, obj) if isinstance(obj, CourseOverview): return _has_access_course(user, action, obj) if isinstance(obj, ErrorBlock): return _has_access_error_block(user, action, obj, course_key) if isinstance(obj, XBlock): return _has_access_to_block(user, action, obj, course_key) if isinstance(obj, CourseKey): return _has_access_course_key(user, action, obj) if isinstance(obj, UsageKey): return _has_access_location(user, action, obj, course_key) if isinstance(obj, str): return _has_access_string(user, action, obj) raise TypeError(Unknown object type in has_access(): {}.format(type(obj)))从中可以看到 ADR 所说的许多子句has_access先按对象类型分派再按action如load、enroll、staff、instructor、see_in_catalog、see_about_page、see_exists、load_mobile等从 checkers 字典中取出对应闭包执行。以课程为例_has_access_course见 access.py一个load动作内部要依次检查旧 Mongo 课程兼容性、visible_to_staff_only、开课时间check_course_open_for_learner、前置课程完成情况_can_view_courseware_with_prerequisites、课程有效期check_course_expired且每一步失败时都会回退给 staff 访问检查。这些子句本质上就是 ADR 期望拆分出的谓词predicates例如_can_view_courseware_with_prerequisites、_can_load_course_on_mobile、_can_enroll_courselike、_has_catalog_visibility、_visible_to_nonstaff_users。将它们转换为独立 Rule 后可以按角色、按对象类型单独测试和复用。行动计划详解四步迁移路线ADR 0003 给出了明确的行动顺序按优先级排列如下。第 1 步将 has_access 调用者转换为 user.has_perm这是整个迁移的自举bootstrap阶段也是最重要的第一步将每个has_access调用者改为user.has_perm(permission_name, obj)为每个新权限实现一个 bridgekeeper 规则规则内部继续引用旧的has_access调用。为了能在过渡期沿用has_access这些临时规则采用双实现策略实现query方法时直接抛异常表示该规则暂时无法在数据库层面过滤调用方不应使用 query 路径实现check方法时透传给has_access保证单对象检查行为与迁移前完全一致。ADR 特别指出该步骤可以逐个调用点增量推进、可并行化但当时 edx-platform 中约有150 处has_access调用工作量并不小。这一策略在当前仓库中已有直接落地证据HasAccessRule定义于 lms/djangoapps/courseware/rules.py正是 ADR 描述的包装器class HasAccessRule(Rule): A rule that calls has_access to determine whether it passes def __init__(self, action, strictFalse): self.action action self.strict strict def check(self, user, instanceNone): if self.strict: with strict_role_checking(): return has_access(user, self.action, instance) return has_access(user, self.action, instance) def query(self, user): # Return an always-empty queryset filter so that this always # fails permissions, but still passes the is_possible_for check return Q(pk__in[])注意query返回Q(pk__in[])——一个恒为空的过滤器。这与 ADR 最初query 抛异常的设想略有演进改为返回空 Q 对象既能在列表过滤时保守地拒绝不误放行又能让 django admin 中基于is_possible_for的入口判断正常工作。该规则通过strictTrue时还会启用strict_role_checking()用于严格角色检查场景。以具名权限定义为例lms/djangoapps/courseware/permissions.py 中from bridgekeeper import perms from .rules import HasAccessRule, HasStaffAccessToContent EDIT_BOOKMARK courseware.edit_bookmark MASQUERADE_AS_STUDENT courseware.masquerade_as_student VIEW_COURSE_HOME courseware.view_course_home VIEW_COURSEWARE courseware.view_courseware VIEW_XQA_INTERFACE courseware.view_xqa_interface perms[EDIT_BOOKMARK] HasAccessRule(staff) perms[MASQUERADE_AS_STUDENT] HasStaffAccessToContent() perms[VIEW_COURSE_HOME] HasAccessRule(load) perms[VIEW_COURSEWARE] HasAccessRule(load) perms[VIEW_XQA_INTERFACE] HasAccessRule(staff)VIEW_COURSEWAREcourseware.view_courseware直接复用HasAccessRule(load)即加载课程内容这一历史动作被显式命名。其他模块同样遵循此模式lms/djangoapps/ccx/permissions.pyccx.view_ccx_coach_dashboard→HasAccessRule(staff)lms/djangoapps/certificates/permissions.pycertificates.preview_certificates→HasAccessRule(staff)certificates.view_all_certificates/generate_all_certificates→HasAccessRule(certificates)openedx/core/djangoapps/enrollments/permissions.pyenrollment.enroll_in_course→HasAccessRule(enroll)lms/djangoapps/instructor/permissions.py定义了instructor.dashboard、instructor.enroll、instructor.override_grades等三十余个权限除HasAccessRule外还组合了is_staffbridgekeeper 内置规则与HasRolesRule(data_researcher)等角色规则例如perms[ENABLE_CERTIFICATE_GENERATION] is_staff | HasAccessRule(instructor) perms[CAN_RESEARCH] is_staff | HasRolesRule(data_researcher) perms[VIEW_DASHBOARD] HasRolesRule(staff, instructor, data_researcher) \ | HasAccessRule(staff) | HasAccessRule(instructor)HasRolesRulerules.py则负责检查用户是否拥有 CourseRole 或 OrgRole覆盖 course_key 级与 org 级两种作用域。在 requirements/edx/base.txt 中可以看到依赖声明bridgekeeper0.9即当前仓库锁定使用的版本。第 2 步重构 has_access 的内容为独立谓词在全部调用点完成包装后下一步是把has_access内部的逻辑拆分为可独立使用的谓词。ADR 的建议是将原先的多重子句转换为更小的 Rule按角色、对象类型或两者结合进行划分。这样每个谓词都更易于单独测试也便于在定义未来权限时直接复用。以 rules.py 中的HasStaffAccessToContent为例它展示了check 回退 query 下推两条路径如何共存_check_with_has_access走旧的has_access(user, staff, instance)_check_with_query对单对象调用self.filter(...).exists()用数据库查询验证访问权限两者通过laboratory.ExperimentStaffAccessExperiment做 A/B 对照在settings.DEBUG下结果不一致时记录堆栈告警用于验证查询路径与旧路径行为等价。class HasStaffAccessToContent(Rule): def check(self, user, instanceNone): staff_sql_experiment StaffAccessExperiment( raise_on_mismatchsettings.DEBUG, context{userid: user.id, instance: repr(instance)} ) staff_sql_experiment.control(self._check_with_has_access, args(user, instance)) staff_sql_experiment.candidate(self._check_with_query, args(user, instance)) return staff_sql_experiment.conduct()这种双路径对照是迁移工程中非常实用的手段它证明新的 SQL 过滤语义与旧的逐对象has_access语义一致从而可以安全地切换列表视图到数据库过滤。第 3 步用 query/filter 优化列表视图性能这是本 ADR 的核心收益点也是引入 bridgekeeper 的根本原因。计划如下对当前查询数据库后再逐对象过滤的列表视图改为使用 bridgekeeper 权限的过滤能力直接作用于 QuerySet若视图只需要展示对象的存在性而非内容可以使用两个独立权限第一个权限把 QuerySet 限制为应当展示的对象集合第二个权限再检查用户是否有权查看对象内容。HasStaffAccessToContent.queryrules.py给出了一个真实的 SQL 下推实现def query(self, user): if not user.is_authenticated: return EMPTY masq_settings getattr(user, masquerade_settings, {}) masq_as_student [ course_key for (course_key, masq_setting) in masq_settings.items() if masq_setting.role student ] not_masquerading_as_student ~Q(id__inmasq_as_student) is_global_staff user.is_staff course_staff_or_instructor_courses CourseAccessRole.objects.filter( useruser, role__in(staff, instructor) ).exclude( course_idCourseKeyField.Empty, ).values(course_id) org_staff_or_instructor_courses CourseAccessRole.objects.filter( useruser, role__in(staff, instructor), course_idCourseKeyField.Empty, org__isnullFalse ).values(org) query not_masquerading_as_student if not is_global_staff: query Q(id__incourse_staff_or_instructor_courses) | Q(org__inorg_staff_or_instructor_courses) return query该规则在 SQL 层面实现了staff/instructor 可见课程的判定非全局 staff 用户可见的课程 自己担任 staff/instructor 的课程 ∪ 自己担任 org 级 staff/instructor 的组织下所有课程同时排除正在以学生身份伪装masquerade的课程。整个过程只对CourseAccessRole表做一次查询即可生成过滤CourseOverview的 Q 对象——这正是 ADR 期望的用权限过滤 queryset 以取代查完再筛的落地形态可直接应用于 openedx/core/djangoapps/content/course_overviews/models.py 中的CourseOverview查询。bridgekeeper的规则组合能力在 openedx/core/djangoapps/content_libraries/permissions.py 中有更充分的展示它使用Attribute、Relation、ManyRelation、blanket_rule、UNIVERSAL等桥接原语声明式地组合权限例如is_user_active rules.is_authenticated rules.is_active is_global_staff is_user_active rules.is_staff has_explicit_read_permission_for_library ManyRelation( permission_grants, (Attribute(user, lambda user: user) | Relation(group, in_current_groups)) ) perms[CAN_LEARN_FROM_THIS_CONTENT_LIBRARY] ( is_global_staff | Attribute(allow_public_learning, True) | (is_user_active has_explicit_read_permission_for_library) | (is_studio_request is_user_active Attribute(allow_public_read, True)) )而HasPermissionInContentLibraryScope类则展示了query 生成 Q 对象的完整方法论从 openedx-authzCasbin授权系统查询用户拥有某权限的作用域如lib:OrgA:lib-a再解析成Q(org__short_nameOrgA, sluglib-a)形式的组合过滤器甚至支持lib:*平台级作用域直接返回UNIVERSAL。其check方法则对单个对象调用authz_api.is_user_allowed(...)。这印证了 ADR 中手动指定自定义规则或使用Attribute/Relation实现query的两种途径。第 4 步将 track 成员检查转换为权限计划的最后一部分是处理 CourseMode 成员检查。目前 LMS 中大量与学生学习权限相关的判断都基于用户注册的 track如 audit、verified、honor 等模式定义于 common/djangoapps/course_modes/models.py通过直接检查CourseMode实现。ADR 的目标是把每处 track 成员检查转换为user.has_perm具名权限用权限实现这些检查从而允许将 edx-platform 的各个功能解耦并更容易添加权限模式相似但不完全相同的新 track。当前仓库中已能看到一个过渡性实现在 rules.py 中is_track_ok_for_exam谓词通过is_enrollment_valid_for_proctoring判断用户所在 enrollment mode 是否适合参加监考考试并以edx_proctoring.can_take_proctored_exam注册为权限。这正是把 track 成员检查变成权限的雏形。ADR 同时指出其长期愿景是让 CourseMode 权限数据驱动在配置阶段即可指定 CourseMode而不是依赖当前代码 数据库条目的组合。Offramps范围裁剪的退路ADR 对项目可能被中断的情况预先设定了三层退路Offramps体现了务实的工程管理主要退路完成所有has_access调用者到user.has_perm的转换后即可暂停项目——此时收益统一的权限入口已经实现时间充裕时继续重构has_access内部逻辑为独立谓词属于明确的加分项但非必需被迫裁剪范围时只部分完成has_access的转换也是改进可以同时为仍然直接调用has_access的地方添加弃用警告deprecation warnings通过 INCRincremental improvement工单跟踪剩余工作量。迁移完成后的预期收益ADR 的 Consequences 部分列出了三项明确收益权限条件可扩展has_access转换完成后向特定对象的权限检查添加新条件变得更简单谓词可以写在负责该权限的应用的集中位置而不再被迫堆进庞大的 access.py。CourseMode 可扩展且可配置CourseMode 成员检查全部转换后添加权限模式相似的新 CourseMode 类型更轻松并为进一步走向数据驱动配置期指定 CourseMode而非代码与数据库条目结合铺平道路。列表视图性能优化所有检查统一到 bridgekeeper 后列表视图的查询性能得以优化——这正是本文开头所述性能痛点的根治方案。需要说明的是该 ADR 在文档中的状态标注为Proposed提议阶段但从当前仓库源码看其核心构想HasAccessRule包装器、perms具名权限注册、query下推的HasStaffAccessToContent、track 权限can_take_proctored_exam等已经大面积落地可作为实际工程实践的参考范式。后续演进中部分模块如 content_libraries还进一步接入了 openedx-authzCasbin授权体系并通过 bridgekeeper 规则桥接新旧两套系统这也是阅读 0004-personalized-relative-dates.rst 等其他决策时可以对照的演进脉络。总结ADR 0003 记录了一次典型的存量系统权限现代化决策面对约 150 处隐式权限调用与列表视图的 N1 查询性能瓶颈团队选择放弃 django-rules转向支持 QuerySet 过滤的 bridgekeeper并设计了先包装、再拆分、后下推的渐进式迁移路线。当前仓库中的 rules.py、permissions.py 及各业务模块的 permissions 文件正是这套方案从决策文档走向真实代码的完整样本。对于任何面临历史权限代码难扩展、列表查询性能差问题的 Django 项目这个决策案例都具有直接的借鉴价值用具名权限统一入口用规则谓词拆分逻辑用query/filter把鉴权下沉到数据库。【免费下载链接】openedx-platformThe Open edX LMS Studio, powering education sites around the world!项目地址: https://gitcode.com/GitHub_Trending/ed/openedx-platform创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表