完善角色权限与数据隔离
This commit is contained in:
@@ -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{})
|
||||
}
|
||||
Reference in New Issue
Block a user