完善角色权限与数据隔离

This commit is contained in:
yml2213
2026-07-26 14:00:50 +08:00
parent b483cbc26d
commit 16d9ab0e5e
38 changed files with 2847 additions and 292 deletions
+249
View File
@@ -0,0 +1,249 @@
package middleware
import (
"net/http"
"strconv"
"sync"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"kefu-cloud/server/internal/model"
)
type cacheEntry struct {
db *gorm.DB
perms map[string]bool
dataScopes map[string]string
version string
expireAt time.Time
}
var (
permCache = sync.Map{}
cacheTTL = 5 * time.Minute
permCtxKey = "permissions"
)
// LoadPermissions 加载当前用户角色的权限码集合,存入 context。
// 使用内存缓存减少数据库查询。
func LoadPermissions(c Context, _ uint, tenantID uint, role string) string {
key := permissionCacheKey(tenantID, role)
if raw, ok := permCache.Load(key); ok {
if entry, ok := raw.(cacheEntry); ok && entry.db == model.DB && time.Now().Before(entry.expireAt) {
c.Set(permCtxKey, entry.perms)
c.Set(dataScopeCtxKey, entry.dataScopes)
return entry.version
}
}
var roleRecord model.Role
err := model.DB.Where("tenant_id = ? AND code = ?", tenantID, role).First(&roleRecord).Error
if err != nil {
perms := legacyRolePermissions(role)
dataScopes := model.DefaultRoleDataScopes(role)
entry := cacheEntry{db: model.DB, perms: perms, dataScopes: dataScopes, version: "legacy:" + role, expireAt: time.Now().Add(cacheTTL)}
permCache.Store(key, entry)
c.Set(permCtxKey, perms)
c.Set(dataScopeCtxKey, dataScopes)
return entry.version
}
var codes []string
model.DB.Table("role_permissions rp").
Joins("JOIN permissions p ON p.id = rp.permission_id").
Where("rp.role_id = ?", roleRecord.ID).
Pluck("p.code", &codes)
perms := make(map[string]bool, len(codes))
for _, c := range codes {
perms[c] = true
}
dataScopes := model.DefaultRoleDataScopes(role)
var scopes []model.RoleDataScope
if err := model.DB.Where("role_id = ?", roleRecord.ID).Find(&scopes).Error; err == nil {
for _, scope := range scopes {
dataScopes[scope.Module] = scope.Scope
}
}
entry := cacheEntry{
db: model.DB, perms: perms, dataScopes: dataScopes,
version: roleRecord.UpdatedAt.UTC().Format(time.RFC3339Nano), expireAt: time.Now().Add(cacheTTL),
}
permCache.Store(key, entry)
c.Set(permCtxKey, perms)
c.Set(dataScopeCtxKey, dataScopes)
return entry.version
}
// InvalidatePermissionCache 权限变更后清除角色缓存。
func InvalidatePermissionCache(tenantID uint, role string) {
permCache.Delete(permissionCacheKey(tenantID, role))
}
// InvalidateTenantPermissionCache 清除租户下所有角色缓存。
func InvalidateTenantPermissionCache(tenantID uint) {
prefix := strconv.FormatUint(uint64(tenantID), 10) + ":"
permCache.Range(func(k, _ interface{}) bool {
if key, ok := k.(string); ok {
if len(key) > len(prefix) && key[:len(prefix)] == prefix {
permCache.Delete(key)
}
}
return true
})
}
const dataScopeCtxKey = "data_scopes"
func permissionCacheKey(tenantID uint, role string) string {
return strconv.FormatUint(uint64(tenantID), 10) + ":" + role
}
// GetDataScope 返回当前角色在指定模块的数据范围,异常或未知范围按 self 收紧。
func GetDataScope(c Context, module string) string {
raw, ok := c.Get(dataScopeCtxKey)
if !ok {
return model.DataScopeSelf
}
scopes, ok := raw.(map[string]string)
if !ok || scopes[module] != model.DataScopeAll {
return model.DataScopeSelf
}
return model.DataScopeAll
}
// CanAccessAllData 判断当前请求是否可读取指定模块的全租户数据。
func CanAccessAllData(c Context, module string) bool {
return GetDataScope(c, module) == model.DataScopeAll
}
// RequirePermission 在路由层强制校验任意一个权限码。
func RequirePermission(codes ...string) gin.HandlerFunc {
return func(c *gin.Context) {
if HasAnyPermission(c, codes...) {
c.Next()
return
}
model.DB.Create(&model.OperationLog{
OperatorID: GetUserID(c), Action: "permission_denied",
Detail: "接口权限不足: " + c.Request.Method + " " + c.FullPath(),
TargetType: "permission", IP: c.ClientIP(),
})
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "无权限执行此操作"})
c.Abort()
}
}
// AuditDataAccessDenied 记录通过参数尝试访问越权数据的行为。
func AuditDataAccessDenied(c *gin.Context, targetType string, targetID *uint) {
model.DB.Create(&model.OperationLog{
OperatorID: GetUserID(c), Action: "data_access_denied",
Detail: "数据范围越权: " + c.Request.Method + " " + c.FullPath(),
TargetType: targetType, TargetID: targetID, IP: c.ClientIP(),
})
}
// HasPermission 检查当前用户是否拥有指定权限码。
func HasPermission(c Context, code string) bool {
raw, ok := c.Get(permCtxKey)
if !ok {
return false
}
perms, ok := raw.(map[string]bool)
if !ok {
return false
}
return perms[code]
}
// HasAnyPermission 检查是否拥有任意一项权限码。
func HasAnyPermission(c Context, codes ...string) bool {
raw, ok := c.Get(permCtxKey)
if !ok {
return false
}
perms, ok := raw.(map[string]bool)
if !ok {
return false
}
for _, code := range codes {
if perms[code] {
return true
}
}
return false
}
// GetPermissions 返回当前用户所有权限码集合。
func GetPermissions(c Context) map[string]bool {
raw, ok := c.Get(permCtxKey)
if !ok {
return nil
}
perms, ok := raw.(map[string]bool)
if !ok {
return nil
}
return perms
}
// legacyRolePermissions 当角色表尚无记录时的兜底权限映射。
func legacyRolePermissions(role string) map[string]bool {
base := map[string]map[string]bool{
"platform_admin": {},
"admin": {
"session.view": true, "session.reply": true, "session.transfer": true,
"session.end": true, "session.note": true, "session.priority": true,
"customer.view": true, "customer.create": true, "customer.edit": true,
"customer.export": true, "customer.tag": true,
"chat_history.view": true, "chat_history.detail": true,
"chat_history.export": true, "chat_history.batch_archive": true,
"knowledge.view": true, "knowledge.create": true, "knowledge.edit": true,
"knowledge.publish": true, "knowledge.delete": true,
"quick_reply.view": true, "quick_reply.team_create": true,
"quick_reply.team_edit": true, "quick_reply.personal_manage": true,
"blacklist.view": true, "blacklist.create": true, "blacklist.delete": true,
"statistics.view": true, "statistics.export": true, "statistics.performance": true,
"settings.basic": true, "settings.channel": true, "settings.staff": true,
"settings.assign_rule": true, "settings.customer_tag": true,
"settings.auto_reply": true, "settings.worktime": true, "settings.notification": true,
"permission.view": true, "permission.create_role": true,
"permission.delete_role": true, "permission.assign_role": true,
},
"supervisor": {
"session.view": true, "session.reply": true, "session.transfer": true,
"session.end": true, "session.note": true, "session.priority": true,
"customer.view": true, "customer.create": true, "customer.edit": true,
"customer.export": true, "customer.tag": true,
"chat_history.view": true, "chat_history.detail": true,
"chat_history.export": true, "chat_history.batch_archive": true,
"knowledge.view": true, "knowledge.create": true, "knowledge.edit": true,
"knowledge.publish": true, "knowledge.delete": true,
"quick_reply.view": true, "quick_reply.team_create": true,
"quick_reply.team_edit": true, "quick_reply.personal_manage": true,
"blacklist.view": true, "blacklist.create": true, "blacklist.delete": true,
"statistics.view": true, "statistics.export": true, "statistics.performance": true,
"settings.customer_tag": true, "settings.auto_reply": true,
},
"agent": {
"session.view": true, "session.reply": true,
"session.end": true, "session.note": true, "session.priority": true,
"customer.view": true, "customer.create": true, "customer.edit": true, "customer.tag": true,
"chat_history.view": true, "chat_history.detail": true,
"knowledge.view": true,
"quick_reply.view": true, "quick_reply.personal_manage": true,
"statistics.view": true, "statistics.performance": true,
},
}
if perms, ok := base[role]; ok {
return perms
}
return nil
}
type Context interface {
Get(key string) (value interface{}, exists bool)
Set(key string, value interface{})
}