Files

515 lines
30 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package middleware
import (
"strings"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/qianfree/team-api/internal/consts"
"github.com/qianfree/team-api/internal/logic/admin"
"github.com/qianfree/team-api/internal/response"
)
// 说明:曾有 RequirePermission / RequireTenantRole 两个「按路由挂载」的中间件,
// 均无任何调用方,已删除。管理后台的授权统一由 AdminPermissionGuard + adminPermissionRules
// 集中判定(默认拒绝、规则完整性由 rbac_coverage_test.go 保证);租户控制台的角色校验
// 在 logic 层逐方法进行。留着一个从未挂载的 RequireTenantRole 会让人误以为
// /api/tenant 有路由级角色闸门,实际并没有。
// adminPermissionRules defines the mapping from route patterns to permission points.
// Unmapped admin routes are denied by default in AdminPermissionGuard.
var adminPermissionRules = []permissionRule{
// ── tenant 租户管理 ──
{method: "GET", path: "/api/admin/tenants", perm: "tenant:view"},
{method: "GET", prefix: "/api/admin/tenants/", perm: "tenant:view"},
{method: "POST", path: "/api/admin/tenants", perm: "tenant:create"},
{method: "PUT", prefix: "/api/admin/tenants/", suffix: "/status", perm: "tenant:suspend"},
{method: "PUT", prefix: "/api/admin/tenants/", suffix: "/channel-scope", perm: "tenant:edit"},
{method: "PUT", prefix: "/api/admin/tenants/", perm: "tenant:edit"},
{method: "GET", path: "/api/admin/tenants/export", perm: "tenant:view"},
{method: "GET", path: "/api/admin/tenants/select", perm: "tenant:view"},
{method: "GET", path: "/api/admin/tenants/members/export", perm: "member:view"},
// ── user 用户管理(管理员) ──
{method: "GET", path: "/api/admin/users", perm: "user:view"},
{method: "GET", path: "/api/admin/users/export", perm: "user:view"},
{method: "POST", path: "/api/admin/users", perm: "user:create"},
{method: "PUT", prefix: "/api/admin/users/", suffix: "/status", perm: "user:edit"},
{method: "PUT", prefix: "/api/admin/users/", suffix: "/reset-password", perm: "user:edit"},
{method: "PUT", prefix: "/api/admin/users/", suffix: "/unlock", perm: "user:edit"},
{method: "PUT", prefix: "/api/admin/users/", perm: "user:edit"},
{method: "DELETE", prefix: "/api/admin/users/", perm: "user:delete"},
// ── channel 渠道管理 ──
{method: "GET", path: "/api/admin/channels", perm: "channel:view"},
{method: "GET", path: "/api/admin/channels/export", perm: "channel:view"},
{method: "GET", path: "/api/admin/channels/provider-default-urls", perm: "channel:view"},
{method: "GET", prefix: "/api/admin/channels/", perm: "channel:view"},
{method: "POST", path: "/api/admin/channels", perm: "channel:create"},
{method: "POST", prefix: "/api/admin/channels/", suffix: "/test", perm: "channel:test"},
{method: "POST", prefix: "/api/admin/channels/", suffix: "/clone", perm: "channel:create"},
{method: "POST", prefix: "/api/admin/channels/", suffix: "/reset-health", perm: "channel:edit"},
{method: "POST", prefix: "/api/admin/channels/", suffix: "/keys", perm: "channel:edit"},
{method: "DELETE", prefix: "/api/admin/channels/", suffix: "/keys/", perm: "channel:edit"},
{method: "GET", prefix: "/api/admin/channels/", suffix: "/keys", perm: "channel:view"},
{method: "GET", prefix: "/api/admin/channels/", suffix: "/abilities", perm: "channel:view"},
{method: "PUT", prefix: "/api/admin/channels/", suffix: "/abilities", perm: "channel:edit"},
{method: "GET", prefix: "/api/admin/channels/", suffix: "/health_trend", perm: "channel:view"},
{method: "PUT", prefix: "/api/admin/channels/", perm: "channel:edit"},
{method: "DELETE", prefix: "/api/admin/channels/", perm: "channel:delete"},
// Channel OAuth helper endpoints
{method: "POST", path: "/api/admin/channels/oauth/auth-url", perm: "channel:edit"},
{method: "POST", path: "/api/admin/channels/oauth/exchange", perm: "channel:edit"},
{method: "POST", path: "/api/admin/channels/oauth/refresh", perm: "channel:edit"},
// ── model 模型管理 ──
{method: "GET", path: "/api/admin/models", perm: "model:view"},
{method: "GET", path: "/api/admin/models/options", perm: "model:view"},
{method: "GET", path: "/api/admin/models/export", perm: "model:view"},
{method: "GET", path: "/api/admin/models/official-info", perm: "model:view"},
{method: "GET", prefix: "/api/admin/models/", suffix: "/pricing", perm: "model:view"},
{method: "PUT", prefix: "/api/admin/models/", suffix: "/pricing", perm: "model:edit"},
{method: "GET", prefix: "/api/admin/models/", suffix: "/official-pricing", perm: "model:view"},
{method: "GET", prefix: "/api/admin/models/", perm: "model:view"},
{method: "POST", path: "/api/admin/models", perm: "model:create"},
{method: "POST", path: "/api/admin/models/export-json", perm: "model:view"},
{method: "POST", path: "/api/admin/models/import-preview", perm: "model:create"},
{method: "POST", path: "/api/admin/models/import", perm: "model:create"},
{method: "PUT", prefix: "/api/admin/models/", perm: "model:edit"},
{method: "DELETE", prefix: "/api/admin/models/", perm: "model:delete"},
// Model groups
{method: "GET", path: "/api/admin/model-groups", perm: "model:view"},
{method: "GET", path: "/api/admin/model-groups/options", perm: "model:view"},
{method: "GET", prefix: "/api/admin/model-groups/", suffix: "/models", perm: "model:view"},
{method: "PUT", prefix: "/api/admin/model-groups/", suffix: "/models", perm: "model:edit"},
{method: "GET", prefix: "/api/admin/model-groups/", perm: "model:view"},
{method: "POST", path: "/api/admin/model-groups", perm: "model:create"},
{method: "PUT", prefix: "/api/admin/model-groups/", perm: "model:edit"},
{method: "DELETE", prefix: "/api/admin/model-groups/", perm: "model:delete"},
// Tenant model assignments
{method: "GET", prefix: "/api/admin/tenants/", suffix: "/models", perm: "model:view"},
{method: "GET", prefix: "/api/admin/tenants/", suffix: "/available-models", perm: "model:view"},
{method: "GET", prefix: "/api/admin/tenants/", suffix: "/groups", perm: "model:view"},
{method: "POST", prefix: "/api/admin/tenants/", suffix: "/models", perm: "model:edit"},
{method: "PUT", prefix: "/api/admin/tenants/", suffix: "/groups", perm: "model:edit"},
{method: "PUT", prefix: "/api/admin/tenants/", suffix: "/models/", perm: "model:edit"},
{method: "DELETE", prefix: "/api/admin/tenants/", suffix: "/models/", perm: "model:delete"},
// ── billing 计费管理 ──
{method: "GET", path: "/api/admin/billing-records", perm: "billing:view"},
{method: "GET", path: "/api/admin/billing-records/export", perm: "billing:export"},
{method: "GET", path: "/api/admin/usage-logs", perm: "billing:view"},
{method: "GET", path: "/api/admin/usage-logs/summary", perm: "billing:view"},
{method: "GET", path: "/api/admin/usage-logs/export", perm: "billing:export"},
// 用量日志详情(/usage-logs/{id}):前缀兜底匹配,summary/export/cleanup 等精确规则优先
{method: "GET", prefix: "/api/admin/usage-logs/", perm: "billing:view"},
{method: "GET", path: "/api/admin/transactions", perm: "billing:view"},
{method: "GET", path: "/api/admin/wallets", perm: "billing:view"},
{method: "GET", prefix: "/api/admin/wallets/", perm: "billing:view"},
{method: "POST", prefix: "/api/admin/wallets/", suffix: "/adjust", perm: "billing:refund"},
{method: "POST", prefix: "/api/admin/wallets/", suffix: "/offline-recharge", perm: "billing:refund"},
{method: "POST", prefix: "/api/admin/wallets/", suffix: "/frozen-items/release", perm: "billing:refund"},
{method: "POST", prefix: "/api/admin/wallets/", suffix: "/frozen-items/release-all", perm: "billing:refund"},
{method: "PUT", prefix: "/api/admin/wallets/", suffix: "/warning-threshold", perm: "billing:refund"},
// ── plan 套餐管理 ──
{method: "GET", path: "/api/admin/plans", perm: "plan:view"},
{method: "GET", path: "/api/admin/plans/export", perm: "plan:view"},
{method: "GET", prefix: "/api/admin/plans/", perm: "plan:view"},
{method: "POST", path: "/api/admin/plans", perm: "plan:create"},
{method: "PUT", prefix: "/api/admin/plans/", perm: "plan:edit"},
{method: "DELETE", prefix: "/api/admin/plans/", perm: "plan:delete"},
// ── order 订单管理 ──
{method: "GET", path: "/api/admin/orders", perm: "order:view"},
{method: "GET", path: "/api/admin/orders/export", perm: "order:view"},
{method: "GET", prefix: "/api/admin/orders/", perm: "order:view"},
{method: "POST", prefix: "/api/admin/orders/", suffix: "/refund", perm: "order:refund"},
// 手动完成订单会走 markOrderPaidByAdmin + payment.FulfillOrder:无支付凭证直接履约
// (充值入钱包 / 发放套餐),是资金动作而非订单查看,按退款同级管控。
{method: "POST", prefix: "/api/admin/orders/", suffix: "/complete", perm: "order:refund"},
// ── operation 内容运营 ──
{method: "GET", path: "/api/admin/changelogs", perm: "operation:view"},
{method: "POST", path: "/api/admin/changelogs", perm: "operation:edit"},
{method: "PUT", prefix: "/api/admin/changelogs/", perm: "operation:edit"},
{method: "DELETE", prefix: "/api/admin/changelogs/", perm: "operation:edit"},
{method: "POST", prefix: "/api/admin/changelogs/", suffix: "/publish", perm: "operation:edit"},
{method: "GET", path: "/api/admin/announcements", perm: "operation:view"},
{method: "POST", path: "/api/admin/announcements", perm: "operation:edit"},
{method: "PUT", prefix: "/api/admin/announcements/", perm: "operation:edit"},
{method: "GET", path: "/api/admin/notification/templates", perm: "operation:view"},
{method: "GET", prefix: "/api/admin/notification/templates/", perm: "operation:view"},
{method: "PUT", prefix: "/api/admin/notification/templates/", perm: "operation:edit"},
{method: "POST", prefix: "/api/admin/notification/templates/", suffix: "/test", perm: "operation:edit"},
{method: "GET", path: "/api/admin/notification/messages", perm: "operation:view"},
{method: "POST", path: "/api/admin/notification/messages/send", perm: "operation:edit"},
{method: "POST", path: "/api/admin/notification/messages/broadcast", perm: "operation:edit"},
{method: "GET", path: "/api/admin/feedbacks", perm: "support:view"},
{method: "GET", path: "/api/admin/feedbacks/stats", perm: "support:view"},
{method: "POST", prefix: "/api/admin/feedbacks/", suffix: "/reply", perm: "support:reply"},
{method: "PUT", prefix: "/api/admin/feedbacks/", suffix: "/status", perm: "support:edit"},
{method: "GET", path: "/api/admin/tickets", perm: "support:view"},
{method: "GET", prefix: "/api/admin/tickets/", perm: "support:view"},
{method: "PUT", prefix: "/api/admin/tickets/", suffix: "/assign", perm: "support:edit"},
{method: "POST", prefix: "/api/admin/tickets/", suffix: "/reply", perm: "support:reply"},
{method: "PUT", prefix: "/api/admin/tickets/", suffix: "/status", perm: "support:edit"},
// 顶栏铃铛待办计数(工单 + 反馈聚合,权限收敛在 support:view)
{method: "GET", path: "/api/admin/support/pending-summary", perm: "support:view"},
// ── help center 帮助中心 ──
{method: "GET", path: "/api/admin/help-categories", perm: "support:view"},
{method: "POST", path: "/api/admin/help-categories", perm: "support:edit"},
{method: "PUT", prefix: "/api/admin/help-categories/", perm: "support:edit"},
{method: "DELETE", prefix: "/api/admin/help-categories/", perm: "support:edit"},
{method: "GET", path: "/api/admin/help-articles", perm: "support:view"},
{method: "GET", prefix: "/api/admin/help-articles/", perm: "support:view"},
{method: "POST", path: "/api/admin/help-articles", perm: "support:edit"},
{method: "PUT", prefix: "/api/admin/help-articles/", perm: "support:edit"},
{method: "DELETE", prefix: "/api/admin/help-articles/", perm: "support:edit"},
// ── audit 审计日志 ──
{method: "GET", path: "/api/admin/audit/config", perm: "audit:view"},
// 写 sys_options.audit_level(全局审计级别,可设为 none 关闭全站审计采集):
// 属于系统配置而非审计查看,否则只读档的技术支持可以自行关掉审计留痕。
{method: "PUT", path: "/api/admin/audit/config", perm: "system:edit"},
{method: "GET", path: "/api/admin/audit/operation-logs", perm: "audit:view"},
{method: "GET", path: "/api/admin/audit/operation-logs/export", perm: "audit:export"},
{method: "GET", path: "/api/admin/audit/request-logs", perm: "audit:view"},
{method: "GET", prefix: "/api/admin/audit/request-logs/", perm: "audit:view"},
{method: "GET", prefix: "/api/admin/audit/forwarding-trace/", perm: "audit:view"},
{method: "GET", path: "/api/admin/audit/sensitive-logs", perm: "audit:read_sensitive"},
{method: "GET", path: "/api/admin/audit/content-filter-logs", perm: "audit:view"},
{method: "DELETE", path: "/api/admin/audit/content-filter-logs/clear", perm: "audit:clear"},
// ── file 文件管理 ──
{method: "GET", path: "/api/admin/files", perm: "file:view"},
{method: "GET", path: "/api/admin/files/stats", perm: "file:view"},
{method: "POST", path: "/api/admin/files/cleanup", perm: "file:cleanup"},
{method: "GET", prefix: "/api/admin/files/", suffix: "/download", perm: "file:view"},
{method: "GET", prefix: "/api/admin/files/", suffix: "/serve", perm: "file:view"},
{method: "DELETE", prefix: "/api/admin/files/", perm: "file:delete"},
// ── monitor 监控告警 ──
{method: "GET", prefix: "/api/admin/monitor/", perm: "monitor:view"},
{method: "GET", path: "/api/admin/alert/rules", perm: "monitor:view"},
{method: "GET", path: "/api/admin/alert/options", perm: "monitor:view"},
{method: "POST", path: "/api/admin/alert/rules", perm: "monitor:edit"},
{method: "PUT", prefix: "/api/admin/alert/rules/", perm: "monitor:edit"},
{method: "DELETE", prefix: "/api/admin/alert/rules/", perm: "monitor:edit"},
{method: "POST", prefix: "/api/admin/alert/rules/", suffix: "/test", perm: "monitor:edit"},
{method: "GET", path: "/api/admin/alert/events", perm: "monitor:view"},
{method: "PUT", prefix: "/api/admin/alert/events/", perm: "monitor:edit"},
{method: "DELETE", path: "/api/admin/alert/events/clear", perm: "monitor:edit"},
{method: "GET", path: "/api/admin/error-logs", perm: "monitor:view"},
{method: "GET", prefix: "/api/admin/error-logs/", perm: "monitor:view"},
{method: "PUT", prefix: "/api/admin/error-logs/", perm: "monitor:edit"},
{method: "PUT", path: "/api/admin/error-logs/batch-resolve", perm: "monitor:edit"},
{method: "DELETE", path: "/api/admin/error-logs/clear", perm: "monitor:edit"},
{method: "DELETE", path: "/api/admin/monitor/channel-errors/clear", perm: "monitor:edit"},
{method: "GET", path: "/api/admin/error-logs/stats", perm: "monitor:view"},
{method: "GET", path: "/api/admin/cron-jobs", perm: "system:view"},
{method: "POST", prefix: "/api/admin/cron-jobs/", suffix: "/trigger", perm: "system:edit"},
// ── system 系统设置 ──
{method: "GET", path: "/api/admin/settings/categories", perm: "system:view"},
{method: "GET", prefix: "/api/admin/settings/", perm: "system:view"},
{method: "PUT", prefix: "/api/admin/settings/", perm: "system:edit"},
// 配置连通性测试(对象存储 / 邮件):会真实调用外部服务并可能发信,按写权限管控
{method: "POST", prefix: "/api/admin/settings/", perm: "system:edit"},
// Payment settings & channels:渠道配置含商户密钥明文,使用独立的 payment:*
// 权限点,与 system:*(系统设置)解耦,必须显式授予才能访问
{method: "GET", path: "/api/admin/payment-channels", perm: "payment:view"},
{method: "PUT", prefix: "/api/admin/payment-channels/", perm: "payment:edit"},
{method: "GET", path: "/api/admin/payment-settings", perm: "payment:view"},
{method: "PUT", path: "/api/admin/payment-settings", perm: "payment:edit"},
{method: "GET", path: "/api/admin/data-governance/settings", perm: "system:view"},
{method: "PUT", path: "/api/admin/data-governance/settings", perm: "system:edit"},
{method: "POST", prefix: "/api/admin/data-governance/", perm: "system:edit"},
{method: "GET", prefix: "/api/admin/plugins", perm: "system:plugin"},
{method: "POST", prefix: "/api/admin/plugins/", perm: "system:plugin"},
{method: "PUT", prefix: "/api/admin/plugins/", perm: "system:plugin"},
// 邮件发送记录是运营查看通知触达效果的手段,与通知模板同属内容运营,不归系统设置
{method: "GET", prefix: "/api/admin/email/", perm: "operation:view"},
// ── tenant level config 租户等级配置 ──
{method: "GET", path: "/api/admin/tenant-level-configs", perm: "tenant:view"},
{method: "POST", path: "/api/admin/tenant-level-configs", perm: "tenant:create"},
{method: "PUT", prefix: "/api/admin/tenant-level-configs/", perm: "tenant:edit"},
{method: "DELETE", prefix: "/api/admin/tenant-level-configs/", perm: "tenant:delete"},
// ── promo 优惠码管理 ──
{method: "GET", path: "/api/admin/promo-codes", perm: "promo:view"},
{method: "GET", path: "/api/admin/promo-codes/export", perm: "promo:view"},
{method: "GET", prefix: "/api/admin/promo-codes/", perm: "promo:view"},
{method: "POST", path: "/api/admin/promo-codes", perm: "promo:create"},
{method: "PUT", prefix: "/api/admin/promo-codes/", perm: "promo:edit"},
// ── member 成员管理(管理后台维度) ──
{method: "GET", path: "/api/admin/members", perm: "member:view"},
{method: "POST", path: "/api/admin/members", perm: "member:import"},
// 禁用/启用/改密/解锁都是对租户成员账号的处置(改密等于接管该成员账号,
// 进而可用其 API Key 消耗租户余额),必须走写权限点,不能沿用只读的 member:view。
{method: "PUT", prefix: "/api/admin/members/", suffix: "/disable", perm: "member:manage"},
{method: "PUT", prefix: "/api/admin/members/", suffix: "/enable", perm: "member:manage"},
{method: "PUT", prefix: "/api/admin/members/", suffix: "/reset-password", perm: "member:manage"},
{method: "PUT", prefix: "/api/admin/members/", suffix: "/unlock", perm: "member:manage"},
// ── redemption 兑换码管理 ──
{method: "GET", path: "/api/admin/redemptions", perm: "redemption:view"},
{method: "GET", path: "/api/admin/redemptions/usages", perm: "redemption:view"},
{method: "GET", path: "/api/admin/redemptions/export", perm: "redemption:view"},
{method: "POST", path: "/api/admin/redemptions", perm: "redemption:create"},
{method: "PUT", prefix: "/api/admin/redemptions/", perm: "redemption:edit"},
// ── role 角色管理(复用 user 组权限点) ──
// 账号管理与角色管理是同一件事的两面(谁能进后台、能干什么),拆成两组权限没有实际场景:
// 「能建角色但不能分配给人」毫无意义,「能分配角色但不能建角色」用禁用角色即可表达。
{method: "GET", path: "/api/admin/roles", perm: "user:view"},
{method: "POST", path: "/api/admin/roles", perm: "user:create"},
{method: "POST", prefix: "/api/admin/roles/", suffix: "/reset", perm: "user:edit"},
{method: "GET", prefix: "/api/admin/roles/", perm: "user:view"},
{method: "PUT", prefix: "/api/admin/roles/", suffix: "/status", perm: "user:edit"},
{method: "PUT", prefix: "/api/admin/roles/", perm: "user:edit"},
{method: "DELETE", prefix: "/api/admin/roles/", perm: "user:delete"},
{method: "GET", prefix: "/api/admin/users/", suffix: "/roles", perm: "user:view"},
{method: "PUT", prefix: "/api/admin/users/", suffix: "/roles", perm: "user:edit"},
// ── permission 权限管理(仅 user 组) ──
{method: "GET", path: "/api/admin/permissions", perm: "user:view"},
{method: "GET", prefix: "/api/admin/users/", suffix: "/permissions", perm: "user:view"},
{method: "PUT", prefix: "/api/admin/users/", suffix: "/permissions", perm: "user:edit"},
// ── admin session and security ──
{method: "POST", path: "/api/admin/auth/logout", perm: "self:access"},
// 当前用户信息与有效权限:登录即可访问自己的资料,不需要任何业务权限点
{method: "GET", path: "/api/admin/auth/me", perm: "self:access"},
// 会话管理列出并处置的是【全部管理员】的登录态,属于账号管理(user 域)的操作能力:
// 按用户管理模块的操作档位(user:edit)授权,只读档位不可见。
// 超管会话的额外保护在 logic 层(assertCanOperateSessions):仅账号本人可操作。
{method: "GET", path: "/api/admin/auth/sessions", perm: "user:edit"},
{method: "DELETE", prefix: "/api/admin/auth/sessions/", perm: "user:edit"},
{method: "PUT", path: "/api/admin/auth/change-password", perm: "self:access"},
{method: "POST", prefix: "/api/admin/security/2fa/", perm: "self:access"},
{method: "GET", path: "/api/admin/security/login-history", perm: "audit:view"},
{method: "GET", path: "/api/admin/security/tenant-login-history", perm: "audit:view"},
{method: "GET", prefix: "/api/admin/agreements/", suffix: "/acceptances", perm: "audit:view"},
{method: "GET", path: "/api/admin/agreements/pending", perm: "self:access"},
{method: "POST", path: "/api/admin/agreements/accept", perm: "self:access"},
{method: "GET", path: "/api/admin/agreements", perm: "system:view"},
{method: "GET", prefix: "/api/admin/agreements/", perm: "system:view"},
{method: "POST", path: "/api/admin/agreements", perm: "system:edit"},
{method: "PUT", prefix: "/api/admin/agreements/", perm: "system:edit"},
{method: "DELETE", prefix: "/api/admin/agreements/", perm: "system:edit"},
{method: "POST", prefix: "/api/admin/agreements/", suffix: "/publish", perm: "system:edit"},
// ── dashboard and async task management ──
{method: "GET", prefix: "/api/admin/dashboard", perm: "dashboard:view"},
// 工作台读接口只要求 dashboard:view:跨域条目由 logic 层按 channel:view / billing:view /
// support:view 等逐条过滤,进得来不等于看得到,权限收敛在数据侧而非路由侧。
// 工作台没有写接口 —— 待办由源表实时派生,唯一的处置方式是去源头解决。
{method: "GET", path: "/api/admin/workbench/summary", perm: "dashboard:view"},
{method: "GET", path: "/api/admin/workbench/badges", perm: "dashboard:view"},
{method: "GET", path: "/api/admin/tasks", perm: "task:view"},
{method: "GET", prefix: "/api/admin/tasks/", perm: "task:view"},
{method: "POST", prefix: "/api/admin/tasks/", suffix: "/cancel", perm: "task:edit"},
{method: "POST", path: "/api/admin/usage-logs/cleanup", perm: "system:edit"},
{method: "GET", path: "/api/admin/usage-logs/cleanup/tasks", perm: "system:view"},
{method: "POST", prefix: "/api/admin/usage-logs/cleanup/tasks/", suffix: "/cancel", perm: "system:edit"},
}
// superAdminOnlyRules 列出仅超级管理员可访问的路由。
//
// 角色管理不走「权限点授权」而是硬性限定超管,理由是它是权限体系的元操作:
// 谁能改角色,谁就能决定所有人的权限。一旦把它下放出去(哪怕只给 user:edit),
// 被下放者就能给自己挂上更高权限的角色——权限体系当场失效。
//
// 这条限制让提权路径根本不存在,而不是靠「只能授予自己已有的权限」这类兜底去堵。
// 普通账号管理(创建/禁用/改密/删除)不受影响,仍按 user:* 授权,可以正常下放。
var superAdminOnlyRules = []permissionRule{
// 角色的增删改查与启停
{method: "GET", path: "/api/admin/roles"},
{method: "POST", path: "/api/admin/roles"},
{method: "GET", prefix: "/api/admin/roles/"},
{method: "POST", prefix: "/api/admin/roles/"},
{method: "PUT", prefix: "/api/admin/roles/"},
{method: "DELETE", prefix: "/api/admin/roles/"},
// 给账号分配角色 —— 这是最直接的提权入口
{method: "GET", prefix: "/api/admin/users/", suffix: "/roles"},
{method: "PUT", prefix: "/api/admin/users/", suffix: "/roles"},
// 在线自更新(版本状态 / 检查 / 执行 / 回滚):替换平台二进制并重启服务,
// 等同于在服务器上执行代码,是比角色管理更根本的平台级元操作,不下放。
// 版本检查(GET)看似只读,但前端每次刷新都会请求,若走权限点授权,
// 未授权角色会持续收到 403 —— 硬性限定超管后非超管前端不再发起请求。
{method: "GET", prefix: "/api/admin/update/"},
{method: "POST", prefix: "/api/admin/update/"},
}
// isSuperAdminOnly 判断路由是否仅限超级管理员。
// 任一规则命中即为真,不需要优先级判定。
func isSuperAdminOnly(method, path string) bool {
for _, rule := range superAdminOnlyRules {
if rule.method != method {
continue
}
if rule.path != "" && path == rule.path {
return true
}
if rule.prefix != "" && strings.HasPrefix(path, rule.prefix) {
if rule.suffix == "" || suffixMatches(path, rule.suffix) {
return true
}
}
}
return false
}
type permissionRule struct {
method string // HTTP method
path string // exact path match (mutually exclusive with prefix)
prefix string // path prefix match
suffix string // path suffix match (combined with prefix)
perm string // required permission point
}
// AdminPermissionGuard enforces RBAC permission checks for admin routes.
//
// 策略:默认拒绝 —— 未匹配到权限规则的接口一律 403。
//
// 早期为降低上线回归风险曾采用「未匹配即放行」,那在只有 super_admin / admin 两种可信
// 身份时无害;引入运营、技术支持等低权限角色后,放行等同于越权:任何漏配规则的接口对
// 所有角色都是敞开的。改为默认拒绝后,遗漏会立刻表现为 403 而不是静默的权限缺口。
//
// 兜底保证:super_admin 在规则匹配之前短路放行,因此即便规则有遗漏也不会把系统锁死,
// 超管始终可以进入并修复。规则的完整性由 rbac_coverage_test.go 在 CI 阶段保证。
func AdminPermissionGuard(r *ghttp.Request) {
// 路径必须是规范形式:GoFrame 不会归一化重复斜杠,/api/admin//roles 会带着原样路径
// 进到 handler,而规则表按精确路径/前缀匹配,同一个 handler 就有了两种匹配结果。
// 目前默认拒绝把这类变体都挡在外面,但那是「恰好安全」——只要将来某条前缀规则的
// 权限点比精确规则弱,加一个斜杠就能降档。与其依赖这个巧合,不如直接拒收非规范路径:
// 正常客户端不会产生它,攻击者才需要它。
if !isCanonicalPath(r.URL.Path) {
response.ErrorMsg(r, consts.CodeForbidden, "非法的请求路径")
return
}
if isAdminPublicPath(r.URL.Path) {
r.Middleware.Next()
return
}
role := GetUserRole(r.Context())
if role == "super_admin" {
r.Middleware.Next()
return
}
// 角色管理与在线自更新是平台级元操作,硬性限定超管,不参与权限点授权
if isSuperAdminOnly(r.Method, r.URL.Path) {
response.ErrorMsg(r, consts.CodeForbidden, "该操作仅超级管理员可用")
return
}
perm := matchPermission(r.Method, r.URL.Path)
if perm == "" {
// 默认拒绝:未配置权限规则的接口不放行(super_admin 已在上方短路)
response.ErrorMsg(r, consts.CodeForbidden, "接口未配置权限规则,请联系管理员")
return
}
if perm == "self:access" {
r.Middleware.Next()
return
}
userID := GetUserID(r.Context())
if !admin.HasPermission(r.Context(), userID, role, perm) {
response.ErrorMsg(r, consts.CodeForbidden, "缺少权限:"+perm)
return
}
r.Middleware.Next()
}
// isCanonicalPath 判断路径是否为规范形式:不含空路径段("//")与 "." / ".." 段。
//
// 只做判定不做重写:重写会把「当前被默认拒绝挡住的变体」变成放行,等于放宽;
// 直接拒收既不放宽也不留下将来被利用的缝隙。
func isCanonicalPath(path string) bool {
if path == "" {
return false
}
for _, seg := range strings.Split(strings.TrimPrefix(path, "/"), "/") {
if seg == "" || seg == "." || seg == ".." {
return false
}
}
return true
}
// matchPermission finds the permission required for the given method and path.
// Matching priority: exact path > prefix+suffix > prefix-only (catch-all).
// This three-phase approach eliminates rule ordering dependencies.
func matchPermission(method, path string) string {
// Phase 1: exact path match (highest priority)
for _, rule := range adminPermissionRules {
if rule.method != method || rule.path == "" {
continue
}
if path == rule.path {
return rule.perm
}
}
// Phase 2: prefix+suffix match (more specific)
var bestPrefixSuffix *permissionRule
for _, rule := range adminPermissionRules {
if rule.method != method || rule.prefix == "" || rule.suffix == "" {
continue
}
if strings.HasPrefix(path, rule.prefix) && suffixMatches(path, rule.suffix) {
if bestPrefixSuffix == nil || len(rule.prefix)+len(rule.suffix) > len(bestPrefixSuffix.prefix)+len(bestPrefixSuffix.suffix) {
r := rule
bestPrefixSuffix = &r
}
}
}
if bestPrefixSuffix != nil {
return bestPrefixSuffix.perm
}
// Phase 3: prefix-only match (catch-all, lowest priority)
var bestPrefixOnly *permissionRule
for _, rule := range adminPermissionRules {
if rule.method != method || rule.prefix == "" || rule.suffix != "" {
continue
}
if strings.HasPrefix(path, rule.prefix) {
if bestPrefixOnly == nil || len(rule.prefix) > len(bestPrefixOnly.prefix) {
r := rule
bestPrefixOnly = &r
}
}
}
if bestPrefixOnly != nil {
return bestPrefixOnly.perm
}
return ""
}
// suffixMatches 判断 path 是否按【路径分段边界】匹配规则后缀 suffix,避免子串误匹配。
// 旧实现用 strings.Contains(remainder, "test") 会把 /.../latest 误判为命中 /test 规则
// ("latest" 里含 "test"),从而套用错误的权限点。
// - suffix 以 "/" 结尾(如 "/keys/",用于匹配 /.../keys/{id} 这类带尾段的路由):
// 要求 path 中出现完整的 "/keys/" 片段。
// - suffix 不以 "/" 结尾(如 "/status"、"/test"):要求 path 以该后缀结尾,
// 或该后缀后紧跟一个 "/"(即作为完整分段出现,如 /.../test/xxx)。
func suffixMatches(path, suffix string) bool {
if strings.HasSuffix(suffix, "/") {
return strings.Contains(path, suffix)
}
return strings.HasSuffix(path, suffix) || strings.Contains(path, suffix+"/")
}