enforcer_test.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. // 包 rbac 单测:casbin_model.conf 解析 + matcher 四维语义(纯内存,不碰 DB)。
  2. //
  3. // 覆盖范围(S1):
  4. // - model.conf 内嵌加载成功(NewModelFromString 无解析错误);
  5. // - matcher 四维语义:sub(角色码自匹配) × dom(机构/* 通配) × obj(keyMatch2 通配) × act(regexMatch);
  6. // - 策略种子命中矩阵(现路由白名单 1:1 平移的核心场景,对齐 main.go 白名单);
  7. // - 无 g 规则时 sub==p.sub 自匹配恒真(HasLink 源码 line 310 语义验证)。
  8. //
  9. // 不覆盖(留给 S3 真库 e2e):gormAdapter 的 LoadPolicy/SavePolicy/AddPolicy/RemovePolicy
  10. // 对 casbin_rule 表的真实 CRUD(需 e2e_test 库)。
  11. package rbac
  12. import (
  13. "testing"
  14. "github.com/casbin/casbin/v3"
  15. "github.com/casbin/casbin/v3/model"
  16. )
  17. // newMemEnforcer 构造纯内存 Enforcer(内嵌 model.conf + 无 adapter,手动注入策略)。
  18. // 单测隔离用:不依赖 DB,专注验证 model.conf 解析与 matcher 语义。
  19. func newMemEnforcer(t *testing.T) *casbin.Enforcer {
  20. t.Helper()
  21. raw, err := modelConfFS.ReadFile("casbin_model.conf")
  22. if err != nil {
  23. t.Fatalf("读取内嵌 model.conf 失败: %v", err)
  24. }
  25. m, err := model.NewModelFromString(string(raw))
  26. if err != nil {
  27. t.Fatalf("解析 model.conf 失败(matcher 语法错误): %v", err)
  28. }
  29. e, err := casbin.NewEnforcer(m) // 单 model 参数 → InitWithModelAndAdapter(m, nil),不 LoadPolicy
  30. if err != nil {
  31. t.Fatalf("构造 Enforcer 失败: %v", err)
  32. }
  33. return e
  34. }
  35. // seedPolicies 注入 S1 策略种子(现路由白名单 1:1 平移,对齐 core-model-ddl.sql v1.3 INSERT)。
  36. func seedPolicies(t *testing.T, e *casbin.Enforcer) {
  37. t.Helper()
  38. // /org 组:academic/finance/teacher × 4 端点(GET),dom=* 不区分机构
  39. orgPaths := []string{
  40. "/org/institutions", // 列表
  41. "/org/institutions/*", // 详情(keyMatch2 通配 /org/institutions/1)
  42. "/org/campuses",
  43. "/org/campuses/*",
  44. }
  45. for _, role := range []string{"academic", "finance", "teacher"} {
  46. for _, obj := range orgPaths {
  47. if _, err := e.AddPolicy(role, "*", obj, "GET"); err != nil {
  48. t.Fatalf("注入策略失败 %s %s: %v", role, obj, err)
  49. }
  50. }
  51. }
  52. // /org/role-permissions 组:super_admin × 2 端点(GET|PUT)
  53. for _, obj := range []string{"/org/role-permissions", "/org/role-permissions/*"} {
  54. if _, err := e.AddPolicy("super_admin", "*", obj, "GET|PUT"); err != nil {
  55. t.Fatalf("注入策略失败 super_admin %s: %v", obj, err)
  56. }
  57. }
  58. }
  59. // TestModelConfParse 验证 model.conf 内嵌加载 + 四维 matcher 无语法错误。
  60. func TestModelConfParse(t *testing.T) {
  61. e := newMemEnforcer(t)
  62. if e == nil {
  63. t.Fatal("Enforcer 构造为 nil")
  64. }
  65. // 空策略下 Enforce 应恒 false(fail-closed;超管由 middleware 代码层放行,不走策略)。
  66. ok, err := e.Enforce("academic", "1", "/org/institutions", "GET")
  67. if err != nil {
  68. t.Fatalf("Enforce 报错: %v", err)
  69. }
  70. if ok {
  71. t.Error("空策略下 Enforce 应为 false(fail-closed),实际 true")
  72. }
  73. }
  74. // TestRoleSelfMatchNoGRule 验证无 g 规则时 sub==p.sub 自匹配恒真(HasLink line 310 语义)。
  75. func TestRoleSelfMatchNoGRule(t *testing.T) {
  76. e := newMemEnforcer(t)
  77. // 注入单条策略:academic 角色码 + dom=* + /org/institutions + GET
  78. if _, err := e.AddPolicy("academic", "*", "/org/institutions", "GET"); err != nil {
  79. t.Fatalf("注入策略失败: %v", err)
  80. }
  81. // sub=academic(角色码)应命中 p.sub=academic(自匹配,无 g 规则也成立)。
  82. ok, err := e.Enforce("academic", "999", "/org/institutions", "GET")
  83. if err != nil {
  84. t.Fatalf("Enforce 报错: %v", err)
  85. }
  86. if !ok {
  87. t.Error("无 g 规则时 sub=academic 应自匹配 p.sub=academic 命中,实际未命中")
  88. }
  89. }
  90. // TestOrgWhitelistMatrix 验证现路由白名单 1:1 平移的命中矩阵。
  91. func TestOrgWhitelistMatrix(t *testing.T) {
  92. e := newMemEnforcer(t)
  93. seedPolicies(t, e)
  94. cases := []struct {
  95. name string
  96. sub string
  97. dom string
  98. obj string
  99. act string
  100. want bool
  101. }{
  102. // 白名单内角色 × 白名单内路径 → allow
  103. {"academic 列表", "academic", "1", "/org/institutions", "GET", true},
  104. {"academic 详情(keyMatch2)", "academic", "1", "/org/institutions/1", "GET", true},
  105. {"academic 校区列表", "academic", "1", "/org/campuses", "GET", true},
  106. {"academic 校区详情", "academic", "1", "/org/campuses/42", "GET", true},
  107. {"finance 列表", "finance", "1", "/org/institutions", "GET", true},
  108. {"teacher 详情", "teacher", "1", "/org/campuses/7", "GET", true},
  109. // dom=* 通配:任意机构 ID 都命中
  110. {"academic 跨机构 dom", "academic", "999", "/org/institutions", "GET", true},
  111. // 白名单外角色 → deny(1003 场景)
  112. {"student 越权", "student", "1", "/org/institutions", "GET", false},
  113. {"parent 越权", "parent", "1", "/org/campuses", "GET", false},
  114. {"market 越权", "market", "1", "/org/institutions", "GET", false},
  115. // 白名单外路径 → deny
  116. {"academic 越权路径", "academic", "1", "/org/role-permissions", "GET", false},
  117. {"academic 越权方法", "academic", "1", "/org/institutions", "POST", false},
  118. // super_admin 策略(rbacCfg 组)
  119. {"super_admin 读矩阵", "super_admin", "1", "/org/role-permissions", "GET", true},
  120. {"super_admin 写矩阵", "super_admin", "1", "/org/role-permissions", "PUT", true},
  121. {"super_admin 详情通配", "super_admin", "1", "/org/role-permissions/academic", "GET", true},
  122. // academic 不能写矩阵
  123. {"academic 写矩阵越权", "academic", "1", "/org/role-permissions", "PUT", false},
  124. }
  125. for _, c := range cases {
  126. ok, err := e.Enforce(c.sub, c.dom, c.obj, c.act)
  127. if err != nil {
  128. t.Fatalf("Enforce 报错 [%s]: %v", c.name, err)
  129. }
  130. if ok != c.want {
  131. t.Errorf("场景[%s] sub=%s dom=%s obj=%s act=%s 期望=%v 实际=%v",
  132. c.name, c.sub, c.dom, c.obj, c.act, c.want, ok)
  133. }
  134. }
  135. }
  136. // TestActRegexMatch 验证 act 维度 regexMatch 语义(GET|PUT 命中 GET/PUT,不命中 DELETE)。
  137. func TestActRegexMatch(t *testing.T) {
  138. e := newMemEnforcer(t)
  139. if _, err := e.AddPolicy("super_admin", "*", "/org/role-permissions", "GET|PUT"); err != nil {
  140. t.Fatalf("注入策略失败: %v", err)
  141. }
  142. acts := map[string]bool{
  143. "GET": true,
  144. "PUT": true,
  145. "DELETE": false,
  146. "POST": false,
  147. }
  148. for act, want := range acts {
  149. ok, err := e.Enforce("super_admin", "1", "/org/role-permissions", act)
  150. if err != nil {
  151. t.Fatalf("Enforce 报错 act=%s: %v", act, err)
  152. }
  153. if ok != want {
  154. t.Errorf("act=%s 期望=%v 实际=%v", act, want, ok)
  155. }
  156. }
  157. }
  158. // TestKeyMatch2Wildcard 验证 obj 维度 keyMatch2 通配语义(/org/institutions/* 匹配多级/单级尾段)。
  159. func TestKeyMatch2Wildcard(t *testing.T) {
  160. e := newMemEnforcer(t)
  161. if _, err := e.AddPolicy("academic", "*", "/org/institutions/*", "GET"); err != nil {
  162. t.Fatalf("注入策略失败: %v", err)
  163. }
  164. objs := map[string]bool{
  165. "/org/institutions": false, // 精确列表路径不匹配通配尾段(* 需有尾段)
  166. "/org/institutions/1": true, // 单段尾段命中
  167. "/org/institutions/a/b": true, // 多级尾段命中
  168. "/org/other/1": false, // 不同前缀不命中
  169. }
  170. for obj, want := range objs {
  171. ok, err := e.Enforce("academic", "1", obj, "GET")
  172. if err != nil {
  173. t.Fatalf("Enforce 报错 obj=%s: %v", obj, err)
  174. }
  175. if ok != want {
  176. t.Errorf("obj=%s 期望=%v 实际=%v", obj, want, ok)
  177. }
  178. }
  179. }