Files

182 lines
7.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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()