# -*- coding: utf-8 -*- """三级 RBAC 权限模型测试(issue #62)。 覆盖: 1. 三级角色权限矩阵正确(readonly/engineer/admin); 2. 角色继承(admin 继承 engineer 继承 readonly); 3. has_permission 允许/拒绝判定 + 理由可解释; 4. 细粒度收窄(restricted_to_readonly 把写动作降级拒绝); 5. 便捷判定 can_publish / 用户概览。 """ import os import sys import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import _bootstrap # noqa: F401 from template_console.rbac import ( # noqa: E402 Action, Permission, Resource, Role, RoleKind, User, all_roles, can_publish, effective_permissions, get_role, has_permission, user_summary, ) class RoleRegistryTest(unittest.TestCase): """三级角色注册表。""" def test_three_roles_present(self): roles = {r.kind for r in all_roles()} self.assertEqual(roles, {RoleKind.READONLY, RoleKind.ENGINEER, RoleKind.ADMIN}) def test_role_labels_in_chinese(self): self.assertEqual(get_role(RoleKind.READONLY).label, "只读") self.assertEqual(get_role(RoleKind.ENGINEER).label, "行业工程师") self.assertEqual(get_role(RoleKind.ADMIN).label, "管理员") def test_role_descriptions_explainable(self): # 可解释性:每个角色都有职责说明 for role in all_roles(): self.assertTrue(role.description, f"{role.kind} 缺少 description") def test_inheritance_chain(self): self.assertEqual(get_role(RoleKind.ADMIN).inherits, RoleKind.ENGINEER) self.assertEqual(get_role(RoleKind.ENGINEER).inherits, RoleKind.READONLY) self.assertIsNone(get_role(RoleKind.READONLY).inherits) def test_permission_key_format(self): p = Permission(RoleKind.ADMIN, Resource.USER, Action.MANAGE) # 匹配键为资源:动作(角色无关,便于继承);审计键含授予角色 self.assertEqual(p.key(), "user:manage") self.assertEqual(p.audit_key(), "admin:user:manage") class EffectivePermissionTest(unittest.TestCase): """继承后的有效权限集合。""" def test_admin_inherits_engineer_and_readonly(self): eff = effective_permissions(RoleKind.ADMIN) # 匹配键为 resource:action:admin 拥有自身的 user:manage, # 也继承 engineer 的 model_param:edit 与 readonly 的 layout:view self.assertIn("user:manage", eff) self.assertIn("model_param:edit", eff) self.assertIn("layout:view", eff) def test_engineer_cannot_publish(self): eff = effective_permissions(RoleKind.ENGINEER) # 工程师不能发布/推送/管用户 self.assertNotIn("release:publish", eff) self.assertNotIn("push:publish", eff) self.assertNotIn("user:manage", eff) def test_readonly_has_no_write(self): eff = effective_permissions(RoleKind.READONLY) for key in eff: # 只读权限只能以 :view 结尾 self.assertTrue(key.endswith(":view"), f"readonly 不应有写/发布权限: {key}") class HasPermissionTest(unittest.TestCase): """has_permission 判定 + 理由。""" def setUp(self): self.ro = User("viewer", RoleKind.READONLY, "查看员") self.eng = User("li_engineer", RoleKind.ENGINEER, "李工") self.admin = User("root_admin", RoleKind.ADMIN, "管理员甲") def test_readonly_view_allowed(self): d = has_permission(self.ro, Resource.LAYOUT, Action.VIEW) self.assertTrue(d.allow) self.assertEqual(d.source, "explicit") def test_readonly_edit_denied(self): d = has_permission(self.ro, Resource.LAYOUT, Action.EDIT) self.assertFalse(d.allow) self.assertIn("缺少权限", d.reason) def test_engineer_edit_allowed_inherited_view(self): # 工程师编辑是本角色权限(explicit) d_edit = has_permission(self.eng, Resource.LAYOUT, Action.EDIT) self.assertTrue(d_edit.allow) self.assertEqual(d_edit.source, "explicit") # 工程师查看布局是继承自 readonly(inherited) d_view = has_permission(self.eng, Resource.LAYOUT, Action.VIEW) self.assertTrue(d_view.allow) self.assertEqual(d_view.source, "inherited") def test_engineer_publish_denied(self): d = has_permission(self.eng, Resource.RELEASE, Action.PUBLISH) self.assertFalse(d.allow) def test_admin_publish_allowed(self): d = has_permission(self.admin, Resource.RELEASE, Action.PUBLISH) self.assertTrue(d.allow) self.assertEqual(d.source, "explicit") def test_admin_inherited_engineer_edit(self): d = has_permission(self.admin, Resource.MODEL_PARAM, Action.EDIT) self.assertTrue(d.allow) self.assertEqual(d.source, "inherited") def test_decision_carries_reason(self): # 可解释性:无论允许/拒绝,reason 非空且含用户名与资源 for user in (self.ro, self.eng, self.admin): d = has_permission(user, Resource.PUSH, Action.PUBLISH) self.assertIn(user.username, d.reason) self.assertIn(Resource.PUSH.value, d.reason) class RestrictedUserTest(unittest.TestCase): """细粒度收窄:restricted_to_readonly。""" def test_restricted_engineer_cannot_edit_that_resource(self): # 工程师本可编辑布局,但被收窄为只读后应拒绝 u = User("limited", RoleKind.ENGINEER, "受限工程师", restricted_to_readonly=[Resource.LAYOUT]) d = has_permission(u, Resource.LAYOUT, Action.EDIT) self.assertFalse(d.allow) self.assertEqual(d.source, "restricted") def test_restricted_engineer_can_still_view(self): u = User("limited", RoleKind.ENGINEER, "受限工程师", restricted_to_readonly=[Resource.LAYOUT]) d = has_permission(u, Resource.LAYOUT, Action.VIEW) self.assertTrue(d.allow) def test_restricted_only_affects_named_resource(self): u = User("limited", RoleKind.ENGINEER, "受限工程师", restricted_to_readonly=[Resource.LAYOUT]) # 模型超参未被收窄,仍可编辑 d = has_permission(u, Resource.MODEL_PARAM, Action.EDIT) self.assertTrue(d.allow) class ConvenienceTest(unittest.TestCase): """便捷判定与用户概览。""" def test_can_publish(self): self.assertFalse(can_publish(User("v", RoleKind.READONLY))) self.assertFalse(can_publish(User("e", RoleKind.ENGINEER))) self.assertTrue(can_publish(User("a", RoleKind.ADMIN))) def test_user_summary(self): s = user_summary(User("li", RoleKind.ENGINEER, "李工")) self.assertEqual(s["username"], "li") self.assertEqual(s["role"], "engineer") self.assertEqual(s["role_label"], "行业工程师") self.assertGreater(s["effective_permission_count"], 0) self.assertEqual(s["restricted_to_readonly"], []) if __name__ == "__main__": unittest.main()