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