"""scripts/load_path.py — pbl_domain_ext(M8)RBAC 路径注册。 规则(module-development-spec):新增契约必须在此显式注册 .dspy/.ui 路径, **禁止通配符**('*' / 前缀模糊匹配一律不允许),否则新端点上线即 403。 路径与 wwwroot/ 下实际文件一一对应(13 个契约 dspy + 1 个 index.ui)。 """ MODULE_NAME = 'pbl_domain_ext' # 契约端点(设计 modules/pbl_domain_ext.md §3 的 13 个接口,一一对应) API_PATHS = [ # §3.1 关联管理(5) '/pbl_domain_ext/api/pbl_domain_ref_bind.dspy', # bind_ref '/pbl_domain_ext/api/pbl_domain_ref_unbind.dspy', # unbind_ref '/pbl_domain_ext/api/pbl_domain_ref_get.dspy', # get_ref '/pbl_domain_ext/api/pbl_domain_ref_list.dspy', # list_refs '/pbl_domain_ext/api/pbl_domain_ref_update.dspy', # update_ref # §3.2 租户隔离查询封装(5) '/pbl_domain_ext/api/pbl_world_list_by_tenant.dspy', # list_worlds_by_tenant '/pbl_domain_ext/api/pbl_scene_list_by_world.dspy', # list_scenes_by_world '/pbl_domain_ext/api/pbl_entity_list_by_scene.dspy', # list_entities_by_scene '/pbl_domain_ext/api/pbl_world_get_context.dspy', # get_world_with_pbl_context '/pbl_domain_ext/api/pbl_domain_ref_check_access.dspy', # check_ref_access # §3.3 团队/班级维度(3) '/pbl_domain_ext/api/pbl_team_list_by_class.dspy', # list_teams_by_class '/pbl_domain_ext/api/pbl_team_bind_world.dspy', # bind_team_to_world '/pbl_domain_ext/api/pbl_team_world_list.dspy', # get_team_worlds ] # 前端页面 UI_PATHS = [ '/pbl_domain_ext/index.ui', ] # 角色授权:教师(pbl_teacher)可管理关联;学生(pbl_student)只读查询 + 访问校验; # 管理员(admin)全量。角色名沿用 pbl_governance / rbac 既有定义,本模块不新增角色。 ROLE_GRANTS = { '/pbl_domain_ext/api/pbl_domain_ref_bind.dspy': ['admin', 'pbl_teacher'], '/pbl_domain_ext/api/pbl_domain_ref_unbind.dspy': ['admin', 'pbl_teacher'], '/pbl_domain_ext/api/pbl_domain_ref_get.dspy': ['admin', 'pbl_teacher', 'pbl_student'], '/pbl_domain_ext/api/pbl_domain_ref_list.dspy': ['admin', 'pbl_teacher'], '/pbl_domain_ext/api/pbl_domain_ref_update.dspy': ['admin', 'pbl_teacher'], '/pbl_domain_ext/api/pbl_world_list_by_tenant.dspy': ['admin', 'pbl_teacher', 'pbl_student'], '/pbl_domain_ext/api/pbl_scene_list_by_world.dspy': ['admin', 'pbl_teacher', 'pbl_student'], '/pbl_domain_ext/api/pbl_entity_list_by_scene.dspy': ['admin', 'pbl_teacher', 'pbl_student'], '/pbl_domain_ext/api/pbl_world_get_context.dspy': ['admin', 'pbl_teacher', 'pbl_student'], '/pbl_domain_ext/api/pbl_domain_ref_check_access.dspy': ['admin', 'pbl_teacher', 'pbl_student'], '/pbl_domain_ext/api/pbl_team_list_by_class.dspy': ['admin', 'pbl_teacher', 'pbl_student'], '/pbl_domain_ext/api/pbl_team_bind_world.dspy': ['admin', 'pbl_teacher'], '/pbl_domain_ext/api/pbl_team_world_list.dspy': ['admin', 'pbl_teacher', 'pbl_student'], '/pbl_domain_ext/index.ui': ['admin', 'pbl_teacher', 'pbl_student'], } ALL_PATHS = API_PATHS + UI_PATHS def get_paths(): """返回本模块需注册的全部路径(供宿主 rbac 批量 load_path 使用)。""" return list(ALL_PATHS) def get_roles(path): """返回某路径的授权角色列表;未注册路径返回空(fail-closed,不放行)。""" return list(ROLE_GRANTS.get(path, [])) def register(env=None): """向宿主 rbac 注册路径(幂等)。env 缺省自动取 ServerEnv。""" if env is None: try: from ahserver.serverenv import ServerEnv # type: ignore env = ServerEnv() except Exception: env = None registered = [] for path in ALL_PATHS: roles = ROLE_GRANTS.get(path, []) if env is not None: for name in ('load_path', 'rbac_load_path', 'add_path', 'register_path'): func = getattr(env, name, None) if callable(func): try: func(path, roles) except TypeError: func(path) break registered.append({'path': path, 'roles': roles, 'module': MODULE_NAME}) return registered def selfcheck(): """自检:路径无通配符、API/UI 全部有角色授权、无重复。""" problems = [] seen = set() for path in ALL_PATHS: if '*' in path or '?' in path: problems.append('通配符路径禁止注册: %s' % path) if path in seen: problems.append('重复注册: %s' % path) seen.add(path) if not ROLE_GRANTS.get(path): problems.append('缺少角色授权: %s' % path) if not path.startswith('/%s/' % MODULE_NAME): problems.append('路径未落在模块前缀下: %s' % path) return problems if __name__ == '__main__': issues = selfcheck() print('module=%s paths=%d api=%d ui=%d' % (MODULE_NAME, len(ALL_PATHS), len(API_PATHS), len(UI_PATHS))) if issues: for issue in issues: print('FAIL %s' % issue) raise SystemExit(1) print('OK 全部路径显式注册、无通配符、角色授权齐备')