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)的调用者直接放行。 // // 授权依据直接走 DB 强一致查询,而不是 caller 的 UD 缓存: // 超管刚把 caller 从高级降到中级时,如果 UD 缓存的 Clean 因为 Redis 抖动失败,caller 的缓存里还 // 是旧的高 MinPermsLevel;缓存 TTL 窗口内 caller 仍可凭旧级别批量分配超出当前权限的角色 // (审计 M-3 TOCTOU + 缓存失效延迟)。这里按"最小代价避开缓存"的原则,只在 assignment 决策点 // 打一条 FindMinPermsLevelByUserIdAndProductCode 走 NoCache 查询。 func GuardRoleLevelAssignable(ctx context.Context, svcCtx *svc.ServiceContext, caller *loaders.UserDetails, rolePermsLevel int64) error { if HasFullProductPerms(caller) { return nil } if caller == nil { return response.ErrForbidden("您没有可分配的角色等级") } freshLevel, err := svcCtx.SysRoleModel.FindMinPermsLevelByUserIdAndProductCode(ctx, caller.UserId, caller.ProductCode) if err != nil { if errors.Is(err, sqlx.ErrNotFound) { return response.ErrForbidden("您没有可分配的角色等级") } // 其他错误走 fail-close,避免 DB 抖动被同化为"无角色 = 最低级"放行超权分配。 return response.NewCodeError(500, "校验可分配角色等级失败,请稍后重试") } if rolePermsLevel <= freshLevel { 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 } // CheckAddMemberAccess 校验 caller 是否有权把 target 作为**新成员**拉进产品。与 CheckManageAccess 的 // 差异: // 1. 不做 memberType / permsLevel 比对——AddMember 时 target 还不是成员,checkPermLevel 的 // FindOneByProductCodeUserId 必定落空报 403,整个流程对产品 ADMIN 直接不可用; // 2. 对产品 ADMIN 也强制执行部门链校验(checkDeptHierarchy 的 ADMIN bypass 不生效),防止 // ADMIN 从部门树外 / HR / 财务把人强拉入自己的产品(见审计 H-3); // 3. SuperAdmin 仍完全豁免,自改自加场景由上层业务规则另行屏蔽。 // // 调用方应先走 RequireProductAdminFor 保证 caller 本身有"添加成员"的基线权限,再调这一步过部门链。 func CheckAddMemberAccess(ctx context.Context, svcCtx *svc.ServiceContext, target *userModel.SysUser) error { caller := middleware.GetUserDetails(ctx) if caller == nil { return response.ErrUnauthorized("未登录") } if caller.IsSuperAdmin { return nil } if target == nil { return response.ErrBadRequest("缺少目标用户信息") } if caller.UserId == target.Id { return nil } // 不走 checkDeptHierarchy 的 ADMIN 分支:ADMIN bypass 设计本意是"product ADMIN 对产品内既有 // 成员有全面管理权",但 AddMember 还没把 target 拉进产品,这里的 ADMIN bypass 会变成一个 // "随手拉部门树外的人进来"的漏洞(审计 H-3)。 if caller.DeptId == 0 || caller.DeptPath == "" { return response.ErrForbidden("您未归属任何部门,无权添加产品成员") } 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 } // 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 }