| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184 |
- // Package rbac RBAC 接口强制层(第②层)—— Casbin v3 策略引擎 + 自写 GORM 存储。
- //
- // 定位(RBAC 三层模型的"接口强制层",2026-11-08 拍板"引入 Casbin+GORM"):
- // - 本包持有进程内 *casbin.Enforcer(RBAC with Domains,模型见 casbin_model.conf);
- // - 策略存 MySQL `casbin_rule` 表(ptype/v0/v1/v2/v3),经自写 GORM Adapter 读写
- // (不用官方 gorm-adapter——其依赖拉入 pgx/sqlserver/postgres driver + modernc.org/sqlite 大跳,
- // 风险大;自写 Adapter 仅 6 方法,零额外 driver 依赖,只需 casbin/v3);
- // - 接口强制中间件见 middleware.go(CasbinAuth);策略改配写库后 Reload 即时生效(跨实例 30s 轮询,设计 §9)。
- //
- // 设计依据:unified/01-architecture/casbin-rbac-design.md v1.1。
- // - sub=角色码(sc.Role,非 ULID;保留 g 关系口子供未来人级授权);
- // - 两表并存:casbin_rule(接口路径)与 t_role_permission(菜单权限点)不合并;
- // - 超管代码层恒放行(middleware 判 sc.SuperAdmin,不查策略)。
- //
- // 依赖方向:本包属 infra 横切(鉴权执行),依赖 infra/db + gorm + casbin/v3;
- // 不依赖业务域(identity/org)。装配层(cmd/api/main.go)在 DB 就绪后 NewEnforcer。
- package rbac
- import (
- "embed"
- "fmt"
- "strings"
- "github.com/casbin/casbin/v3"
- "github.com/casbin/casbin/v3/model"
- "github.com/casbin/casbin/v3/persist"
- "gorm.io/gorm"
- )
- //go:embed casbin_model.conf
- var modelConfFS embed.FS
- // gormAdapter 自写 GORM 存储适配器(实现 casbin persist.Adapter 6 方法)。
- //
- // 表结构 `casbin_rule`(ptype/v0/v1/v2/v3)——目标态 DDL 见 core-model-ddl.sql v1.3,
- // 本包只读写、不建表(建表走 DDL 先行纪律 + main 的 AutoMigrate 守卫,见 S3)。
- type gormAdapter struct {
- db *gorm.DB
- }
- // ruleRow casbin_rule 表行映射(显式列名,对齐 §1.2 命名纪律)。
- type ruleRow struct {
- Ptype string `gorm:"column:ptype"`
- V0 string `gorm:"column:v0"`
- V1 string `gorm:"column:v1"`
- V2 string `gorm:"column:v2"`
- V3 string `gorm:"column:v3"`
- }
- // 编译期断言:gormAdapter 须满足 casbin persist.Adapter(签名漂移即编译失败)。
- var _ persist.Adapter = (*gormAdapter)(nil)
- // LoadPolicy 全量加载策略入模型(LoadPolicy 语义:清空现有 + 逐行载入)。
- func (a *gormAdapter) LoadPolicy(m model.Model) error {
- var rows []ruleRow
- if err := a.db.Model(&ruleRow{}).Find(&rows).Error; err != nil {
- return err
- }
- m.ClearPolicy()
- for _, r := range rows {
- line := strings.Join([]string{r.Ptype, r.V0, r.V1, r.V2, r.V3}, ",")
- if err := persist.LoadPolicyLine(line, m); err != nil {
- return err
- }
- }
- return nil
- }
- // SavePolicy 全量保存策略(清空表 + 逐条写入;本包仅在显式全量替换时调用,默认改配走 RemoveFilteredPolicy+AddPolicy)。
- func (a *gormAdapter) SavePolicy(m model.Model) error {
- // 清空
- if err := a.db.Exec("DELETE FROM casbin_rule").Error; err != nil {
- return err
- }
- // 逐条写入(p 全量;g 一般为空)。
- // v3 model.GetPolicy(sec, ptype) 双参、双返回值;本模型 p 段 ptype="p"、g 段 ptype="g"。
- for _, sec := range []string{"p", "g"} {
- pol, err := m.GetPolicy(sec, sec)
- if err != nil {
- return err
- }
- for _, rule := range pol {
- if err := a.AddPolicy(sec, sec, rule); err != nil {
- return err
- }
- }
- }
- return nil
- }
- // AddPolicy 追加一条策略(ptype + 字段;v3 列不足时补空串,保持 5 列)。
- func (a *gormAdapter) AddPolicy(sec string, ptype string, rule []string) error {
- fields := rule
- for len(fields) < 4 {
- fields = append(fields, "")
- }
- row := ruleRow{
- Ptype: ptype,
- V0: fields[0],
- V1: fields[1],
- V2: fields[2],
- V3: fields[3],
- }
- return a.db.Create(&row).Error
- }
- // RemovePolicy 删除精确匹配的某条策略。
- func (a *gormAdapter) RemovePolicy(sec string, ptype string, rule []string) error {
- q := a.db.Where("ptype = ?", ptype)
- if len(rule) > 0 {
- q = q.Where("v0 = ?", rule[0])
- }
- if len(rule) > 1 {
- q = q.Where("v1 = ?", rule[1])
- }
- if len(rule) > 2 {
- q = q.Where("v2 = ?", rule[2])
- }
- if len(rule) > 3 {
- q = q.Where("v3 = ?", rule[3])
- }
- return q.Delete(&ruleRow{}).Error
- }
- // RemoveFilteredPolicy 按字段位删策略(fieldIndex 从 0 计,对应 v0..v3)。
- // fieldValues 为空且 fieldIndex>=0 时,删除该 ptype 下所有行(全量替换语义用)。
- func (a *gormAdapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error {
- q := a.db.Where("ptype = ?", ptype)
- if len(fieldValues) > 0 {
- if fieldIndex < 0 || fieldIndex > 3 {
- return fmt.Errorf("rbac: RemoveFilteredPolicy 非法字段位 %d", fieldIndex)
- }
- col := []string{"v0", "v1", "v2", "v3"}[fieldIndex]
- q = q.Where(fmt.Sprintf("%s = ?", col), fieldValues[0])
- }
- return q.Delete(&ruleRow{}).Error
- }
- // Enforcer 进程内 Casbin 鉴权执行器(启动装配一次,全进程共享;含自写 GORM Adapter)。
- //
- // 并发说明:*casbin.Enforcer 内部自带读写锁,Enforce/LoadPolicy 并发安全;
- // 本结构仅持有只读引用 + 暴露 Reload,不额外加锁。
- type Enforcer struct {
- e *casbin.Enforcer
- }
- // NewEnforcer 构造 Enforcer(model.conf 内嵌 + GORM Adapter;启动即 LoadPolicy)。
- //
- // 入参:
- // - db *gorm.DB:数据库句柄(必填;casbin_rule 表须已建,见 core-model-ddl.sql v1.3)。
- //
- // 出参:
- // - *Enforcer:就绪执行器(LoadPolicy 成功后返回;表为空时策略为空集——非超管全 1003,
- // 属 fail-closed;装配层应告警提示种子缺失,见设计 §9 启动)。
- // - error:DB 加载策略失败。
- func NewEnforcer(db *gorm.DB) (*Enforcer, error) {
- if db == nil {
- return nil, fmt.Errorf("rbac: DB 未注入")
- }
- raw, err := modelConfFS.ReadFile("casbin_model.conf")
- if err != nil {
- return nil, fmt.Errorf("rbac: 读取内嵌 model.conf 失败: %w", err)
- }
- m, err := model.NewModelFromString(string(raw))
- if err != nil {
- return nil, fmt.Errorf("rbac: 解析 model.conf 失败: %w", err)
- }
- adapter := &gormAdapter{db: db}
- e, err := casbin.NewEnforcer(m, adapter)
- if err != nil {
- return nil, fmt.Errorf("rbac: 构造 Enforcer 失败: %w", err)
- }
- return &Enforcer{e: e}, nil
- }
- // Reload 重新加载策略(改配端点写库后调用,本实例即时生效;跨实例靠 30s 轮询,设计 §9)。
- func (en *Enforcer) Reload() error {
- return en.e.LoadPolicy()
- }
- // Get 暴露底层 *casbin.Enforcer(供 middleware 调 Enforce;不暴露写接口,写走改配端点)。
- func (en *Enforcer) Get() *casbin.Enforcer {
- return en.e
- }
|