// 包 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) } } }