| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- package auth
- import (
- "context"
- "errors"
- "math"
- "strings"
- "perms-system-server/internal/consts"
- "perms-system-server/internal/loaders"
- "perms-system-server/internal/middleware"
- userModel "perms-system-server/internal/model/user"
- "perms-system-server/internal/response"
- "perms-system-server/internal/svc"
- "github.com/zeromicro/go-zero/core/stores/sqlx"
- )
- func memberTypePriority(memberType string) int {
- switch memberType {
- case consts.MemberTypeSuperAdmin:
- return 0
- case consts.MemberTypeAdmin:
- return 1
- case consts.MemberTypeDeveloper:
- return 2
- case consts.MemberTypeMember:
- return 3
- default:
- return math.MaxInt32
- }
- }
- // ManageAccessOption 给 CheckManageAccess 的可选参数;主要用来传递调用方已经拿到的目标用户对象,
- // 避免 checkDeptHierarchy 内部再做一次 FindOne(targetUserId)(见审计 M-5)。
- type ManageAccessOption func(*manageAccessOpts)
- type manageAccessOpts struct {
- prefetchedTarget *userModel.SysUser
- }
- // WithPrefetchedTarget 供调用方透传已获取的目标用户数据。仅在 target.Id == targetUserId 时有效,
- // 调用方负责保证一致性;不一致时该选项被忽略,回落到普通 FindOne。
- func WithPrefetchedTarget(target *userModel.SysUser) ManageAccessOption {
- return func(o *manageAccessOpts) {
- o.prefetchedTarget = target
- }
- }
- // CheckManageAccess 检查当前操作者是否有权管理目标用户。
- // 规则:
- // 1. SUPER_ADMIN 完全豁免
- // 2. 操作自己豁免
- // 3. 部门检查:目标用户须在操作者本部门或下级子部门(ADMIN 豁免)
- // 4. 权限级别检查:操作者的级别必须严格高于目标用户
- // - 先比 memberType 优先级(SUPER_ADMIN > ADMIN > DEVELOPER > MEMBER)
- // - 同 memberType 时比 permsLevel(数值越小权限越高)
- func CheckManageAccess(ctx context.Context, svcCtx *svc.ServiceContext, targetUserId int64, productCode string, opts ...ManageAccessOption) error {
- caller := middleware.GetUserDetails(ctx)
- if caller == nil {
- return response.ErrUnauthorized("未登录")
- }
- if caller.IsSuperAdmin {
- return nil
- }
- if caller.UserId == targetUserId {
- return nil
- }
- options := &manageAccessOpts{}
- for _, opt := range opts {
- opt(options)
- }
- prefetched := options.prefetchedTarget
- if prefetched != nil && prefetched.Id != targetUserId {
- prefetched = nil
- }
- if err := checkDeptHierarchy(ctx, svcCtx, caller, targetUserId, prefetched); err != nil {
- return err
- }
- return checkPermLevel(ctx, svcCtx, caller, targetUserId, productCode)
- }
- // CheckMemberTypeAssignment 检查操作者是否有权分配指定的 memberType。
- func CheckMemberTypeAssignment(ctx context.Context, assignedType string) error {
- caller := middleware.GetUserDetails(ctx)
- if caller == nil {
- return response.ErrUnauthorized("未登录")
- }
- if caller.IsSuperAdmin {
- return nil
- }
- if caller.MemberType == "" {
- return response.ErrForbidden("缺少产品成员上下文")
- }
- if memberTypePriority(caller.MemberType) >= memberTypePriority(assignedType) {
- return response.ErrForbidden("无权分配该成员类型,不能分配与自己同级或更高级别的类型")
- }
- return nil
- }
- // RequireSuperAdmin 要求当前操作者必须是超级管理员。
- func RequireSuperAdmin(ctx context.Context) error {
- caller := middleware.GetUserDetails(ctx)
- if caller == nil {
- return response.ErrUnauthorized("未登录")
- }
- if !caller.IsSuperAdmin {
- return response.ErrForbidden("仅超级管理员可执行此操作")
- }
- return nil
- }
- // RequireProductAdminFor 要求当前操作者是超级管理员或指定产品的管理员。
- func RequireProductAdminFor(ctx context.Context, targetProductCode string) error {
- caller := middleware.GetUserDetails(ctx)
- if caller == nil {
- return response.ErrUnauthorized("未登录")
- }
- if caller.IsSuperAdmin {
- return nil
- }
- if caller.MemberType == consts.MemberTypeAdmin && caller.ProductCode == targetProductCode {
- return nil
- }
- return response.ErrForbidden("仅超级管理员或该产品的管理员可执行此操作")
- }
- // GuardRoleLevelAssignable 校验调用者能否把 rolePermsLevel 这一等级的角色分配给他人。
- // 约束:"只能分配严格低于自身的等级"(数字更大 = 更低),与 checkPermLevel 的 ">=" 拦截口径对齐,
- // 避免调用者把下属拉到与自己平级后彻底失去管控(见审计 H-3)。
- // 拥有产品全权(SuperAdmin / ADMIN / DEVELOPER)的调用者直接放行。
- func GuardRoleLevelAssignable(caller *loaders.UserDetails, rolePermsLevel int64) error {
- if HasFullProductPerms(caller) {
- return nil
- }
- if caller == nil || caller.MinPermsLevel == math.MaxInt64 {
- return response.ErrForbidden("您没有可分配的角色等级")
- }
- if rolePermsLevel <= caller.MinPermsLevel {
- return response.ErrForbidden("不能分配权限级别高于自身的角色(含同级)")
- }
- return nil
- }
- // HasFullProductPerms 判断调用者是否拥有当前产品的全部权限(无需做 permsLevel 校验)。
- // SuperAdmin / ADMIN / DEVELOPER 均视为全权;loadPerms 对此三者走全权分支。
- // 所有依赖"调用者已拥有全权"的短路逻辑应复用此函数,变更只需改一处。
- func HasFullProductPerms(caller *loaders.UserDetails) bool {
- if caller == nil {
- return false
- }
- return caller.IsSuperAdmin ||
- caller.MemberType == consts.MemberTypeAdmin ||
- caller.MemberType == consts.MemberTypeDeveloper
- }
- // ValidateStatusChange 校验状态变更的合法性(不允许自改状态、不允许冻结超管)。
- // UpdateUser 和 UpdateUserStatus 共用此函数以确保校验逻辑一致。
- //
- // 返回校验通过时对应的目标用户对象,方便调用方透传给 CheckManageAccess 的 WithPrefetchedTarget
- // 选项和后续业务使用,避免同一请求内重复 FindOne(见审计 M-5)。
- func ValidateStatusChange(ctx context.Context, svcCtx *svc.ServiceContext, callerId, targetUserId int64) (*userModel.SysUser, error) {
- if callerId == targetUserId {
- return nil, response.ErrBadRequest("不能修改自己的状态")
- }
- target, err := svcCtx.SysUserModel.FindOne(ctx, targetUserId)
- if err != nil {
- return nil, response.ErrNotFound("用户不存在")
- }
- if target.IsSuperAdmin == consts.IsSuperAdminYes {
- return nil, response.ErrForbidden("不能修改超级管理员的状态")
- }
- return target, nil
- }
- func checkDeptHierarchy(ctx context.Context, svcCtx *svc.ServiceContext, caller *loaders.UserDetails, targetUserId int64, prefetchedTarget *userModel.SysUser) error {
- if caller.MemberType == consts.MemberTypeAdmin {
- return nil
- }
- // TODO(L-6): H-4 落地之后,新建 MEMBER/DEVELOPER 不会再出现 DeptId=0;但迁移/老数据里仍可能存在
- // "MemberType!=ADMIN 且 DeptId=0" 的幽灵账号,此处一律 403 会让这类账号失去任何管理能力
- // (包括原本可以由 checkPermLevel 通过的 product-admin-downward 操作)。运维应补一次 data fix
- // 把这类账号归入默认部门;若未来需要放宽,可在此允许"管理自己"或跳过部门链校验直接交由
- // checkPermLevel 判定。
- if caller.DeptId == 0 {
- return response.ErrForbidden("您未归属任何部门,无权管理其他用户")
- }
- if caller.DeptPath == "" {
- return response.ErrForbidden("您的部门信息异常,无法执行此操作")
- }
- target := prefetchedTarget
- if target == nil {
- t, err := svcCtx.SysUserModel.FindOne(ctx, targetUserId)
- if err != nil {
- return response.ErrNotFound("目标用户不存在")
- }
- target = t
- }
- if target.DeptId == 0 {
- return response.ErrForbidden("目标用户未归属部门,仅超管或产品管理员可管理")
- }
- targetDept, err := svcCtx.SysDeptModel.FindOne(ctx, target.DeptId)
- if err != nil {
- return response.ErrForbidden("无权操作")
- }
- if !strings.HasPrefix(targetDept.Path, caller.DeptPath) {
- return response.ErrForbidden("无权管理其他部门的用户")
- }
- return nil
- }
- func checkPermLevel(ctx context.Context, svcCtx *svc.ServiceContext, caller *loaders.UserDetails, targetUserId int64, productCode string) error {
- if productCode == "" {
- return response.ErrBadRequest("缺少产品上下文,无法进行权限级别判定")
- }
- targetMember, err := svcCtx.SysProductMemberModel.FindOneByProductCodeUserId(ctx, productCode, targetUserId)
- if err != nil {
- return response.ErrForbidden("目标用户不是当前产品的成员,无法执行管理操作")
- }
- targetMemberType := targetMember.MemberType
- callerPri := memberTypePriority(caller.MemberType)
- targetPri := memberTypePriority(targetMemberType)
- if callerPri > targetPri {
- return response.ErrForbidden("无权管理权限级别高于您的用户")
- }
- if callerPri < targetPri {
- return nil
- }
- // memberType 相同,比较 permsLevel
- targetLevel, err := svcCtx.SysRoleModel.FindMinPermsLevelByUserIdAndProductCode(ctx, targetUserId, productCode)
- if err != nil {
- // 区分"无角色 → 等价最低等级"与"DB 抖动 → 未知":只有 ErrNotFound 语义的场景才允许
- // 降级为 MaxInt64 放行管辖;其余错误一律视作不确定,fail-close 返回 500,避免 DB 抖动
- // 被同化成"目标无角色"造成越权放行(见审计 L-4)。
- if !errors.Is(err, sqlx.ErrNotFound) {
- return response.NewCodeError(500, "校验权限级别失败,请稍后重试")
- }
- targetLevel = math.MaxInt64
- }
- if caller.MinPermsLevel >= targetLevel {
- return response.ErrForbidden("无权管理权限级别高于或等于您的用户")
- }
- return nil
- }
|