rbac.go 4.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. // 路由角色白名单中间件(T6.5-d,RBAC 三层模型的"接口强制层",2026-11-06 用户拍板)。
  2. //
  3. // 定位(RBAC 三层模型第②层,与展示层/数据层分工):
  4. // - 展示层(org/rbac.Resolver):role→菜单树/权限码,供 /auth/issue·/auth/me 下发;
  5. // - **本中间件(接口强制层)**:受保护路由声明允许访问的**角色白名单**,请求角色不在
  6. // 白名单内 → code 1003(无权限);超管(super_admin)代码层全量放行,不查白名单;
  7. // - 数据层(org/service.assertInScope + Repository Scoping):随业务域,本中间件不涉。
  8. //
  9. // 为什么是"角色白名单"而非"权限点判定"(拍板 #1 依据):
  10. // - 粒度=7 角色(super_admin/academic/finance/teacher/student/parent/market),与
  11. // membership.role 对齐,路由声明直观(如 GET /org/institutions 只允许 academic+super_admin);
  12. // - 权限点(域.资源.动作)走展示层下发 + 前端 v-perm,接口强制用"角色"已足够——
  13. // 避免每次请求查 t_role_permission 矩阵(TTL 缓存只覆盖菜单,路由白名单是静态声明);
  14. // - 白名单在路由注册处**声明式**挂上(org.Use(authMw, handler.RequireRole("academic"))),
  15. // 改权限=改路由声明(随域模块演进),不动中间件代码。
  16. //
  17. // 数据流(与 T4-5 验签中间件衔接,须在其**之后**挂上):
  18. // 1. authMw 验签 + scope.Set(c, sc)(scope.Scope 已写入 c.Locals);
  19. // 2. 本中间件:scope.FromFiber(c) 取 *Scope;
  20. // 3. sc==nil → 1001(中间件链配置错误:白名单挂在公开路由上);
  21. // 4. sc.SuperAdmin → 放行(超管代码层全量,不查白名单);
  22. // 5. sc.Role ∈ 白名单 → c.Next();否则 1003(无权限,不泄漏"哪些角色可访问")。
  23. //
  24. // 错误码:
  25. // - 1001(ErrUnauth):无已验签 scope(白名单误挂公开路由 / 验签中间件未先执行);
  26. // - 1003(ErrForbidden):角色不在白名单(已登录但无该接口权限)。
  27. package handler
  28. import (
  29. "github.com/gofiber/fiber/v2"
  30. apperrors "yuansiwei/unified-backend/internal/infra/errors"
  31. "yuansiwei/unified-backend/internal/infra/response"
  32. "yuansiwei/unified-backend/internal/infra/scope"
  33. )
  34. // RequireRole 构造"路由角色白名单"中间件(受保护路由专用,须挂在验签中间件之后)。
  35. //
  36. // 入参:roles []string:允许访问该路由的角色码白名单(≥1 个;空切片视为配置错误——
  37. // 见 fail-fast:返回固定 1003 不放行,杜绝"空白名单=全放行"的灾难性误配)。
  38. // 出参:fiber.Handler——可直接 app.Group(...).Use(authMw, RequireRole("academic","finance"))。
  39. //
  40. // 语义:
  41. // - 超管(sc.SuperAdmin=true)恒放行(代码层全量,拍板:超管不受白名单约束);
  42. // - 普通角色:sc.Role 命中白名单任一角色才放行,否则 1003;
  43. // - sc==nil:1001(白名单误挂公开路由,纵深防御 fail-closed,绝不静默放行)。
  44. //
  45. // 使用样例(main.go 路由挂载):
  46. //
  47. // org := app.Group("/org")
  48. // org.Use(authMw) // 先验签+scope 注入
  49. // org.Use(handler.RequireRole("academic", "finance")) // 再角色白名单
  50. // org.Get("/institutions", orgH.Institutions)
  51. func RequireRole(roles ...string) fiber.Handler {
  52. // 空白名单 fail-closed:拒绝所有(含超管除外,超管恒放行)。
  53. // 配置"空白名单"通常是想放行全部角色——但正确做法是显式列全 7 角色或不挂本中间件;
  54. // 静默放行会让"忘了填白名单"的开发者以为"没配=不限制",实际限制的是空集,语义反直觉。
  55. return func(c *fiber.Ctx) error {
  56. sc := scope.FromFiber(c)
  57. if sc == nil {
  58. return response.Fail(c, apperrors.ErrUnauth) // 1001:无 scope(白名单误挂公开路由)
  59. }
  60. if sc.SuperAdmin {
  61. return c.Next() // 超管代码层全量放行(不查白名单)
  62. }
  63. for _, r := range roles {
  64. if r == sc.Role {
  65. return c.Next()
  66. }
  67. }
  68. return response.Fail(c, apperrors.ErrForbidden) // 1003:角色不在白名单
  69. }
  70. }