2026-04-28 14:53:04 +08:00
|
|
|
|
package service
|
2026-03-14 02:09:38 +08:00
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
|
2026-04-22 16:01:59 +08:00
|
|
|
|
apperrors "with_you/internal/errors"
|
|
|
|
|
|
"with_you/internal/model"
|
|
|
|
|
|
"with_you/internal/repository"
|
2026-03-14 02:09:38 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// RoleService 角色管理服务接口
|
|
|
|
|
|
type RoleService interface {
|
|
|
|
|
|
// GetUserRoles 获取用户的所有角色
|
|
|
|
|
|
GetUserRoles(ctx context.Context, userID string) ([]model.Role, error)
|
|
|
|
|
|
|
|
|
|
|
|
// AssignRole 为用户分配角色
|
|
|
|
|
|
AssignRole(ctx context.Context, operatorID, targetUserID, role string) error
|
|
|
|
|
|
|
|
|
|
|
|
// RemoveRole 移除用户的角色
|
|
|
|
|
|
RemoveRole(ctx context.Context, operatorID, targetUserID, role string) error
|
|
|
|
|
|
|
|
|
|
|
|
// GetAllRoles 获取所有角色定义
|
|
|
|
|
|
GetAllRoles(ctx context.Context) ([]model.Role, error)
|
|
|
|
|
|
|
|
|
|
|
|
// GetRole 获取角色详情
|
|
|
|
|
|
GetRole(ctx context.Context, name string) (*model.Role, error)
|
2026-03-14 18:01:55 +08:00
|
|
|
|
|
|
|
|
|
|
// GetRoleDetail 获取角色详情(包含用户数量和权限)
|
|
|
|
|
|
GetRoleDetail(ctx context.Context, name string) (*RoleDetail, error)
|
|
|
|
|
|
|
|
|
|
|
|
// GetRolePermissions 获取角色的权限列表
|
|
|
|
|
|
GetRolePermissions(ctx context.Context, name string) ([]Permission, error)
|
|
|
|
|
|
|
|
|
|
|
|
// GetAllPermissions 获取系统中所有权限定义
|
|
|
|
|
|
GetAllPermissions(ctx context.Context) ([]Permission, error)
|
|
|
|
|
|
|
|
|
|
|
|
// UpdateRolePermissions 更新角色权限
|
|
|
|
|
|
UpdateRolePermissions(ctx context.Context, roleName string, permissions []Permission) error
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Permission 权限定义
|
|
|
|
|
|
type Permission struct {
|
|
|
|
|
|
Resource string `json:"resource"`
|
|
|
|
|
|
Action string `json:"action"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// RoleDetail 角色详情
|
|
|
|
|
|
type RoleDetail struct {
|
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
|
DisplayName string `json:"display_name"`
|
|
|
|
|
|
Description string `json:"description"`
|
|
|
|
|
|
ParentRole *string `json:"parent_role"`
|
|
|
|
|
|
Priority int `json:"priority"`
|
|
|
|
|
|
UserCount int64 `json:"user_count"`
|
|
|
|
|
|
Permissions []Permission `json:"permissions"`
|
|
|
|
|
|
CreatedAt string `json:"created_at"`
|
|
|
|
|
|
UpdatedAt string `json:"updated_at"`
|
2026-03-14 02:09:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type roleService struct {
|
|
|
|
|
|
roleRepo repository.RoleRepository
|
|
|
|
|
|
casbinSvc CasbinService
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewRoleService 创建角色管理服务
|
|
|
|
|
|
func NewRoleService(
|
|
|
|
|
|
roleRepo repository.RoleRepository,
|
|
|
|
|
|
casbinSvc CasbinService,
|
|
|
|
|
|
) RoleService {
|
|
|
|
|
|
return &roleService{
|
|
|
|
|
|
roleRepo: roleRepo,
|
|
|
|
|
|
casbinSvc: casbinSvc,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *roleService) GetUserRoles(ctx context.Context, userID string) ([]model.Role, error) {
|
|
|
|
|
|
roleNames, err := s.casbinSvc.GetRolesForUser(ctx, userID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var roles []model.Role
|
|
|
|
|
|
for _, name := range roleNames {
|
|
|
|
|
|
role, err := s.roleRepo.GetRole(ctx, name)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
continue // 忽略找不到的角色
|
|
|
|
|
|
}
|
|
|
|
|
|
roles = append(roles, *role)
|
|
|
|
|
|
}
|
|
|
|
|
|
return roles, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *roleService) AssignRole(ctx context.Context, operatorID, targetUserID, role string) error {
|
|
|
|
|
|
// 不能修改自己的角色
|
|
|
|
|
|
if operatorID == targetUserID {
|
|
|
|
|
|
return apperrors.ErrCannotModifyOwnRole
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
// 纵深防御:分配 super_admin 仅 super_admin 自身可操作。
|
|
|
|
|
|
// 路由层 Authorize 已通过 admin.users.roles 限制 super_admin 能拿到此动作,
|
|
|
|
|
|
// 这里再独立校验 operator 角色,防止路由策略错配或服务被其他路径调用造成提权。
|
|
|
|
|
|
if role == model.RoleSuperAdmin {
|
|
|
|
|
|
roles, err := s.casbinSvc.GetRolesForUser(ctx, operatorID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return apperrors.ErrCasbinInternal
|
|
|
|
|
|
}
|
|
|
|
|
|
if !containsRole(roles, model.RoleSuperAdmin) {
|
|
|
|
|
|
return apperrors.ErrCannotModifySuperAdmin
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 02:09:38 +08:00
|
|
|
|
// 检查目标用户是否已有该角色
|
|
|
|
|
|
hasRole, err := s.casbinSvc.HasRoleForUser(ctx, targetUserID, role)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if hasRole {
|
|
|
|
|
|
return apperrors.ErrRoleAlreadyAssigned
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查角色是否存在
|
|
|
|
|
|
_, err = s.roleRepo.GetRole(ctx, role)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return apperrors.ErrRoleNotFound
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加角色
|
|
|
|
|
|
return s.casbinSvc.AddRoleForUser(ctx, targetUserID, role)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *roleService) RemoveRole(ctx context.Context, operatorID, targetUserID, role string) error {
|
|
|
|
|
|
// 不能修改自己的角色
|
|
|
|
|
|
if operatorID == targetUserID {
|
|
|
|
|
|
return apperrors.ErrCannotModifyOwnRole
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 不能修改超级管理员角色
|
|
|
|
|
|
if role == model.RoleSuperAdmin {
|
|
|
|
|
|
return apperrors.ErrCannotModifySuperAdmin
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
// 纵深防御:移除其他用户的 super_admin 角色同样要求 operator 是 super_admin。
|
|
|
|
|
|
// (role==super_admin 已被上面拦截,这里针对未来扩展:如果允许移除其他高优先级角色再加规则。)
|
2026-03-14 02:09:38 +08:00
|
|
|
|
// 检查用户是否有该角色
|
|
|
|
|
|
hasRole, err := s.casbinSvc.HasRoleForUser(ctx, targetUserID, role)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if !hasRole {
|
|
|
|
|
|
return apperrors.ErrRoleNotAssigned
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return s.casbinSvc.DeleteRoleForUser(ctx, targetUserID, role)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
// containsRole 检查角色集合中是否包含指定角色。
|
|
|
|
|
|
func containsRole(roles []string, role string) bool {
|
|
|
|
|
|
for _, r := range roles {
|
|
|
|
|
|
if r == role {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 02:09:38 +08:00
|
|
|
|
func (s *roleService) GetAllRoles(ctx context.Context) ([]model.Role, error) {
|
|
|
|
|
|
return s.roleRepo.GetAllRoles(ctx)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *roleService) GetRole(ctx context.Context, name string) (*model.Role, error) {
|
|
|
|
|
|
return s.roleRepo.GetRole(ctx, name)
|
|
|
|
|
|
}
|
2026-03-14 18:01:55 +08:00
|
|
|
|
|
|
|
|
|
|
func (s *roleService) GetRoleDetail(ctx context.Context, name string) (*RoleDetail, error) {
|
|
|
|
|
|
role, err := s.roleRepo.GetRole(ctx, name)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, apperrors.ErrRoleNotFound
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取用户数量
|
|
|
|
|
|
userCount, err := s.roleRepo.GetRoleUserCount(ctx, name)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
userCount = 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取权限
|
|
|
|
|
|
permissions, err := s.casbinSvc.GetPermissionsForRole(ctx, name)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
perms := make([]Permission, 0)
|
|
|
|
|
|
for _, p := range permissions {
|
|
|
|
|
|
if len(p) >= 2 {
|
|
|
|
|
|
perms = append(perms, Permission{
|
|
|
|
|
|
Resource: p[0],
|
|
|
|
|
|
Action: p[1],
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return &RoleDetail{
|
|
|
|
|
|
Name: role.Name,
|
|
|
|
|
|
DisplayName: role.DisplayName,
|
|
|
|
|
|
Description: role.Description,
|
|
|
|
|
|
ParentRole: role.ParentRole,
|
|
|
|
|
|
Priority: role.Priority,
|
|
|
|
|
|
UserCount: userCount,
|
|
|
|
|
|
Permissions: perms,
|
|
|
|
|
|
CreatedAt: role.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
|
|
|
|
UpdatedAt: role.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *roleService) GetRolePermissions(ctx context.Context, name string) ([]Permission, error) {
|
|
|
|
|
|
// 检查角色是否存在
|
|
|
|
|
|
_, err := s.roleRepo.GetRole(ctx, name)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, apperrors.ErrRoleNotFound
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
permissions, err := s.casbinSvc.GetPermissionsForRole(ctx, name)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化为空数组,避免 JSON 序列化为 null
|
|
|
|
|
|
perms := make([]Permission, 0)
|
|
|
|
|
|
for _, p := range permissions {
|
|
|
|
|
|
if len(p) >= 2 {
|
|
|
|
|
|
perms = append(perms, Permission{
|
|
|
|
|
|
Resource: p[0],
|
|
|
|
|
|
Action: p[1],
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return perms, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *roleService) GetAllPermissions(ctx context.Context) ([]Permission, error) {
|
|
|
|
|
|
permissions := s.casbinSvc.GetAllPermissions(ctx)
|
|
|
|
|
|
|
|
|
|
|
|
perms := make([]Permission, 0)
|
|
|
|
|
|
for _, p := range permissions {
|
|
|
|
|
|
if len(p) >= 3 {
|
|
|
|
|
|
// p[0] 是角色名,p[1] 是资源,p[2] 是操作
|
|
|
|
|
|
perms = append(perms, Permission{
|
|
|
|
|
|
Resource: p[1],
|
|
|
|
|
|
Action: p[2],
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return perms, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *roleService) UpdateRolePermissions(ctx context.Context, roleName string, permissions []Permission) error {
|
|
|
|
|
|
// 检查角色是否存在
|
|
|
|
|
|
_, err := s.roleRepo.GetRole(ctx, roleName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return apperrors.ErrRoleNotFound
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 不能修改超级管理员权限
|
|
|
|
|
|
if roleName == model.RoleSuperAdmin {
|
|
|
|
|
|
return apperrors.ErrCannotModifySuperAdmin
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 转换权限格式
|
|
|
|
|
|
var perms [][]string
|
|
|
|
|
|
for _, p := range permissions {
|
|
|
|
|
|
perms = append(perms, []string{p.Resource, p.Action})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return s.casbinSvc.UpdatePermissionsForRole(ctx, roleName, perms)
|
|
|
|
|
|
}
|