feat(rbac): M3 RBAC 权限模型完整实现

- 4 表设计: permissions, roles, role_permissions(m2m), user_roles
- 多租户隔离: 角色和用户角色分配按 tenant_id 隔离
- 权限并集: 多角色用户取权限并集,无继承
- 系统角色保护: is_system 角色不可修改/删除
- 种子数据: 7 个默认权限 + 3 个系统角色(super_admin/tenant_admin/member)
- 权限中间件: RequirePermission 支持按路由粒度控制
- API 路由: /api/v1/rbac/ 下完整 CRUD 端点
- fx 注册: RBAC Module 集成到 app.go
This commit is contained in:
MengStack Dev 2026-10-03 00:54:26 +08:00
parent 22dba67920
commit f8c374a624
13 changed files with 1077 additions and 0 deletions

View File

@ -13,6 +13,7 @@ import (
"mengstack/internal/config"
"mengstack/internal/logger"
authinterfaces "mengstack/internal/modules/auth/interfaces"
rbacinterfaces "mengstack/internal/modules/rbac/interfaces"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
@ -37,6 +38,7 @@ func newEngine(
rdb *redis.Client,
authHandler *authinterfaces.Handler,
authMW gin.HandlerFunc,
rbacHandler *rbacinterfaces.Handler,
) *gin.Engine {
ginMode := "release"
if cfg.Server.Mode == "debug" || cfg.Server.Mode == "dev" {
@ -56,6 +58,7 @@ func newEngine(
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
authinterfaces.SetupRoutes(r, authHandler, authMW)
rbacinterfaces.SetupRoutes(r, rbacHandler, authMW)
return r
}
@ -97,6 +100,7 @@ func NewApp() *fx.App {
database.Module,
cache.Module,
authinterfaces.Module,
rbacinterfaces.Module,
Module,
)
}

View File

@ -0,0 +1,266 @@
package application
import (
"context"
"mengstack/internal/kernel/errors"
"mengstack/internal/kernel/tenant"
"mengstack/internal/modules/rbac/domain"
)
type Service struct {
permRepo domain.PermissionRepository
roleRepo domain.RoleRepository
userRoleRepo domain.UserRoleRepository
}
func NewService(
permRepo domain.PermissionRepository,
roleRepo domain.RoleRepository,
userRoleRepo domain.UserRoleRepository,
) *Service {
return &Service{
permRepo: permRepo,
roleRepo: roleRepo,
userRoleRepo: userRoleRepo,
}
}
func (s *Service) ListPermissions(ctx context.Context) ([]domain.PermissionDTO, error) {
perms, err := s.permRepo.FindAll(ctx)
if err != nil {
return nil, errors.Wrap(err, "QUERY_PERMS_FAILED", "failed to query permissions", 500)
}
dtos := make([]domain.PermissionDTO, len(perms))
for i, p := range perms {
dtos[i] = domain.ToPermissionDTO(&p)
}
return dtos, nil
}
func (s *Service) ListPermissionsByModule(ctx context.Context, module string) ([]domain.PermissionDTO, error) {
perms, err := s.permRepo.FindByModule(ctx, module)
if err != nil {
return nil, errors.Wrap(err, "QUERY_PERMS_FAILED", "failed to query permissions", 500)
}
dtos := make([]domain.PermissionDTO, len(perms))
for i, p := range perms {
dtos[i] = domain.ToPermissionDTO(&p)
}
return dtos, nil
}
func (s *Service) CreateRole(ctx context.Context, req domain.CreateRoleRequest) (domain.RoleDTO, error) {
tenantID, ok := tenant.FromContext(ctx)
if !ok {
return domain.RoleDTO{}, errors.ErrTenantRequired
}
role := &domain.Role{
TenantID: tenantID,
Name: req.Name,
Description: req.Description,
}
if err := s.roleRepo.Create(ctx, role); err != nil {
return domain.RoleDTO{}, errors.Wrap(err, "CREATE_ROLE_FAILED", "failed to create role", 500)
}
return domain.ToRoleDTO(role), nil
}
func (s *Service) ListRoles(ctx context.Context) ([]domain.RoleDTO, error) {
tenantID, ok := tenant.FromContext(ctx)
if !ok {
return nil, errors.ErrTenantRequired
}
roles, err := s.roleRepo.FindByTenant(ctx, tenantID)
if err != nil {
return nil, errors.Wrap(err, "QUERY_ROLES_FAILED", "failed to query roles", 500)
}
dtos := make([]domain.RoleDTO, len(roles))
for i, r := range roles {
dtos[i] = domain.ToRoleDTO(&r)
}
return dtos, nil
}
func (s *Service) GetRole(ctx context.Context, roleID uint) (domain.RoleDTO, error) {
tenantID, ok := tenant.FromContext(ctx)
if !ok {
return domain.RoleDTO{}, errors.ErrTenantRequired
}
role, err := s.roleRepo.FindByIDWithPermissions(ctx, tenantID, roleID)
if err != nil {
return domain.RoleDTO{}, errors.ErrNotFound
}
return domain.ToRoleDTO(role), nil
}
func (s *Service) UpdateRole(ctx context.Context, roleID uint, req domain.UpdateRoleRequest) (domain.RoleDTO, error) {
tenantID, ok := tenant.FromContext(ctx)
if !ok {
return domain.RoleDTO{}, errors.ErrTenantRequired
}
role, err := s.roleRepo.FindByID(ctx, tenantID, roleID)
if err != nil {
return domain.RoleDTO{}, errors.ErrNotFound
}
if role.IsSystem {
return domain.RoleDTO{}, errors.New("SYSTEM_ROLE", "cannot modify system role", 403)
}
if req.Name != "" {
role.Name = req.Name
}
if req.Description != "" {
role.Description = req.Description
}
if err := s.roleRepo.Update(ctx, role); err != nil {
return domain.RoleDTO{}, errors.Wrap(err, "UPDATE_ROLE_FAILED", "failed to update role", 500)
}
return domain.ToRoleDTO(role), nil
}
func (s *Service) DeleteRole(ctx context.Context, roleID uint) error {
tenantID, ok := tenant.FromContext(ctx)
if !ok {
return errors.ErrTenantRequired
}
role, err := s.roleRepo.FindByID(ctx, tenantID, roleID)
if err != nil {
return errors.ErrNotFound
}
if role.IsSystem {
return errors.New("SYSTEM_ROLE", "cannot delete system role", 403)
}
if err := s.roleRepo.Delete(ctx, tenantID, roleID); err != nil {
return errors.Wrap(err, "DELETE_ROLE_FAILED", "failed to delete role", 500)
}
return nil
}
func (s *Service) SetRolePermissions(ctx context.Context, roleID uint, req domain.SetRolePermissionsRequest) error {
tenantID, ok := tenant.FromContext(ctx)
if !ok {
return errors.ErrTenantRequired
}
role, err := s.roleRepo.FindByID(ctx, tenantID, roleID)
if err != nil {
return errors.ErrNotFound
}
if role.IsSystem {
return errors.New("SYSTEM_ROLE", "cannot modify system role permissions", 403)
}
for _, pid := range req.PermissionIDs {
if _, err := s.permRepo.FindByID(ctx, pid); err != nil {
return errors.New("PERM_NOT_FOUND", "permission not found", 404)
}
}
if err := s.roleRepo.SetPermissions(ctx, roleID, req.PermissionIDs); err != nil {
return errors.Wrap(err, "SET_PERMS_FAILED", "failed to set permissions", 500)
}
return nil
}
func (s *Service) AssignRoleToUser(ctx context.Context, userID uint, req domain.AssignRoleRequest) error {
tenantID, ok := tenant.FromContext(ctx)
if !ok {
return errors.ErrTenantRequired
}
if _, err := s.roleRepo.FindByID(ctx, tenantID, req.RoleID); err != nil {
return errors.ErrNotFound
}
ur := &domain.UserRole{
UserID: userID,
TenantID: tenantID,
RoleID: req.RoleID,
}
if err := s.userRoleRepo.Assign(ctx, ur); err != nil {
return errors.Wrap(err, "ASSIGN_ROLE_FAILED", "failed to assign role", 500)
}
return nil
}
func (s *Service) RemoveRoleFromUser(ctx context.Context, userID uint, roleID uint) error {
tenantID, ok := tenant.FromContext(ctx)
if !ok {
return errors.ErrTenantRequired
}
if err := s.userRoleRepo.Remove(ctx, userID, tenantID, roleID); err != nil {
return errors.Wrap(err, "REMOVE_ROLE_FAILED", "failed to remove role", 500)
}
return nil
}
func (s *Service) GetUserPermissions(ctx context.Context, userID uint) ([]domain.PermissionDTO, error) {
tenantID, ok := tenant.FromContext(ctx)
if !ok {
return nil, errors.ErrTenantRequired
}
roles, err := s.userRoleRepo.FindRolesByUser(ctx, userID, tenantID)
if err != nil {
return nil, errors.Wrap(err, "QUERY_USER_ROLES_FAILED", "failed to query user roles", 500)
}
permMap := make(map[uint]domain.PermissionDTO)
for _, role := range roles {
for _, perm := range role.Permissions {
if _, exists := permMap[perm.ID]; !exists {
permMap[perm.ID] = domain.ToPermissionDTO(&perm)
}
}
}
dtos := make([]domain.PermissionDTO, 0, len(permMap))
for _, dto := range permMap {
dtos = append(dtos, dto)
}
return dtos, nil
}
func (s *Service) UserHasPermission(ctx context.Context, userID uint, permCode string) (bool, error) {
tenantID, ok := tenant.FromContext(ctx)
if !ok {
return false, errors.ErrTenantRequired
}
roles, err := s.userRoleRepo.FindRolesByUser(ctx, userID, tenantID)
if err != nil {
return false, errors.Wrap(err, "QUERY_USER_ROLES_FAILED", "failed to query user roles", 500)
}
for _, role := range roles {
for _, perm := range role.Permissions {
if perm.Code == permCode {
return true, nil
}
}
}
return false, nil
}

View File

@ -0,0 +1,35 @@
package domain
import "time"
type Permission struct {
ID uint `json:"id" gorm:"primaryKey"`
Code string `json:"code" gorm:"uniqueIndex;size:128;not null"`
Name string `json:"name" gorm:"size:128;not null"`
Module string `json:"module" gorm:"size:64;not null;index"`
Description string `json:"description" gorm:"size:256"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (Permission) TableName() string {
return "permissions"
}
type PermissionDTO struct {
ID uint `json:"id"`
Code string `json:"code"`
Name string `json:"name"`
Module string `json:"module"`
Description string `json:"description"`
}
func ToPermissionDTO(p *Permission) PermissionDTO {
return PermissionDTO{
ID: p.ID,
Code: p.Code,
Name: p.Name,
Module: p.Module,
Description: p.Description,
}
}

View File

@ -0,0 +1,28 @@
package domain
import "context"
type PermissionRepository interface {
Create(ctx context.Context, perm *Permission) error
FindByID(ctx context.Context, id uint) (*Permission, error)
FindByCode(ctx context.Context, code string) (*Permission, error)
FindAll(ctx context.Context) ([]Permission, error)
FindByModule(ctx context.Context, module string) ([]Permission, error)
}
type RoleRepository interface {
Create(ctx context.Context, role *Role) error
FindByID(ctx context.Context, tenantID string, id uint) (*Role, error)
FindByIDWithPermissions(ctx context.Context, tenantID string, id uint) (*Role, error)
FindByTenant(ctx context.Context, tenantID string) ([]Role, error)
Update(ctx context.Context, role *Role) error
Delete(ctx context.Context, tenantID string, id uint) error
SetPermissions(ctx context.Context, roleID uint, permIDs []uint) error
}
type UserRoleRepository interface {
Assign(ctx context.Context, ur *UserRole) error
Remove(ctx context.Context, userID uint, tenantID string, roleID uint) error
FindByUser(ctx context.Context, userID uint, tenantID string) ([]UserRole, error)
FindRolesByUser(ctx context.Context, userID uint, tenantID string) ([]Role, error)
}

View File

@ -0,0 +1,82 @@
package domain
import "time"
type Role struct {
ID uint `json:"id" gorm:"primaryKey"`
TenantID string `json:"tenant_id" gorm:"type:uuid;not null;primaryKey;index:idx_tenant_role_name,unique"`
Name string `json:"name" gorm:"size:64;not null;index:idx_tenant_role_name,unique"`
Description string `json:"description" gorm:"size:256"`
IsSystem bool `json:"is_system" gorm:"default:false"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Permissions []Permission `json:"permissions,omitempty" gorm:"many2many:role_permissions;"`
}
func (Role) TableName() string {
return "roles"
}
type RolePermission struct {
RoleID uint `gorm:"primaryKey"`
PermissionID uint `gorm:"primaryKey"`
}
func (RolePermission) TableName() string {
return "role_permissions"
}
type UserRole struct {
UserID uint `json:"user_id" gorm:"primaryKey"`
TenantID string `json:"tenant_id" gorm:"type:uuid;not null;primaryKey"`
RoleID uint `json:"role_id" gorm:"not null"`
}
func (UserRole) TableName() string {
return "user_roles"
}
type RoleDTO struct {
ID uint `json:"id"`
TenantID string `json:"tenant_id"`
Name string `json:"name"`
Description string `json:"description"`
IsSystem bool `json:"is_system"`
Permissions []PermissionDTO `json:"permissions,omitempty"`
}
func ToRoleDTO(r *Role) RoleDTO {
dto := RoleDTO{
ID: r.ID,
TenantID: r.TenantID,
Name: r.Name,
Description: r.Description,
IsSystem: r.IsSystem,
}
if r.Permissions != nil {
dto.Permissions = make([]PermissionDTO, len(r.Permissions))
for i, p := range r.Permissions {
dto.Permissions[i] = ToPermissionDTO(&p)
}
}
return dto
}
type CreateRoleRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
}
type UpdateRoleRequest struct {
Name string `json:"name"`
Description string `json:"description"`
}
type SetRolePermissionsRequest struct {
PermissionIDs []uint `json:"permission_ids" binding:"required"`
}
type AssignRoleRequest struct {
RoleID uint `json:"role_id" binding:"required"`
}

View File

@ -0,0 +1,16 @@
package infrastructure
import (
"mengstack/internal/modules/rbac/domain"
"gorm.io/gorm"
)
func Migrate(db *gorm.DB) error {
return db.AutoMigrate(
&domain.Permission{},
&domain.Role{},
&domain.RolePermission{},
&domain.UserRole{},
)
}

View File

@ -0,0 +1,45 @@
package infrastructure
import (
"context"
"mengstack/internal/modules/rbac/domain"
"gorm.io/gorm"
)
type permRepo struct {
db *gorm.DB
}
func NewPermissionRepository(db *gorm.DB) domain.PermissionRepository {
return &permRepo{db: db}
}
func (r *permRepo) Create(ctx context.Context, perm *domain.Permission) error {
return r.db.WithContext(ctx).Create(perm).Error
}
func (r *permRepo) FindByID(ctx context.Context, id uint) (*domain.Permission, error) {
var perm domain.Permission
err := r.db.WithContext(ctx).First(&perm, id).Error
return &perm, err
}
func (r *permRepo) FindByCode(ctx context.Context, code string) (*domain.Permission, error) {
var perm domain.Permission
err := r.db.WithContext(ctx).Where("code = ?", code).First(&perm).Error
return &perm, err
}
func (r *permRepo) FindAll(ctx context.Context) ([]domain.Permission, error) {
var perms []domain.Permission
err := r.db.WithContext(ctx).Order("module, code").Find(&perms).Error
return perms, err
}
func (r *permRepo) FindByModule(ctx context.Context, module string) ([]domain.Permission, error) {
var perms []domain.Permission
err := r.db.WithContext(ctx).Where("module = ?", module).Order("code").Find(&perms).Error
return perms, err
}

View File

@ -0,0 +1,65 @@
package infrastructure
import (
"context"
"mengstack/internal/modules/rbac/domain"
"gorm.io/gorm"
)
type roleRepo struct {
db *gorm.DB
}
func NewRoleRepository(db *gorm.DB) domain.RoleRepository {
return &roleRepo{db: db}
}
func (r *roleRepo) Create(ctx context.Context, role *domain.Role) error {
return r.db.WithContext(ctx).Create(role).Error
}
func (r *roleRepo) FindByID(ctx context.Context, tenantID string, id uint) (*domain.Role, error) {
var role domain.Role
err := r.db.WithContext(ctx).Where("id = ? AND tenant_id = ?", id, tenantID).First(&role).Error
return &role, err
}
func (r *roleRepo) FindByIDWithPermissions(ctx context.Context, tenantID string, id uint) (*domain.Role, error) {
var role domain.Role
err := r.db.WithContext(ctx).
Preload("Permissions").
Where("roles.id = ? AND roles.tenant_id = ?", id, tenantID).
First(&role).Error
return &role, err
}
func (r *roleRepo) FindByTenant(ctx context.Context, tenantID string) ([]domain.Role, error) {
var roles []domain.Role
err := r.db.WithContext(ctx).
Preload("Permissions").
Where("tenant_id = ?", tenantID).
Order("name").
Find(&roles).Error
return roles, err
}
func (r *roleRepo) Update(ctx context.Context, role *domain.Role) error {
return r.db.WithContext(ctx).Save(role).Error
}
func (r *roleRepo) Delete(ctx context.Context, tenantID string, id uint) error {
return r.db.WithContext(ctx).
Where("id = ? AND tenant_id = ?", id, tenantID).
Delete(&domain.Role{}).Error
}
func (r *roleRepo) SetPermissions(ctx context.Context, roleID uint, permIDs []uint) error {
role := &domain.Role{ID: roleID}
perms := make([]domain.Permission, len(permIDs))
for i, id := range permIDs {
perms[i] = domain.Permission{ID: id}
}
return r.db.WithContext(ctx).Model(role).Association("Permissions").Replace(perms)
}

View File

@ -0,0 +1,179 @@
package infrastructure
import (
"context"
"strings"
"mengstack/internal/modules/rbac/domain"
"go.uber.org/zap"
"gorm.io/gorm"
)
type permDef struct {
Code string
Name string
Module string
Desc string
}
var defaultPermissions = []permDef{
{"auth.user.read", "查看用户", "auth", "查看用户列表和详情"},
{"auth.user.write", "管理用户", "auth", "创建、编辑、删除用户"},
{"rbac.role.read", "查看角色", "rbac", "查看角色列表和详情"},
{"rbac.role.write", "管理角色", "rbac", "创建、编辑、删除角色"},
{"rbac.permission.read", "查看权限", "rbac", "查看权限列表"},
{"rbac.permission.assign", "分配权限", "rbac", "为角色分配权限"},
{"rbac.user_role.write", "分配角色", "rbac", "为用户分配角色"},
}
func Seed(db *gorm.DB, log *zap.Logger) error {
ctx := context.Background()
for _, p := range defaultPermissions {
var count int64
db.Model(&domain.Permission{}).Where("code = ?", p.Code).Count(&count)
if count > 0 {
continue
}
perm := &domain.Permission{
Code: p.Code,
Name: p.Name,
Module: p.Module,
Description: p.Desc,
}
if err := db.WithContext(ctx).Create(perm).Error; err != nil {
return err
}
log.Info("seeded permission", zap.String("code", p.Code))
}
var allPerms []domain.Permission
if err := db.WithContext(ctx).Find(&allPerms).Error; err != nil {
return err
}
if len(allPerms) == 0 {
return nil
}
var tenantIDs []string
if err := db.WithContext(ctx).Table("users").Distinct("tenant_id").
Pluck("tenant_id", &tenantIDs).Error; err != nil {
return err
}
for _, tid := range tenantIDs {
if err := seedTenantRoles(ctx, db, log, tid, allPerms); err != nil {
log.Error("seed tenant roles failed",
zap.String("tenant_id", tid),
zap.Error(err),
)
}
}
return nil
}
func seedTenantRoles(ctx context.Context, db *gorm.DB, log *zap.Logger, tenantID string, allPerms []domain.Permission) error {
superAdmin := &domain.Role{}
err := db.WithContext(ctx).
Where("tenant_id = ? AND name = ?", tenantID, "super_admin").
First(superAdmin).Error
if err == gorm.ErrRecordNotFound {
superAdmin = &domain.Role{
TenantID: tenantID,
Name: "super_admin",
Description: "超级管理员",
IsSystem: true,
}
if err := db.WithContext(ctx).Create(superAdmin).Error; err != nil {
return err
}
permRefs := make([]domain.Permission, len(allPerms))
copy(permRefs, allPerms)
if err := db.WithContext(ctx).Model(superAdmin).Association("Permissions").Replace(permRefs); err != nil {
return err
}
log.Info("seeded super_admin role", zap.String("tenant_id", tenantID))
} else if err != nil {
return err
}
tenantAdmin := &domain.Role{}
err = db.WithContext(ctx).
Where("tenant_id = ? AND name = ?", tenantID, "tenant_admin").
First(tenantAdmin).Error
if err == gorm.ErrRecordNotFound {
tenantAdmin = &domain.Role{
TenantID: tenantID,
Name: "tenant_admin",
Description: "租户管理员",
IsSystem: true,
}
if err := db.WithContext(ctx).Create(tenantAdmin).Error; err != nil {
return err
}
adminPermCodes := []string{
"auth.user.read", "auth.user.write",
"rbac.role.read", "rbac.permission.read", "rbac.user_role.write",
}
var adminPerms []domain.Permission
for _, p := range allPerms {
for _, code := range adminPermCodes {
if p.Code == code {
adminPerms = append(adminPerms, p)
break
}
}
}
if len(adminPerms) > 0 {
if err := db.WithContext(ctx).Model(tenantAdmin).Association("Permissions").Replace(adminPerms); err != nil {
return err
}
}
log.Info("seeded tenant_admin role", zap.String("tenant_id", tenantID))
} else if err != nil {
return err
}
member := &domain.Role{}
err = db.WithContext(ctx).
Where("tenant_id = ? AND name = ?", tenantID, "member").
First(member).Error
if err == gorm.ErrRecordNotFound {
member = &domain.Role{
TenantID: tenantID,
Name: "member",
Description: "普通成员",
IsSystem: true,
}
if err := db.WithContext(ctx).Create(member).Error; err != nil {
return err
}
memberPermCodes := []string{"auth.user.read", "rbac.permission.read"}
var memberPerms []domain.Permission
for _, p := range allPerms {
for _, code := range memberPermCodes {
if strings.EqualFold(p.Code, code) {
memberPerms = append(memberPerms, p)
break
}
}
}
if len(memberPerms) > 0 {
if err := db.WithContext(ctx).Model(member).Association("Permissions").Replace(memberPerms); err != nil {
return err
}
}
log.Info("seeded member role", zap.String("tenant_id", tenantID))
} else if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,45 @@
package infrastructure
import (
"context"
"mengstack/internal/modules/rbac/domain"
"gorm.io/gorm"
)
type userRoleRepo struct {
db *gorm.DB
}
func NewUserRoleRepository(db *gorm.DB) domain.UserRoleRepository {
return &userRoleRepo{db: db}
}
func (r *userRoleRepo) Assign(ctx context.Context, ur *domain.UserRole) error {
return r.db.WithContext(ctx).Create(ur).Error
}
func (r *userRoleRepo) Remove(ctx context.Context, userID uint, tenantID string, roleID uint) error {
return r.db.WithContext(ctx).
Where("user_id = ? AND tenant_id = ? AND role_id = ?", userID, tenantID, roleID).
Delete(&domain.UserRole{}).Error
}
func (r *userRoleRepo) FindByUser(ctx context.Context, userID uint, tenantID string) ([]domain.UserRole, error) {
var urs []domain.UserRole
err := r.db.WithContext(ctx).
Where("user_id = ? AND tenant_id = ?", userID, tenantID).
Find(&urs).Error
return urs, err
}
func (r *userRoleRepo) FindRolesByUser(ctx context.Context, userID uint, tenantID string) ([]domain.Role, error) {
var roles []domain.Role
err := r.db.WithContext(ctx).
Joins("JOIN user_roles ON user_roles.role_id = roles.id").
Where("user_roles.user_id = ? AND user_roles.tenant_id = ?", userID, tenantID).
Preload("Permissions").
Find(&roles).Error
return roles, err
}

View File

@ -0,0 +1,215 @@
package interfaces
import (
"strconv"
"mengstack/internal/kernel/response"
"mengstack/internal/modules/rbac/application"
"mengstack/internal/modules/rbac/domain"
"github.com/gin-gonic/gin"
)
type Handler struct {
svc *application.Service
}
func NewHandler(svc *application.Service) *Handler {
return &Handler{svc: svc}
}
func (h *Handler) ListPermissions(c *gin.Context) {
module := c.Query("module")
var (
dtos []domain.PermissionDTO
err error
)
if module != "" {
dtos, err = h.svc.ListPermissionsByModule(c.Request.Context(), module)
} else {
dtos, err = h.svc.ListPermissions(c.Request.Context())
}
if err != nil {
response.HandleError(c, err)
return
}
response.Success(c, dtos)
}
func (h *Handler) CreateRole(c *gin.Context) {
var req domain.CreateRoleRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, response.NewBadRequest("invalid request body"))
return
}
dto, err := h.svc.CreateRole(c.Request.Context(), req)
if err != nil {
response.HandleError(c, err)
return
}
response.Success(c, dto)
}
func (h *Handler) ListRoles(c *gin.Context) {
dtos, err := h.svc.ListRoles(c.Request.Context())
if err != nil {
response.HandleError(c, err)
return
}
response.Success(c, dtos)
}
func (h *Handler) GetRole(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.Fail(c, response.NewBadRequest("invalid role id"))
return
}
dto, err := h.svc.GetRole(c.Request.Context(), uint(id))
if err != nil {
response.HandleError(c, err)
return
}
response.Success(c, dto)
}
func (h *Handler) UpdateRole(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.Fail(c, response.NewBadRequest("invalid role id"))
return
}
var req domain.UpdateRoleRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, response.NewBadRequest("invalid request body"))
return
}
dto, err := h.svc.UpdateRole(c.Request.Context(), uint(id), req)
if err != nil {
response.HandleError(c, err)
return
}
response.Success(c, dto)
}
func (h *Handler) DeleteRole(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.Fail(c, response.NewBadRequest("invalid role id"))
return
}
if err := h.svc.DeleteRole(c.Request.Context(), uint(id)); err != nil {
response.HandleError(c, err)
return
}
response.Success(c, nil)
}
func (h *Handler) SetRolePermissions(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.Fail(c, response.NewBadRequest("invalid role id"))
return
}
var req domain.SetRolePermissionsRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, response.NewBadRequest("invalid request body"))
return
}
if err := h.svc.SetRolePermissions(c.Request.Context(), uint(id), req); err != nil {
response.HandleError(c, err)
return
}
response.Success(c, nil)
}
func (h *Handler) GetRolePermissions(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.Fail(c, response.NewBadRequest("invalid role id"))
return
}
dto, err := h.svc.GetRole(c.Request.Context(), uint(id))
if err != nil {
response.HandleError(c, err)
return
}
response.Success(c, dto.Permissions)
}
func (h *Handler) AssignRole(c *gin.Context) {
userID, err := strconv.ParseUint(c.Param("userId"), 10, 64)
if err != nil {
response.Fail(c, response.NewBadRequest("invalid user id"))
return
}
var req domain.AssignRoleRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, response.NewBadRequest("invalid request body"))
return
}
if err := h.svc.AssignRoleToUser(c.Request.Context(), uint(userID), req); err != nil {
response.HandleError(c, err)
return
}
response.Success(c, nil)
}
func (h *Handler) RemoveRole(c *gin.Context) {
userID, err := strconv.ParseUint(c.Param("userId"), 10, 64)
if err != nil {
response.Fail(c, response.NewBadRequest("invalid user id"))
return
}
roleID, err := strconv.ParseUint(c.Param("roleId"), 10, 64)
if err != nil {
response.Fail(c, response.NewBadRequest("invalid role id"))
return
}
if err := h.svc.RemoveRoleFromUser(c.Request.Context(), uint(userID), uint(roleID)); err != nil {
response.HandleError(c, err)
return
}
response.Success(c, nil)
}
func (h *Handler) GetUserRoles(c *gin.Context) {
userID, err := strconv.ParseUint(c.Param("userId"), 10, 64)
if err != nil {
response.Fail(c, response.NewBadRequest("invalid user id"))
return
}
dtos, err := h.svc.GetUserPermissions(c.Request.Context(), uint(userID))
if err != nil {
response.HandleError(c, err)
return
}
response.Success(c, dtos)
}

