完善角色权限与数据隔离

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
+457
View File
@@ -0,0 +1,457 @@
package handler
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"net/http"
"sort"
"strings"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"kefu-cloud/server/internal/middleware"
"kefu-cloud/server/internal/model"
)
type PermissionHandler struct{}
func NewPermissionHandler() *PermissionHandler { return &PermissionHandler{} }
func ensurePermissionData(tenantID uint) error {
if err := model.EnsurePermissions(); err != nil {
return err
}
return model.EnsureBuiltinRoles(tenantID)
}
// ListPermissions 获取所有系统权限码(全租户共享,供权限配置页使用)。
func (h *PermissionHandler) ListPermissions(c *gin.Context) {
if err := ensurePermissionData(middleware.GetTenantID(c)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "初始化权限数据失败"})
return
}
var permissions []model.Permission
if err := model.DB.Order("sort_order asc").Find(&permissions).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询失败"})
return
}
middleware.JSON(c, permissions)
}
// ListRoles 获取当前租户的角色列表(含成员数、权限数)。
func (h *PermissionHandler) ListRoles(c *gin.Context) {
tenantID := middleware.GetTenantID(c)
if err := ensurePermissionData(tenantID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "初始化权限数据失败"})
return
}
var roles []model.Role
if err := model.DB.Where("tenant_id = ?", tenantID).Order("id asc").Find(&roles).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询失败"})
return
}
type roleWithStats struct {
model.Role
MemberCount int64 `json:"member_count"`
PermCount int64 `json:"perm_count"`
}
result := make([]roleWithStats, len(roles))
for i, role := range roles {
result[i].Role = role
if err := model.DB.Model(&model.User{}).Where("tenant_id = ? AND role = ?", tenantID, role.Code).Count(&result[i].MemberCount).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "统计角色成员失败"})
return
}
if err := model.DB.Model(&model.RolePermission{}).Where("role_id = ?", role.ID).Count(&result[i].PermCount).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "统计角色权限失败"})
return
}
}
middleware.JSON(c, result)
}
type roleAccessSnapshot struct {
Permissions []string `json:"permissions"`
DataScopes map[string]string `json:"data_scopes"`
}
func loadRoleAccess(tx *gorm.DB, role model.Role) (roleAccessSnapshot, error) {
snapshot := roleAccessSnapshot{Permissions: []string{}, DataScopes: model.DefaultRoleDataScopes(role.Code)}
if err := tx.Table("role_permissions rp").
Joins("JOIN permissions p ON p.id = rp.permission_id").
Where("rp.role_id = ?", role.ID).
Order("p.sort_order asc").
Pluck("p.code", &snapshot.Permissions).Error; err != nil {
return snapshot, err
}
var scopes []model.RoleDataScope
if err := tx.Where("role_id = ?", role.ID).Find(&scopes).Error; err != nil {
return snapshot, err
}
for _, scope := range scopes {
snapshot.DataScopes[scope.Module] = scope.Scope
}
return snapshot, nil
}
// GetRole 获取角色详情(含权限码与数据范围)。
func (h *PermissionHandler) GetRole(c *gin.Context) {
tenantID := middleware.GetTenantID(c)
var role model.Role
if err := model.DB.Where("id = ? AND tenant_id = ?", c.Param("id"), tenantID).First(&role).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "角色不存在"})
return
}
access, err := loadRoleAccess(model.DB, role)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询角色权限失败"})
return
}
var memberCount int64
if err := model.DB.Model(&model.User{}).Where("tenant_id = ? AND role = ?", tenantID, role.Code).Count(&memberCount).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "统计角色成员失败"})
return
}
middleware.JSON(c, gin.H{
"role": role, "permissions": access.Permissions,
"data_scopes": access.DataScopes, "member_count": memberCount,
})
}
type saveRoleReq struct {
Name *string `json:"name"`
Desc *string `json:"desc"`
Permissions []string `json:"permissions"`
DataScopes map[string]string `json:"data_scopes"`
}
func validateRoleName(name string) (string, error) {
name = strings.TrimSpace(name)
if name == "" || len([]rune(name)) > 50 {
return "", errors.New("角色名称需 1-50 个字符")
}
return name, nil
}
func validateRoleAccess(tx *gorm.DB, permissions []string, dataScopes map[string]string) ([]model.Permission, error) {
uniqueCodes := make(map[string]struct{}, len(permissions))
for _, code := range permissions {
code = strings.TrimSpace(code)
if code != "" {
uniqueCodes[code] = struct{}{}
}
}
codes := make([]string, 0, len(uniqueCodes))
for code := range uniqueCodes {
codes = append(codes, code)
}
sort.Strings(codes)
var records []model.Permission
if len(codes) > 0 {
if err := tx.Where("code IN ?", codes).Order("sort_order asc").Find(&records).Error; err != nil {
return nil, err
}
if len(records) != len(codes) {
return nil, errors.New("包含无效权限码")
}
}
selected := make(map[string]bool, len(records))
moduleViews := make(map[string][]string)
for _, permission := range records {
selected[permission.Code] = true
}
var allPermissions []model.Permission
if err := tx.Order("sort_order asc").Find(&allPermissions).Error; err != nil {
return nil, err
}
for _, permission := range allPermissions {
if permission.Category == "view" {
moduleViews[permission.Module] = append(moduleViews[permission.Module], permission.Code)
}
}
for _, permission := range records {
if permission.Category != "operate" || len(moduleViews[permission.Module]) == 0 {
continue
}
hasView := false
for _, viewCode := range moduleViews[permission.Module] {
hasView = hasView || selected[viewCode]
}
if !hasView {
return nil, errors.New(permission.Module + "的操作权限必须同时启用查看权限")
}
}
allowedModules := make(map[string]bool)
for _, module := range model.DataScopeModules() {
allowedModules[module] = true
}
for module, scope := range dataScopes {
if !allowedModules[module] || (scope != model.DataScopeAll && scope != model.DataScopeSelf) {
return nil, errors.New("包含无效数据范围")
}
}
return records, nil
}
func saveRoleAccess(tx *gorm.DB, role model.Role, permissions []string, dataScopes map[string]string) error {
records, err := validateRoleAccess(tx, permissions, dataScopes)
if err != nil {
return err
}
if err := tx.Where("role_id = ?", role.ID).Delete(&model.RolePermission{}).Error; err != nil {
return err
}
for _, permission := range records {
if err := tx.Create(&model.RolePermission{RoleID: role.ID, PermissionID: permission.ID}).Error; err != nil {
return err
}
}
for module, scope := range dataScopes {
item := model.RoleDataScope{RoleID: role.ID, Module: module}
if err := tx.Where(item).Assign(model.RoleDataScope{Scope: scope}).FirstOrCreate(&item).Error; err != nil {
return err
}
}
// UpdatedAt 同时作为前端权限缓存版本,关联表变更时也必须推进。
return tx.Model(&role).UpdateColumn("updated_at", time.Now()).Error
}
func newCustomRoleCode() (string, error) {
random := make([]byte, 8)
if _, err := rand.Read(random); err != nil {
return "", err
}
return "custom_" + hex.EncodeToString(random), nil
}
// CreateRole 创建自定义角色。
func (h *PermissionHandler) CreateRole(c *gin.Context) {
tenantID := middleware.GetTenantID(c)
var req saveRoleReq
if err := c.ShouldBindJSON(&req); err != nil || req.Name == nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
return
}
name, err := validateRoleName(*req.Name)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
var duplicate int64
if err := model.DB.Model(&model.Role{}).Where("tenant_id = ? AND name = ?", tenantID, name).Count(&duplicate).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "检查角色名称失败"})
return
}
if duplicate > 0 {
c.JSON(http.StatusConflict, gin.H{"code": 409, "message": "角色名称已被占用"})
return
}
code, err := newCustomRoleCode()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "生成角色标识失败"})
return
}
desc := ""
if req.Desc != nil {
desc = strings.TrimSpace(*req.Desc)
}
role := model.Role{TenantID: tenantID, Name: name, Code: code, Type: "custom", Desc: desc}
dataScopes := req.DataScopes
if dataScopes == nil {
dataScopes = model.DefaultRoleDataScopes(role.Code)
}
if err := model.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&role).Error; err != nil {
return err
}
return saveRoleAccess(tx, role, req.Permissions, dataScopes)
}); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
middleware.InvalidatePermissionCache(tenantID, role.Code)
logPermissionChange(c, "create_role", role, roleAccessSnapshot{}, roleAccessSnapshot{Permissions: req.Permissions, DataScopes: dataScopes})
middleware.JSON(c, role)
}
// UpdateRole 更新自定义角色信息、权限与数据范围,角色标识保持不变。
func (h *PermissionHandler) UpdateRole(c *gin.Context) {
tenantID := middleware.GetTenantID(c)
var role model.Role
if err := model.DB.Where("id = ? AND tenant_id = ?", c.Param("id"), tenantID).First(&role).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "角色不存在"})
return
}
if role.Type == "builtin" {
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "内置角色请使用专用配置接口"})
return
}
var req saveRoleReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
return
}
before, err := loadRoleAccess(model.DB, role)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "读取原权限失败"})
return
}
if req.Permissions == nil {
req.Permissions = before.Permissions
}
if req.DataScopes == nil {
req.DataScopes = before.DataScopes
}
updates := map[string]interface{}{}
if req.Name != nil {
name, err := validateRoleName(*req.Name)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
var duplicate int64
model.DB.Model(&model.Role{}).Where("tenant_id = ? AND name = ? AND id <> ?", tenantID, name, role.ID).Count(&duplicate)
if duplicate > 0 {
c.JSON(http.StatusConflict, gin.H{"code": 409, "message": "角色名称已被占用"})
return
}
updates["name"] = name
}
if req.Desc != nil {
updates["desc"] = strings.TrimSpace(*req.Desc)
}
if err := model.DB.Transaction(func(tx *gorm.DB) error {
if len(updates) > 0 {
if err := tx.Model(&role).Updates(updates).Error; err != nil {
return err
}
}
return saveRoleAccess(tx, role, req.Permissions, req.DataScopes)
}); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
model.DB.First(&role, role.ID)
after, _ := loadRoleAccess(model.DB, role)
middleware.InvalidatePermissionCache(tenantID, role.Code)
logPermissionChange(c, "update_role", role, before, after)
middleware.JSON(c, role)
}
// UpdateBuiltinRole 更新内置角色权限与数据范围,不允许清空权限或修改标识。
func (h *PermissionHandler) UpdateBuiltinRole(c *gin.Context) {
tenantID := middleware.GetTenantID(c)
var role model.Role
if err := model.DB.Where("id = ? AND tenant_id = ?", c.Param("id"), tenantID).First(&role).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "角色不存在"})
return
}
if role.Type != "builtin" {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "仅内置角色可使用此接口"})
return
}
var req saveRoleReq
if err := c.ShouldBindJSON(&req); err != nil || len(req.Permissions) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "内置角色至少需要一项权限"})
return
}
before, err := loadRoleAccess(model.DB, role)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "读取原权限失败"})
return
}
if req.DataScopes == nil {
req.DataScopes = before.DataScopes
}
if role.Code == "admin" {
required := map[string]bool{
"permission.view": false, "permission.create_role": false,
"permission.delete_role": false, "permission.assign_role": false,
}
for _, code := range req.Permissions {
if _, exists := required[code]; exists {
required[code] = true
}
}
for _, enabled := range required {
if !enabled {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "管理员必须保留全部权限控制权限"})
return
}
}
for _, module := range model.DataScopeModules() {
req.DataScopes[module] = model.DataScopeAll
}
}
if err := model.DB.Transaction(func(tx *gorm.DB) error {
if req.Desc != nil {
if err := tx.Model(&role).Update("desc", strings.TrimSpace(*req.Desc)).Error; err != nil {
return err
}
}
return saveRoleAccess(tx, role, req.Permissions, req.DataScopes)
}); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
model.DB.First(&role, role.ID)
after, _ := loadRoleAccess(model.DB, role)
middleware.InvalidatePermissionCache(tenantID, role.Code)
logPermissionChange(c, "update_builtin_role", role, before, after)
middleware.JSON(c, role)
}
// DeleteRole 删除无成员绑定的自定义角色。
func (h *PermissionHandler) DeleteRole(c *gin.Context) {
tenantID := middleware.GetTenantID(c)
var role model.Role
if err := model.DB.Where("id = ? AND tenant_id = ?", c.Param("id"), tenantID).First(&role).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "角色不存在"})
return
}
if role.Type == "builtin" {
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "内置角色不可删除"})
return
}
var memberCount int64
if err := model.DB.Model(&model.User{}).Where("tenant_id = ? AND role = ?", tenantID, role.Code).Count(&memberCount).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "检查角色成员失败"})
return
}
if memberCount > 0 {
c.JSON(http.StatusConflict, gin.H{"code": 409, "message": "请先为该角色下的成员更换角色", "member_count": memberCount})
return
}
before, _ := loadRoleAccess(model.DB, role)
if err := model.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("role_id = ?", role.ID).Delete(&model.RolePermission{}).Error; err != nil {
return err
}
if err := tx.Where("role_id = ?", role.ID).Delete(&model.RoleDataScope{}).Error; err != nil {
return err
}
return tx.Delete(&role).Error
}); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "删除失败"})
return
}
middleware.InvalidatePermissionCache(tenantID, role.Code)
logPermissionChange(c, "delete_role", role, before, roleAccessSnapshot{})
middleware.JSON(c, gin.H{"message": "已删除"})
}
func logPermissionChange(c *gin.Context, action string, role model.Role, before, after roleAccessSnapshot) {
detail, _ := json.Marshal(gin.H{
"role": role.Name, "code": role.Code, "before": before, "after": after,
})
model.DB.Create(&model.OperationLog{
OperatorID: middleware.GetUserID(c), Action: action, Detail: string(detail),
TargetType: "role", TargetID: &role.ID, IP: c.ClientIP(),
})
}