Add Session model, SessionService, and SessionRepository to track user login sessions and enable token revocation on auth-critical events. - Introduce explicit TokenType (access/refresh) in JWT claims to prevent refresh token misuse via access token endpoints - Add SessionID field to JWT claims, enabling stateless JWT validation against revoked sessions - Replace legacy Auth middleware with RequireAuth/OptionalAuth pipeline that validates token type, account status, and session validity - Implement session revocation on password change, reset, user ban/inactive, and explicit logout - Add Principal cache with active invalidation for banned/role-changed users - Fix IDOR vulnerability: GetMessagesByCursor now validates currentUserID is conversation participant via GetParticipantStrict - Add group member visibility checks: announcements, group info, member list now require group membership - Simplify Casbin policy: remove g grouping, use r.sub == p.sub matcher with globMatch; user_roles table is single source of truth for roles - Add migration logic to clean legacy casbin g rules and migrate old p rules from path-style to admin/<domain> resource naming
279 lines
7.9 KiB
Go
279 lines
7.9 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"time"
|
||
|
||
"with_you/internal/cache"
|
||
"with_you/internal/config"
|
||
"with_you/internal/pkg/auth"
|
||
"with_you/internal/repository"
|
||
|
||
"github.com/casbin/casbin/v3"
|
||
)
|
||
|
||
// CasbinService Casbin 权限服务接口
|
||
type CasbinService interface {
|
||
// Enforce 检查权限
|
||
Enforce(ctx context.Context, sub, obj, act string) (bool, error)
|
||
|
||
// EnforceForUser 检查用户权限 (自动获取用户角色)
|
||
EnforceForUser(ctx context.Context, userID, obj, act string) (bool, error)
|
||
|
||
// AddRoleForUser 为用户添加角色
|
||
AddRoleForUser(ctx context.Context, userID, role string) error
|
||
|
||
// DeleteRoleForUser 移除用户角色
|
||
DeleteRoleForUser(ctx context.Context, userID, role string) error
|
||
|
||
// GetRolesForUser 获取用户所有角色
|
||
GetRolesForUser(ctx context.Context, userID string) ([]string, error)
|
||
|
||
// HasRoleForUser 检查用户是否拥有指定角色
|
||
HasRoleForUser(ctx context.Context, userID, role string) (bool, error)
|
||
|
||
// AddPolicy 添加策略
|
||
AddPolicy(ctx context.Context, role, resource, action string) error
|
||
|
||
// RemovePolicy 移除策略
|
||
RemovePolicy(ctx context.Context, role, resource, action string) error
|
||
|
||
// LoadPolicy 从数据库重新加载策略
|
||
LoadPolicy(ctx context.Context) error
|
||
|
||
// ClearCache 清除权限缓存
|
||
ClearCache(ctx context.Context) error
|
||
|
||
// GetPermissionsForRole 获取角色的所有权限
|
||
GetPermissionsForRole(ctx context.Context, role string) ([][]string, error)
|
||
|
||
// UpdatePermissionsForRole 更新角色的权限(先删除所有权限,再添加新权限)
|
||
UpdatePermissionsForRole(ctx context.Context, role string, permissions [][]string) error
|
||
|
||
// GetAllPermissions 获取系统中所有权限定义
|
||
GetAllPermissions(ctx context.Context) [][]string
|
||
}
|
||
|
||
type casbinService struct {
|
||
enforcer *casbin.Enforcer
|
||
cache cache.Cache
|
||
cacheTTL time.Duration
|
||
roleRepo repository.RoleRepository
|
||
skipRoutes map[string]bool
|
||
}
|
||
|
||
func NewCasbinService(
|
||
enforcer *casbin.Enforcer,
|
||
cache cache.Cache,
|
||
roleRepo repository.RoleRepository,
|
||
cfg *config.CasbinConfig,
|
||
) CasbinService {
|
||
skipRoutes := make(map[string]bool)
|
||
for _, route := range cfg.SkipRoutes {
|
||
skipRoutes[route] = true
|
||
}
|
||
|
||
return &casbinService{
|
||
enforcer: enforcer,
|
||
cache: cache,
|
||
cacheTTL: time.Duration(cfg.CacheTTL) * time.Second,
|
||
roleRepo: roleRepo,
|
||
skipRoutes: skipRoutes,
|
||
}
|
||
}
|
||
|
||
func (s *casbinService) Enforce(ctx context.Context, sub, obj, act string) (bool, error) {
|
||
// 检查是否跳过权限检查
|
||
if s.skipRoutes[obj] {
|
||
return true, nil
|
||
}
|
||
|
||
// 缓存键与策略维度对齐:sub/obj/act 直接对应 p, role, resource, action。
|
||
// user_roles 表为用户-角色真源,由 EnforceForUser 解析后再调本方法;
|
||
// 这里 sub 已经是角色名,避免再走 g(user, role) 分组(model 中 matcher 已不再依赖 g)。
|
||
if s.cache != nil {
|
||
cacheKey := fmt.Sprintf("casbin:policy:%s:%s:%s", sub, obj, act)
|
||
if cached, ok := s.cache.Get(cacheKey); ok {
|
||
if allowed, ok := cached.(bool); ok {
|
||
return allowed, nil
|
||
}
|
||
}
|
||
}
|
||
|
||
// 执行权限检查
|
||
allowed, err := s.enforcer.Enforce(sub, obj, act)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
|
||
// 缓存结果
|
||
if s.cache != nil {
|
||
cacheKey := fmt.Sprintf("casbin:policy:%s:%s:%s", sub, obj, act)
|
||
s.cache.Set(cacheKey, allowed, s.cacheTTL)
|
||
}
|
||
|
||
return allowed, nil
|
||
}
|
||
|
||
func (s *casbinService) EnforceForUser(ctx context.Context, userID, obj, act string) (bool, error) {
|
||
// 获取用户角色
|
||
roles, err := s.GetRolesForUser(ctx, userID)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
|
||
// 如果用户没有角色,使用默认角色
|
||
if len(roles) == 0 {
|
||
roles = []string{"user"} // 默认角色
|
||
}
|
||
|
||
// 检查每个角色的权限
|
||
for _, role := range roles {
|
||
allowed, err := s.Enforce(ctx, role, obj, act)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
if allowed {
|
||
return true, nil
|
||
}
|
||
}
|
||
|
||
return false, nil
|
||
}
|
||
|
||
func (s *casbinService) AddRoleForUser(ctx context.Context, userID, role string) error {
|
||
// 真源策略:user_roles 表为唯一真源,Casbin 不再承载 g, user, role 分组策略。
|
||
// 减少双写漂移与缓存失效盲区;EnforceForUser 内部先查 DB 角色再对每个角色调 Enforce(role,...)。
|
||
if err := s.roleRepo.AddRoleForUser(ctx, userID, role); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 失效该用户的角色缓存,以及与该角色相关的策略缓存。
|
||
s.clearUserRoleCache(ctx, userID)
|
||
return s.clearPolicyCache(ctx)
|
||
}
|
||
|
||
func (s *casbinService) DeleteRoleForUser(ctx context.Context, userID, role string) error {
|
||
if err := s.roleRepo.DeleteRoleForUser(ctx, userID, role); err != nil {
|
||
return err
|
||
}
|
||
|
||
s.clearUserRoleCache(ctx, userID)
|
||
return s.clearPolicyCache(ctx)
|
||
}
|
||
|
||
func (s *casbinService) GetRolesForUser(ctx context.Context, userID string) ([]string, error) {
|
||
// 检查缓存
|
||
if s.cache != nil {
|
||
cacheKey := fmt.Sprintf("casbin:user:roles:%s", userID)
|
||
if cached, ok := s.cache.Get(cacheKey); ok {
|
||
if roles, ok := cached.([]string); ok {
|
||
return roles, nil
|
||
}
|
||
}
|
||
}
|
||
|
||
// 从数据库获取
|
||
roles, err := s.roleRepo.GetUserRoles(ctx, userID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 缓存结果
|
||
if s.cache != nil {
|
||
cacheKey := fmt.Sprintf("casbin:user:roles:%s", userID)
|
||
s.cache.Set(cacheKey, roles, s.cacheTTL)
|
||
}
|
||
|
||
return roles, nil
|
||
}
|
||
|
||
func (s *casbinService) HasRoleForUser(ctx context.Context, userID, role string) (bool, error) {
|
||
return s.roleRepo.HasRole(ctx, userID, role)
|
||
}
|
||
|
||
func (s *casbinService) AddPolicy(ctx context.Context, role, resource, action string) error {
|
||
if _, err := s.enforcer.AddPolicy(role, resource, action); err != nil {
|
||
return err
|
||
}
|
||
return s.ClearCache(ctx)
|
||
}
|
||
|
||
func (s *casbinService) RemovePolicy(ctx context.Context, role, resource, action string) error {
|
||
if _, err := s.enforcer.RemovePolicy(role, resource, action); err != nil {
|
||
return err
|
||
}
|
||
return s.ClearCache(ctx)
|
||
}
|
||
|
||
func (s *casbinService) LoadPolicy(ctx context.Context) error {
|
||
return s.enforcer.LoadPolicy()
|
||
}
|
||
|
||
func (s *casbinService) ClearCache(ctx context.Context) error {
|
||
if s.cache != nil {
|
||
// 清除所有 casbin 相关缓存:策略缓存 + 用户角色缓存。
|
||
s.cache.DeleteByPrefix("casbin:")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// clearPolicyCache 失效所有策略缓存(casbin:policy:*)。
|
||
//
|
||
// 策略变更或角色变更都应调用,确保下次 Enforce 重新查 Casbin。
|
||
func (s *casbinService) clearPolicyCache(ctx context.Context) error {
|
||
if s.cache != nil {
|
||
s.cache.DeleteByPrefix("casbin:policy:")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// clearUserRoleCache 仅失效某个用户的角色缓存(casbin:user:roles:{userID})。
|
||
//
|
||
// 用于 AddRoleForUser/DeleteRoleForUser:只刷新该用户的角色,不波及策略缓存(除非同时调 clearPolicyCache)。
|
||
// 同时失效认证管道的 Principal 缓存,让 RequireAuth 下次重新加载该用户的角色。
|
||
func (s *casbinService) clearUserRoleCache(ctx context.Context, userID string) error {
|
||
if s.cache != nil {
|
||
s.cache.Delete(fmt.Sprintf("casbin:user:roles:%s", userID))
|
||
s.cache.Delete(auth.PrincipalCacheKey(userID))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// clearUserCache Deprecated 兼容旧调用,内部转调 clearUserRoleCache。
|
||
func (s *casbinService) clearUserCache(ctx context.Context, userID string) error {
|
||
return s.clearUserRoleCache(ctx, userID)
|
||
}
|
||
|
||
func (s *casbinService) GetPermissionsForRole(ctx context.Context, role string) ([][]string, error) {
|
||
return s.enforcer.GetPermissionsForUser(role)
|
||
}
|
||
|
||
func (s *casbinService) UpdatePermissionsForRole(ctx context.Context, role string, permissions [][]string) error {
|
||
// 先删除该角色的所有权限
|
||
_, err := s.enforcer.DeletePermissionsForUser(role)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 添加新权限
|
||
for _, perm := range permissions {
|
||
if len(perm) >= 2 {
|
||
resource := perm[0]
|
||
action := perm[1]
|
||
if _, err := s.enforcer.AddPolicy(role, resource, action); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
|
||
// 清除缓存
|
||
return s.ClearCache(ctx)
|
||
}
|
||
|
||
func (s *casbinService) GetAllPermissions(ctx context.Context) [][]string {
|
||
permissions, _ := s.enforcer.GetPolicy()
|
||
return permissions
|
||
}
|