View File

@ -0,0 +1,56 @@
package interfaces
import (
"mengstack/internal/app/middleware"
"mengstack/internal/modules/rbac/application"
"mengstack/internal/modules/rbac/infrastructure"
rbacmiddleware "mengstack/internal/modules/rbac/middleware"
"github.com/gin-gonic/gin"
"go.uber.org/fx"
"go.uber.org/zap"
"gorm.io/gorm"
)
func SetupRoutes(
r *gin.Engine,
h *Handler,
authMW gin.HandlerFunc,
) {
rbac := r.Group("/api/v1/rbac")
rbac.Use(authMW, middleware.MultiTenant())
{
rbac.GET("/permissions", h.ListPermissions)
rbac.POST("/roles", h.CreateRole)
rbac.GET("/roles", h.ListRoles)
rbac.GET("/roles/:id", h.GetRole)
rbac.PUT("/roles/:id", h.UpdateRole)
rbac.DELETE("/roles/:id", h.DeleteRole)
rbac.PUT("/roles/:id/permissions", h.SetRolePermissions)
rbac.GET("/roles/:id/permissions", h.GetRolePermissions)
rbac.POST("/users/:userId/roles", h.AssignRole)
rbac.DELETE("/users/:userId/roles/:roleId", h.RemoveRole)
rbac.GET("/users/:userId/roles", h.GetUserRoles)
}
}
var Module = fx.Module("rbac",
fx.Provide(
infrastructure.NewPermissionRepository,
infrastructure.NewRoleRepository,
infrastructure.NewUserRoleRepository,
application.NewService,
NewHandler,
rbacmiddleware.NewPermissionMiddleware,
),
fx.Invoke(func(db *gorm.DB, log *zap.Logger) {
if err := infrastructure.Migrate(db); err != nil {
log.Fatal("rbac migration failed", zap.Error(err))
}
if err := infrastructure.Seed(db, log); err != nil {
log.Error("rbac seed failed", zap.Error(err))
}
}),
)

View File

@ -0,0 +1,41 @@
package middleware
import (
"net/http"
"mengstack/internal/modules/rbac/application"
"github.com/gin-gonic/gin"
)
type PermissionMiddleware struct {
svc *application.Service
}
type RequirePermissionFunc func(permCode string) gin.HandlerFunc
func NewPermissionMiddleware(svc *application.Service) *PermissionMiddleware {
return &PermissionMiddleware{svc: svc}
}
func (m *PermissionMiddleware) RequirePermission(permCode string) gin.HandlerFunc {
return func(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"code": -1, "message": "unauthorized"})
return
}
has, err := m.svc.UserHasPermission(c.Request.Context(), userID.(uint), permCode)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"code": -1, "message": "permission check failed"})
return
}
if !has {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": -1, "message": "permission denied"})
return
}
c.Next()
}
}