refactor: 数据权限过滤从 GORM Callback 改为 Store 层显式调用
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m2s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m2s
- 移除 RegisterDataPermissionCallback 和 SkipDataPermission 机制 - 在 Auth 中间件预计算 SubordinateShopIDs 并注入 Context - 新增 ApplyShopFilter/ApplyEnterpriseFilter/ApplyOwnerShopFilter 等 Helper 函数 - 所有 Store 层查询方法显式调用数据权限过滤函数 - 权限检查函数 CanManageShop/CanManageEnterprise 改为从 Context 获取数据 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,16 +5,19 @@ import (
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/logger"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// UserContextInfo 用户上下文信息
|
||||
type UserContextInfo struct {
|
||||
UserID uint
|
||||
UserType int
|
||||
ShopID uint
|
||||
EnterpriseID uint
|
||||
CustomerID uint
|
||||
UserID uint
|
||||
UserType int
|
||||
ShopID uint
|
||||
EnterpriseID uint
|
||||
CustomerID uint
|
||||
SubordinateShopIDs []uint // 代理用户的下级店铺ID列表,nil 表示不受数据权限限制
|
||||
}
|
||||
|
||||
// SetUserContext 将用户信息设置到 context 中
|
||||
@@ -25,6 +28,10 @@ func SetUserContext(ctx context.Context, info *UserContextInfo) context.Context
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyShopID, info.ShopID)
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyEnterpriseID, info.EnterpriseID)
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyCustomerID, info.CustomerID)
|
||||
// SubordinateShopIDs: nil 表示不限制,空切片表示无权限
|
||||
if info.SubordinateShopIDs != nil {
|
||||
ctx = context.WithValue(ctx, constants.ContextKeySubordinateShopIDs, info.SubordinateShopIDs)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -134,12 +141,21 @@ func SetUserToFiberContext(c *fiber.Ctx, info *UserContextInfo) {
|
||||
c.Locals(constants.ContextKeyShopID, info.ShopID)
|
||||
c.Locals(constants.ContextKeyEnterpriseID, info.EnterpriseID)
|
||||
c.Locals(constants.ContextKeyCustomerID, info.CustomerID)
|
||||
if info.SubordinateShopIDs != nil {
|
||||
c.Locals(constants.ContextKeySubordinateShopIDs, info.SubordinateShopIDs)
|
||||
}
|
||||
|
||||
// 设置到标准 context(用于 GORM 数据权限过滤)
|
||||
// 设置到标准 context(用于数据权限过滤)
|
||||
ctx := SetUserContext(c.UserContext(), info)
|
||||
c.SetUserContext(ctx)
|
||||
}
|
||||
|
||||
// AuthShopStoreInterface 店铺存储接口
|
||||
// 用于 Auth 中间件获取下级店铺 ID,避免循环依赖
|
||||
type AuthShopStoreInterface interface {
|
||||
GetSubordinateShopIDs(ctx context.Context, shopID uint) ([]uint, error)
|
||||
}
|
||||
|
||||
// AuthConfig Auth 中间件配置
|
||||
type AuthConfig struct {
|
||||
// TokenExtractor 自定义 token 提取函数
|
||||
@@ -153,6 +169,10 @@ type AuthConfig struct {
|
||||
|
||||
// SkipPaths 跳过认证的路径列表
|
||||
SkipPaths []string
|
||||
|
||||
// ShopStore 店铺存储,用于预计算代理用户的下级店铺 ID
|
||||
// 可选,不传则不预计算 SubordinateShopIDs
|
||||
ShopStore AuthShopStoreInterface
|
||||
}
|
||||
|
||||
// Auth 认证中间件
|
||||
@@ -196,6 +216,21 @@ func Auth(config AuthConfig) fiber.Handler {
|
||||
return errors.Wrap(errors.CodeInvalidToken, err, "认证令牌无效")
|
||||
}
|
||||
|
||||
// 预计算代理用户的下级店铺 ID
|
||||
if config.ShopStore != nil &&
|
||||
userInfo.UserType == constants.UserTypeAgent &&
|
||||
userInfo.ShopID > 0 {
|
||||
shopIDs, err := config.ShopStore.GetSubordinateShopIDs(c.UserContext(), userInfo.ShopID)
|
||||
if err != nil {
|
||||
// 降级处理:只包含自己的店铺 ID
|
||||
shopIDs = []uint{userInfo.ShopID}
|
||||
logger.GetAppLogger().Warn("预计算下级店铺失败,降级为只包含自己",
|
||||
zap.Uint("shop_id", userInfo.ShopID),
|
||||
zap.Error(err))
|
||||
}
|
||||
userInfo.SubordinateShopIDs = shopIDs
|
||||
}
|
||||
|
||||
// 将用户信息设置到 context
|
||||
SetUserToFiberContext(c, userInfo)
|
||||
|
||||
|
||||
91
pkg/middleware/data_scope.go
Normal file
91
pkg/middleware/data_scope.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GetSubordinateShopIDs 获取当前用户可管理的店铺ID列表
|
||||
// 返回 nil 表示不受数据权限限制(平台用户/超管)
|
||||
// 返回 []uint 表示限制在这些店铺范围内(代理用户)
|
||||
func GetSubordinateShopIDs(ctx context.Context) []uint {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
if ids, ok := ctx.Value(constants.ContextKeySubordinateShopIDs).([]uint); ok {
|
||||
return ids
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyShopFilter 应用店铺数据权限过滤
|
||||
// 平台用户/超管:不添加条件(SubordinateShopIDs 为 nil)
|
||||
// 代理用户:WHERE shop_id IN (subordinateShopIDs)
|
||||
// 注意:NULL shop_id 的记录对代理用户不可见
|
||||
func ApplyShopFilter(ctx context.Context, query *gorm.DB) *gorm.DB {
|
||||
shopIDs := GetSubordinateShopIDs(ctx)
|
||||
if shopIDs == nil {
|
||||
return query
|
||||
}
|
||||
return query.Where("shop_id IN ?", shopIDs)
|
||||
}
|
||||
|
||||
// ApplyEnterpriseFilter 应用企业数据权限过滤
|
||||
// 非企业用户:不添加条件
|
||||
// 企业用户:WHERE enterprise_id = ?
|
||||
func ApplyEnterpriseFilter(ctx context.Context, query *gorm.DB) *gorm.DB {
|
||||
userType := GetUserTypeFromContext(ctx)
|
||||
if userType != constants.UserTypeEnterprise {
|
||||
return query
|
||||
}
|
||||
enterpriseID := GetEnterpriseIDFromContext(ctx)
|
||||
if enterpriseID == 0 {
|
||||
// 企业用户但无企业ID,返回空结果
|
||||
return query.Where("1 = 0")
|
||||
}
|
||||
return query.Where("enterprise_id = ?", enterpriseID)
|
||||
}
|
||||
|
||||
// ApplyOwnerShopFilter 应用归属店铺数据权限过滤
|
||||
// 用于 Enterprise 等使用 owner_shop_id 字段的表
|
||||
// 平台用户/超管:不添加条件
|
||||
// 代理用户:WHERE owner_shop_id IN (subordinateShopIDs)
|
||||
func ApplyOwnerShopFilter(ctx context.Context, query *gorm.DB) *gorm.DB {
|
||||
shopIDs := GetSubordinateShopIDs(ctx)
|
||||
if shopIDs == nil {
|
||||
return query
|
||||
}
|
||||
return query.Where("owner_shop_id IN ?", shopIDs)
|
||||
}
|
||||
|
||||
// IsUnrestricted 检查当前用户是否不受数据权限限制
|
||||
// 平台用户/超管返回 true,代理/企业用户返回 false
|
||||
func IsUnrestricted(ctx context.Context) bool {
|
||||
return GetSubordinateShopIDs(ctx) == nil
|
||||
}
|
||||
|
||||
// ApplySellerShopFilter 应用销售店铺数据权限过滤
|
||||
// 用于 Order 等使用 seller_shop_id 字段的表
|
||||
// 平台用户/超管:不添加条件
|
||||
// 代理用户:WHERE seller_shop_id IN (subordinateShopIDs)
|
||||
func ApplySellerShopFilter(ctx context.Context, query *gorm.DB) *gorm.DB {
|
||||
shopIDs := GetSubordinateShopIDs(ctx)
|
||||
if shopIDs == nil {
|
||||
return query
|
||||
}
|
||||
return query.Where("seller_shop_id IN ?", shopIDs)
|
||||
}
|
||||
|
||||
// ApplyShopTagFilter 应用店铺标签数据权限过滤
|
||||
// 用于 CardWallet 等使用 shop_id_tag 字段的表
|
||||
// 平台用户/超管:不添加条件
|
||||
// 代理用户:WHERE shop_id_tag IN (subordinateShopIDs)
|
||||
func ApplyShopTagFilter(ctx context.Context, query *gorm.DB) *gorm.DB {
|
||||
shopIDs := GetSubordinateShopIDs(ctx)
|
||||
if shopIDs == nil {
|
||||
return query
|
||||
}
|
||||
return query.Where("shop_id_tag IN ?", shopIDs)
|
||||
}
|
||||
@@ -2,20 +2,13 @@ package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// ShopStoreInterface 店铺存储接口
|
||||
// 用于权限检查时查询店铺信息和下级店铺ID
|
||||
type ShopStoreInterface interface {
|
||||
GetByID(ctx context.Context, id uint) (*model.Shop, error)
|
||||
GetByIDs(ctx context.Context, ids []uint) ([]*model.Shop, error)
|
||||
GetSubordinateShopIDs(ctx context.Context, shopID uint) ([]uint, error)
|
||||
}
|
||||
|
||||
// EnterpriseStoreInterface 企业存储接口
|
||||
// 用于权限检查时查询企业信息
|
||||
type EnterpriseStoreInterface interface {
|
||||
@@ -23,91 +16,80 @@ type EnterpriseStoreInterface interface {
|
||||
GetByIDs(ctx context.Context, ids []uint) ([]*model.Enterprise, error)
|
||||
}
|
||||
|
||||
// CanManageShop 检查当前用户是否有权管理目标店铺的账号
|
||||
// 超级管理员和平台用户自动通过
|
||||
// 代理账号只能管理自己店铺及下级店铺的账号
|
||||
// 企业账号禁止管理店铺账号
|
||||
func CanManageShop(ctx context.Context, targetShopID uint, shopStore ShopStoreInterface) error {
|
||||
// CanManageShop 检查当前用户是否有权管理目标店铺
|
||||
// 超级管理员和平台用户自动通过(SubordinateShopIDs 为 nil)
|
||||
// 代理账号只能管理自己店铺及下级店铺
|
||||
// 企业账号禁止管理店铺
|
||||
func CanManageShop(ctx context.Context, targetShopID uint) error {
|
||||
userType := GetUserTypeFromContext(ctx)
|
||||
|
||||
// 超级管理员和平台用户跳过权限检查
|
||||
if userType == constants.UserTypeSuperAdmin || userType == constants.UserTypePlatform {
|
||||
// 企业账号禁止管理店铺
|
||||
if userType == constants.UserTypeEnterprise {
|
||||
return errors.New(errors.CodeForbidden, "无权限管理店铺")
|
||||
}
|
||||
|
||||
// 从 Context 获取预计算的下级店铺 ID 列表
|
||||
subordinateIDs := GetSubordinateShopIDs(ctx)
|
||||
|
||||
// nil 表示不受限制(超级管理员/平台用户)
|
||||
if subordinateIDs == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 企业账号禁止管理店铺账号
|
||||
if userType != constants.UserTypeAgent {
|
||||
return errors.New(errors.CodeForbidden, "无权限管理店铺账号")
|
||||
}
|
||||
|
||||
// 获取当前代理账号的店铺ID
|
||||
currentShopID := GetShopIDFromContext(ctx)
|
||||
if currentShopID == 0 {
|
||||
return errors.New(errors.CodeForbidden, "无权限管理店铺账号")
|
||||
}
|
||||
|
||||
// 递归查询下级店铺ID(包含自己)
|
||||
subordinateIDs, err := shopStore.GetSubordinateShopIDs(ctx, currentShopID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "查询下级店铺失败")
|
||||
}
|
||||
|
||||
// 检查目标店铺是否在下级列表中
|
||||
for _, id := range subordinateIDs {
|
||||
if id == targetShopID {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New(errors.CodeForbidden, "无权限管理该店铺的账号")
|
||||
}
|
||||
|
||||
// CanManageEnterprise 检查当前用户是否有权管理目标企业的账号
|
||||
// 超级管理员和平台用户自动通过
|
||||
// 代理账号只能管理归属于自己店铺或下级店铺的企业账号
|
||||
// 企业账号禁止管理其他企业账号
|
||||
func CanManageEnterprise(ctx context.Context, targetEnterpriseID uint, enterpriseStore EnterpriseStoreInterface, shopStore ShopStoreInterface) error {
|
||||
userType := GetUserTypeFromContext(ctx)
|
||||
|
||||
// 超级管理员和平台用户跳过权限检查
|
||||
if userType == constants.UserTypeSuperAdmin || userType == constants.UserTypePlatform {
|
||||
if slices.Contains(subordinateIDs, targetShopID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 企业账号禁止管理其他企业账号
|
||||
if userType != constants.UserTypeAgent {
|
||||
return errors.New(errors.CodeForbidden, "无权限管理企业账号")
|
||||
return errors.New(errors.CodeForbidden, "无权限管理该店铺")
|
||||
}
|
||||
|
||||
// CanManageEnterprise 检查当前用户是否有权管理目标企业
|
||||
// 超级管理员和平台用户自动通过(SubordinateShopIDs 为 nil)
|
||||
// 代理账号只能管理归属于自己店铺或下级店铺的企业
|
||||
// 企业账号禁止管理其他企业
|
||||
func CanManageEnterprise(ctx context.Context, targetEnterpriseID uint, enterpriseStore EnterpriseStoreInterface) error {
|
||||
userType := GetUserTypeFromContext(ctx)
|
||||
|
||||
// 企业账号禁止管理其他企业
|
||||
if userType == constants.UserTypeEnterprise {
|
||||
return errors.New(errors.CodeForbidden, "无权限管理企业")
|
||||
}
|
||||
|
||||
// 从 Context 获取预计算的下级店铺 ID 列表
|
||||
subordinateIDs := GetSubordinateShopIDs(ctx)
|
||||
|
||||
// nil 表示不受限制(超级管理员/平台用户)
|
||||
if subordinateIDs == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取目标企业信息
|
||||
enterprise, err := enterpriseStore.GetByID(ctx, targetEnterpriseID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeForbidden, err, "无权限操作该资源或资源不存在")
|
||||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
|
||||
// 代理账号不能管理平台级企业(owner_shop_id为NULL)
|
||||
// 代理账号不能管理平台级企业(owner_shop_id 为 NULL)
|
||||
if enterprise.OwnerShopID == nil {
|
||||
return errors.New(errors.CodeForbidden, "无权限管理平台级企业账号")
|
||||
}
|
||||
|
||||
// 获取当前代理账号的店铺ID
|
||||
currentShopID := GetShopIDFromContext(ctx)
|
||||
if currentShopID == 0 {
|
||||
return errors.New(errors.CodeForbidden, "无权限管理企业账号")
|
||||
}
|
||||
|
||||
// 递归查询下级店铺ID(包含自己)
|
||||
subordinateIDs, err := shopStore.GetSubordinateShopIDs(ctx, currentShopID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "查询下级店铺失败")
|
||||
return errors.New(errors.CodeForbidden, "无权限管理平台级企业")
|
||||
}
|
||||
|
||||
// 检查企业归属的店铺是否在下级列表中
|
||||
for _, id := range subordinateIDs {
|
||||
if id == *enterprise.OwnerShopID {
|
||||
return nil
|
||||
}
|
||||
if slices.Contains(subordinateIDs, *enterprise.OwnerShopID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New(errors.CodeForbidden, "无权限管理该企业的账号")
|
||||
return errors.New(errors.CodeForbidden, "无权限管理该企业")
|
||||
}
|
||||
|
||||
// ContainsShopID 检查目标店铺 ID 是否在当前用户可管理的店铺列表中
|
||||
// 平台用户/超管返回 true(不受限制)
|
||||
// 代理用户检查是否在 SubordinateShopIDs 中
|
||||
func ContainsShopID(ctx context.Context, targetShopID uint) bool {
|
||||
subordinateIDs := GetSubordinateShopIDs(ctx)
|
||||
if subordinateIDs == nil {
|
||||
return true // 不受限制
|
||||
}
|
||||
return slices.Contains(subordinateIDs, targetShopID)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user