| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- // 包 rbac 单测:casbin_model.conf 解析 + matcher 四维语义(纯内存,不碰 DB)。
- //
- // 覆盖范围(S1):
- // - model.conf 内嵌加载成功(NewModelFromString 无解析错误);
- // - matcher 四维语义:sub(角色码自匹配) × dom(机构/* 通配) × obj(keyMatch2 通配) × act(regexMatch);
- // - 策略种子命中矩阵(现路由白名单 1:1 平移的核心场景,对齐 main.go 白名单);
- // - 无 g 规则时 sub==p.sub 自匹配恒真(HasLink 源码 line 310 语义验证)。
- //
- // 不覆盖(留给 S3 真库 e2e):gormAdapter 的 LoadPolicy/SavePolicy/AddPolicy/RemovePolicy
- // 对 casbin_rule 表的真实 CRUD(需 e2e_test 库)。
- package rbac
- import (
- "testing"
- "github.com/casbin/casbin/v3"
- "github.com/casbin/casbin/v3/model"
- )
- // newMemEnforcer 构造纯内存 Enforcer(内嵌 model.conf + 无 adapter,手动注入策略)。
- // 单测隔离用:不依赖 DB,专注验证 model.conf 解析与 matcher 语义。
- func newMemEnforcer(t *testing.T) *casbin.Enforcer {
- t.Helper()
- raw, err := modelConfFS.ReadFile("casbin_model.conf")
- if err != nil {
- t.Fatalf("读取内嵌 model.conf 失败: %v", err)
- }
- m, err := model.NewModelFromString(string(raw))
- if err != nil {
- t.Fatalf("解析 model.conf 失败(matcher 语法错误): %v", err)
- }
- e, err := casbin.NewEnforcer(m) // 单 model 参数 → InitWithModelAndAdapter(m, nil),不 LoadPolicy
- if err != nil {
- t.Fatalf("构造 Enforcer 失败: %v", err)
- }
- return e
- }
- // seedPolicies 注入 S1 策略种子(现路由白名单 1:1 平移,对齐 core-model-ddl.sql v1.3 INSERT)。
- func seedPolicies(t *testing.T, e *casbin.Enforcer) {
- t.Helper()
- // /org 组:academic/finance/teacher × 4 端点(GET),dom=* 不区分机构
- orgPaths := []string{
- "/org/institutions", // 列表
- "/org/institutions/*", // 详情(keyMatch2 通配 /org/institutions/1)
- "/org/campuses",
- "/org/campuses/*",
- }
- for _, role := range []string{"academic", "finance", "teacher"} {
- for _, obj := range orgPaths {
- if _, err := e.AddPolicy(role, "*", obj, "GET"); err != nil {
- t.Fatalf("注入策略失败 %s %s: %v", role, obj, err)
- }
- }
- }
- // /org/role-permissions 组:super_admin × 2 端点(GET|PUT)
- for _, obj := range []string{"/org/role-permissions", "/org/role-permissions/*"} {
- if _, err := e.AddPolicy("super_admin", "*", obj, "GET|PUT"); err != nil {
- t.Fatalf("注入策略失败 super_admin %s: %v", obj, err)
- }
- }
- }
- // TestModelConfParse 验证 model.conf 内嵌加载 + 四维 matcher 无语法错误。
- func TestModelConfParse(t *testing.T) {
- e := newMemEnforcer(t)
- if e == nil {
- t.Fatal("Enforcer 构造为 nil")
- }
- // 空策略下 Enforce 应恒 false(fail-closed;超管由 middleware 代码层放行,不走策略)。
- ok, err := e.Enforce("academic", "1", "/org/institutions", "GET")
- if err != nil {
- t.Fatalf("Enforce 报错: %v", err)
- }
- if ok {
- t.Error("空策略下 Enforce 应为 false(fail-closed),实际 true")
- }
- }
- // TestRoleSelfMatchNoGRule 验证无 g 规则时 sub==p.sub 自匹配恒真(HasLink line 310 语义)。
- func TestRoleSelfMatchNoGRule(t *testing.T) {
- e := newMemEnforcer(t)
- // 注入单条策略:academic 角色码 + dom=* + /org/institutions + GET
- if _, err := e.AddPolicy("academic", "*", "/org/institutions", "GET"); err != nil {
- t.Fatalf("注入策略失败: %v", err)
- }
- // sub=academic(角色码)应命中 p.sub=academic(自匹配,无 g 规则也成立)。
- ok, err := e.Enforce("academic", "999", "/org/institutions", "GET")
- if err != nil {
- t.Fatalf("Enforce 报错: %v", err)
- }
- if !ok {
- t.Error("无 g 规则时 sub=academic 应自匹配 p.sub=academic 命中,实际未命中")
- }
- }
- // TestOrgWhitelistMatrix 验证现路由白名单 1:1 平移的命中矩阵。
- func TestOrgWhitelistMatrix(t *testing.T) {
- e := newMemEnforcer(t)
- seedPolicies(t, e)
- cases := []struct {
- name string
- sub string
- dom string
- obj string
- act string
- want bool
- }{
- // 白名单内角色 × 白名单内路径 → allow
- {"academic 列表", "academic", "1", "/org/institutions", "GET", true},
- {"academic 详情(keyMatch2)", "academic", "1", "/org/institutions/1", "GET", true},
- {"academic 校区列表", "academic", "1", "/org/campuses", "GET", true},
- {"academic 校区详情", "academic", "1", "/org/campuses/42", "GET", true},
- {"finance 列表", "finance", "1", "/org/institutions", "GET", true},
- {"teacher 详情", "teacher", "1", "/org/campuses/7", "GET", true},
- // dom=* 通配:任意机构 ID 都命中
- {"academic 跨机构 dom", "academic", "999", "/org/institutions", "GET", true},
- // 白名单外角色 → deny(1003 场景)
- {"student 越权", "student", "1", "/org/institutions", "GET", false},
- {"parent 越权", "parent", "1", "/org/campuses", "GET", false},
- {"market 越权", "market", "1", "/org/institutions", "GET", false},
- // 白名单外路径 → deny
- {"academic 越权路径", "academic", "1", "/org/role-permissions", "GET", false},
- {"academic 越权方法", "academic", "1", "/org/institutions", "POST", false},
- // super_admin 策略(rbacCfg 组)
- {"super_admin 读矩阵", "super_admin", "1", "/org/role-permissions", "GET", true},
- {"super_admin 写矩阵", "super_admin", "1", "/org/role-permissions", "PUT", true},
- {"super_admin 详情通配", "super_admin", "1", "/org/role-permissions/academic", "GET", true},
- // academic 不能写矩阵
- {"academic 写矩阵越权", "academic", "1", "/org/role-permissions", "PUT", false},
- }
- for _, c := range cases {
- ok, err := e.Enforce(c.sub, c.dom, c.obj, c.act)
- if err != nil {
- t.Fatalf("Enforce 报错 [%s]: %v", c.name, err)
- }
- if ok != c.want {
- t.Errorf("场景[%s] sub=%s dom=%s obj=%s act=%s 期望=%v 实际=%v",
- c.name, c.sub, c.dom, c.obj, c.act, c.want, ok)
- }
- }
- }
- // TestActRegexMatch 验证 act 维度 regexMatch 语义(GET|PUT 命中 GET/PUT,不命中 DELETE)。
- func TestActRegexMatch(t *testing.T) {
- e := newMemEnforcer(t)
- if _, err := e.AddPolicy("super_admin", "*", "/org/role-permissions", "GET|PUT"); err != nil {
- t.Fatalf("注入策略失败: %v", err)
- }
- acts := map[string]bool{
- "GET": true,
- "PUT": true,
- "DELETE": false,
- "POST": false,
- }
- for act, want := range acts {
- ok, err := e.Enforce("super_admin", "1", "/org/role-permissions", act)
- if err != nil {
- t.Fatalf("Enforce 报错 act=%s: %v", act, err)
- }
- if ok != want {
- t.Errorf("act=%s 期望=%v 实际=%v", act, want, ok)
- }
- }
- }
- // TestKeyMatch2Wildcard 验证 obj 维度 keyMatch2 通配语义(/org/institutions/* 匹配多级/单级尾段)。
- func TestKeyMatch2Wildcard(t *testing.T) {
- e := newMemEnforcer(t)
- if _, err := e.AddPolicy("academic", "*", "/org/institutions/*", "GET"); err != nil {
- t.Fatalf("注入策略失败: %v", err)
- }
- objs := map[string]bool{
- "/org/institutions": false, // 精确列表路径不匹配通配尾段(* 需有尾段)
- "/org/institutions/1": true, // 单段尾段命中
- "/org/institutions/a/b": true, // 多级尾段命中
- "/org/other/1": false, // 不同前缀不命中
- }
- for obj, want := range objs {
- ok, err := e.Enforce("academic", "1", obj, "GET")
- if err != nil {
- t.Fatalf("Enforce 报错 obj=%s: %v", obj, err)
- }
- if ok != want {
- t.Errorf("obj=%s 期望=%v 实际=%v", obj, want, ok)
- }
- }
- }
|