Files
backend/internal/service/casbin_service.go
lafay eb931bf1c6
All checks were successful
Build Backend / build (push) Successful in 2m19s
Build Backend / build-docker (push) Successful in 46s
feat(auth): implement session-based token management with revocation support
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
2026-07-05 18:28:08 +08:00

279 lines
7.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}