324 lines
10 KiB
Go
324 lines
10 KiB
Go
// Package rbac 提供后台 RBAC 的权限聚合、菜单树组装、缓存与种子初始化,
|
|
// 以及管理员 / 角色 / 权限(接口、菜单)的 CRUD 业务。
|
|
//
|
|
// 依赖方向:handler → 本包 service → repository/admin → model/admin。
|
|
package rbac
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"portal-mini-go/internal/model/admin"
|
|
"portal-mini-go/internal/pkg/auth"
|
|
"portal-mini-go/internal/pkg/errors"
|
|
adminrepo "portal-mini-go/internal/repository/admin"
|
|
)
|
|
|
|
// UserRepository 是 Provider 与管理员业务所需的用户仓储接口(由 repository/admin 实现)。
|
|
type UserRepository interface {
|
|
GetByUsername(ctx context.Context, username string) (*admin.User, error)
|
|
GetByID(ctx context.Context, id uint64) (*admin.User, error)
|
|
Create(ctx context.Context, u *admin.User) error
|
|
Count(ctx context.Context) (int64, error)
|
|
List(ctx context.Context, f adminrepo.UserListFilter) ([]admin.User, int64, error)
|
|
Update(ctx context.Context, id uint64, cols map[string]interface{}) error
|
|
Delete(ctx context.Context, id uint64) error
|
|
SetRoles(ctx context.Context, adminID uint64, roleIDs []uint64) error
|
|
RoleCodesOf(ctx context.Context, adminID uint64) ([]string, error)
|
|
PermissionCodesOf(ctx context.Context, adminID uint64) ([]string, error)
|
|
}
|
|
|
|
// RoleRepository 是 Provider 与角色业务所需的角色仓储接口。
|
|
type RoleRepository interface {
|
|
GetByID(ctx context.Context, id uint64) (*admin.Role, error)
|
|
GetByCode(ctx context.Context, code string) (*admin.Role, error)
|
|
Create(ctx context.Context, m *admin.Role) error
|
|
List(ctx context.Context, f adminrepo.RoleListFilter) ([]admin.Role, int64, error)
|
|
Update(ctx context.Context, id uint64, cols map[string]interface{}) error
|
|
Delete(ctx context.Context, id uint64) error
|
|
SetPermissions(ctx context.Context, roleID uint64, permIDs []uint64) error
|
|
PermissionCodesOf(ctx context.Context, roleID uint64) ([]string, error)
|
|
}
|
|
|
|
// PermissionRepository 是 Provider 与权限业务所需的权限仓储接口。
|
|
type PermissionRepository interface {
|
|
GetByID(ctx context.Context, id uint64) (*admin.Permission, error)
|
|
Create(ctx context.Context, m *admin.Permission) error
|
|
List(ctx context.Context, f adminrepo.PermListFilter) ([]admin.Permission, int64, error)
|
|
Update(ctx context.Context, id uint64, cols map[string]interface{}) error
|
|
Delete(ctx context.Context, id uint64) error
|
|
AllMenuPermissions(ctx context.Context) ([]admin.Permission, error)
|
|
AllCodes(ctx context.Context) ([]string, error)
|
|
}
|
|
|
|
// CacheInvalidator 用于写操作后失效权限缓存,由 Provider 实现并被各业务服务依赖。
|
|
type CacheInvalidator interface {
|
|
Invalidate(ctx context.Context, adminID uint64)
|
|
InvalidateAll(ctx context.Context)
|
|
}
|
|
|
|
// PermissionCache 是管理员权限集的缓存抽象。默认实现为内存版;
|
|
// 生产环境若接入 Redis,可实现该接口替换(见 NewRedisCache 预留说明)。
|
|
type PermissionCache interface {
|
|
Get(ctx context.Context, adminID uint64) (roles, perms []string, ok bool)
|
|
Set(ctx context.Context, adminID uint64, roles, perms []string)
|
|
Invalidate(ctx context.Context, adminID uint64)
|
|
InvalidateAll(ctx context.Context)
|
|
}
|
|
|
|
// Provider 聚合管理员权限数据,实现 middleware.PermissionProvider。
|
|
type Provider struct {
|
|
userRepo UserRepository
|
|
roleRepo RoleRepository
|
|
permRepo PermissionRepository
|
|
cache PermissionCache
|
|
}
|
|
|
|
// NewProvider 构造权限 Provider。
|
|
func NewProvider(
|
|
userRepo UserRepository,
|
|
roleRepo RoleRepository,
|
|
permRepo PermissionRepository,
|
|
cache PermissionCache,
|
|
) *Provider {
|
|
return &Provider{userRepo: userRepo, roleRepo: roleRepo, permRepo: permRepo, cache: cache}
|
|
}
|
|
|
|
// Load 返回管理员的角色与权限 code 集合,优先读缓存。
|
|
// 若含超级管理员角色,则权限集为全部权限 code(短路放行)。
|
|
func (p *Provider) Load(ctx context.Context, adminID uint64) (roles, perms []string, err error) {
|
|
if r, pp, ok := p.cache.Get(ctx, adminID); ok {
|
|
return r, pp, nil
|
|
}
|
|
roles, err = p.userRepo.RoleCodesOf(ctx, adminID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if contains(roles, admin.SuperAdminRoleCode) {
|
|
all, e := p.permRepo.AllCodes(ctx)
|
|
if e != nil {
|
|
return nil, nil, e
|
|
}
|
|
perms = all
|
|
} else {
|
|
perms, err = p.userRepo.PermissionCodesOf(ctx, adminID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
p.cache.Set(ctx, adminID, roles, perms)
|
|
return roles, perms, nil
|
|
}
|
|
|
|
// RolesOf 返回管理员角色 code 列表。
|
|
func (p *Provider) RolesOf(ctx context.Context, adminID uint64) ([]string, error) {
|
|
roles, _, err := p.Load(ctx, adminID)
|
|
return roles, err
|
|
}
|
|
|
|
// PermissionsOf 返回管理员权限 code 列表。
|
|
func (p *Provider) PermissionsOf(ctx context.Context, adminID uint64) ([]string, error) {
|
|
_, perms, err := p.Load(ctx, adminID)
|
|
return perms, err
|
|
}
|
|
|
|
// MenuTree 返回该管理员可见的菜单树(按权限过滤,超级管理员返回完整树)。
|
|
func (p *Provider) MenuTree(ctx context.Context, adminID uint64) ([]admin.MenuNode, error) {
|
|
roles, perms, err := p.Load(ctx, adminID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
menus, err := p.permRepo.AllMenuPermissions(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
allowed := make(map[string]struct{}, len(perms))
|
|
for _, c := range perms {
|
|
allowed[c] = struct{}{}
|
|
}
|
|
return buildMenuTree(menus, contains(roles, admin.SuperAdminRoleCode), allowed), nil
|
|
}
|
|
|
|
// Invalidate 失效指定管理员的权限缓存。
|
|
func (p *Provider) Invalidate(ctx context.Context, adminID uint64) { p.cache.Invalidate(ctx, adminID) }
|
|
|
|
// InvalidateAll 失效全部管理员的权限缓存(角色 / 权限变更后调用)。
|
|
func (p *Provider) InvalidateAll(ctx context.Context) { p.cache.InvalidateAll(ctx) }
|
|
|
|
// Seed 初始化超级管理员角色与首个管理员账号(若表为空)。
|
|
// defaultUser / defaultPwd 来自环境变量,缺失时使用安全默认值。
|
|
func (p *Provider) Seed(ctx context.Context, defaultUser, defaultPwd string) error {
|
|
if _, err := p.roleRepo.GetByCode(ctx, admin.SuperAdminRoleCode); err != nil {
|
|
if !errors.Is(err, errors.ErrNotFound) {
|
|
return err
|
|
}
|
|
if err := p.roleRepo.Create(ctx, &admin.Role{
|
|
Code: admin.SuperAdminRoleCode,
|
|
Name: "超级管理员",
|
|
Status: admin.UserStatusEnabled,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
n, err := p.userRepo.Count(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n > 0 {
|
|
return nil
|
|
}
|
|
hash, err := auth.HashPassword(defaultPwd)
|
|
if err != nil {
|
|
return errors.Wrap(50000, "hash seed password", err)
|
|
}
|
|
u := &admin.User{
|
|
Username: defaultUser,
|
|
PasswordHash: hash,
|
|
Nickname: "超级管理员",
|
|
Status: admin.UserStatusEnabled,
|
|
}
|
|
if err := p.userRepo.Create(ctx, u); err != nil {
|
|
return err
|
|
}
|
|
role, err := p.roleRepo.GetByCode(ctx, admin.SuperAdminRoleCode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return p.userRepo.SetRoles(ctx, u.ID, []uint64{role.ID})
|
|
}
|
|
|
|
// buildMenuTree 依据菜单权限列表与管理员可见集合组装树。
|
|
// 节点被保留的条件:自身 code 在 allowed 中(超级管理员恒为真),或存在被保留的后代。
|
|
func buildMenuTree(menus []admin.Permission, isSuper bool, allowed map[string]struct{}) []admin.MenuNode {
|
|
byID := make(map[uint64]admin.Permission, len(menus))
|
|
childrenOf := make(map[uint64][]uint64)
|
|
for _, m := range menus {
|
|
byID[m.ID] = m
|
|
childrenOf[m.ParentID] = append(childrenOf[m.ParentID], m.ID)
|
|
}
|
|
kept := make(map[uint64]bool, len(menus))
|
|
var mark func(id uint64) bool
|
|
mark = func(id uint64) bool {
|
|
if v, ok := kept[id]; ok {
|
|
return v
|
|
}
|
|
p := byID[id]
|
|
self := isSuper
|
|
if !isSuper {
|
|
if _, ok := allowed[p.Code]; ok {
|
|
self = true
|
|
}
|
|
}
|
|
childKept := false
|
|
for _, cid := range childrenOf[id] {
|
|
if mark(cid) {
|
|
childKept = true
|
|
}
|
|
}
|
|
keep := self || childKept
|
|
kept[id] = keep
|
|
return keep
|
|
}
|
|
for id := range byID {
|
|
mark(id)
|
|
}
|
|
|
|
var build func(parentID uint64) []admin.MenuNode
|
|
build = func(parentID uint64) []admin.MenuNode {
|
|
ids := childrenOf[parentID]
|
|
out := make([]admin.MenuNode, 0, len(ids))
|
|
for _, id := range ids {
|
|
if !kept[id] {
|
|
continue
|
|
}
|
|
node := admin.MenuNode{Permission: byID[id]}
|
|
node.Children = build(id)
|
|
out = append(out, node)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Sort < out[j].Sort })
|
|
return out
|
|
}
|
|
return build(0)
|
|
}
|
|
|
|
func contains(s []string, target string) bool {
|
|
for _, v := range s {
|
|
if v == target {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// MemoryCache 进程内权限缓存,适用于无 Redis 的本地 / 测试环境。
|
|
// 多实例部署时各进程不共享,仅供单实例使用;生产可替换为 RedisCache。
|
|
type MemoryCache struct {
|
|
mu sync.RWMutex
|
|
data map[uint64]memItem
|
|
ttl time.Duration
|
|
now func() time.Time
|
|
}
|
|
|
|
type memItem struct {
|
|
roles []string
|
|
perms []string
|
|
expireAt time.Time
|
|
}
|
|
|
|
// NewMemoryCache 创建内存权限缓存,ttl 为兜底过期时间(0 表示永不过期,仅靠失效)。
|
|
func NewMemoryCache(ttl time.Duration) *MemoryCache {
|
|
return &MemoryCache{data: make(map[uint64]memItem), ttl: ttl, now: time.Now}
|
|
}
|
|
|
|
// Get 读取缓存的权限集。
|
|
func (m *MemoryCache) Get(_ context.Context, adminID uint64) (roles, perms []string, ok bool) {
|
|
m.mu.RLock()
|
|
e, found := m.data[adminID]
|
|
m.mu.RUnlock()
|
|
if !found {
|
|
return nil, nil, false
|
|
}
|
|
if m.ttl > 0 && m.now().After(e.expireAt) {
|
|
m.mu.Lock()
|
|
delete(m.data, adminID)
|
|
m.mu.Unlock()
|
|
return nil, nil, false
|
|
}
|
|
return e.roles, e.perms, true
|
|
}
|
|
|
|
// Set 写入权限集。
|
|
func (m *MemoryCache) Set(_ context.Context, adminID uint64, roles, perms []string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
e := memItem{roles: roles, perms: perms}
|
|
if m.ttl > 0 {
|
|
e.expireAt = m.now().Add(m.ttl)
|
|
}
|
|
m.data[adminID] = e
|
|
}
|
|
|
|
// Invalidate 失效单个管理员缓存。
|
|
func (m *MemoryCache) Invalidate(_ context.Context, adminID uint64) {
|
|
m.mu.Lock()
|
|
delete(m.data, adminID)
|
|
m.mu.Unlock()
|
|
}
|
|
|
|
// InvalidateAll 清空全部缓存。
|
|
func (m *MemoryCache) InvalidateAll(_ context.Context) {
|
|
m.mu.Lock()
|
|
m.data = make(map[uint64]memItem)
|
|
m.mu.Unlock()
|
|
}
|
|
|
|
// NewRedisCache 预留:生产环境接入 Redis 时实现此构造,
|
|
// 满足 PermissionCache 接口即可在 NewProvider 处替换 MemoryCache。
|
|
// 当前未启用(项目暂未引入 go-redis 依赖),保留接口以明确扩展点。
|
|
func NewRedisCache(_ interface{}) PermissionCache {
|
|
// TODO: 接入 go-redis 后实现 Get/Set/Invalidate/InvalidateAll。
|
|
return nil
|
|
}
|