|
|
//
|
|
|
// Copyright (C) 2024 veypi <i@veypi.com>
|
|
|
// 2025-02-14 16:08:06
|
|
|
// Distributed under terms of the MIT license.
|
|
|
//
|
|
|
|
|
|
package auth
|
|
|
|
|
|
import (
|
|
|
"context"
|
|
|
"fmt"
|
|
|
"regexp"
|
|
|
"strings"
|
|
|
"time"
|
|
|
|
|
|
"github.com/veypi/vbase/cfg"
|
|
|
"github.com/veypi/vbase/libs/cache"
|
|
|
"github.com/veypi/vbase/models"
|
|
|
"github.com/veypi/vigo"
|
|
|
"github.com/veypi/vigo/contrib/event"
|
|
|
)
|
|
|
|
|
|
// Auth 权限管理接口
|
|
|
type Auth interface {
|
|
|
// ========== 中间件生成 ==========
|
|
|
// 基础权限检查
|
|
|
Perm(permissionID string) func(*vigo.X) error
|
|
|
|
|
|
// 资源所有者权限
|
|
|
PermWithOwner(permissionID, ownerKey string) func(*vigo.X) error
|
|
|
|
|
|
// 特定资源权限检查 (自动从 Path/Query 获取资源ID)
|
|
|
PermOnResource(permissionID, resourceKey string) func(*vigo.X) error
|
|
|
|
|
|
// 满足任一权限
|
|
|
PermAny(permissionIDs ...string) func(*vigo.X) error
|
|
|
|
|
|
// 满足所有权限
|
|
|
PermAll(permissionIDs ...string) func(*vigo.X) error
|
|
|
|
|
|
// ========== 角色管理 ==========
|
|
|
// 添加角色定义
|
|
|
// policies 格式: "resource:action",例如 "user:read", "*:*"
|
|
|
AddRole(roleCode, roleName string, policies ...string) error
|
|
|
|
|
|
// ========== 权限管理 ==========
|
|
|
// 授予角色
|
|
|
GrantRole(ctx context.Context, userID, orgID, roleCode string) error
|
|
|
|
|
|
// 撤销角色
|
|
|
RevokeRole(ctx context.Context, userID, orgID, roleCode string) error
|
|
|
|
|
|
// 授予特定资源权限
|
|
|
GrantResourcePerm(ctx context.Context, userID, orgID, permissionID, resourceID string) error
|
|
|
|
|
|
// 撤销特定资源权限
|
|
|
RevokeResourcePerm(ctx context.Context, userID, orgID, permissionID, resourceID string) error
|
|
|
|
|
|
// 撤销用户所有权限
|
|
|
RevokeAll(ctx context.Context, userID, orgID string) error
|
|
|
|
|
|
// ========== 权限查询 ==========
|
|
|
// 检查权限
|
|
|
CheckPermission(ctx context.Context, userID, orgID, permissionID, resourceID string) (bool, error)
|
|
|
|
|
|
// 列出用户权限
|
|
|
ListUserPermissions(ctx context.Context, userID, orgID string) ([]models.UserPermissionResult, error)
|
|
|
|
|
|
// 列出资源授权用户
|
|
|
ListResourceUsers(ctx context.Context, orgID, permissionID, resourceID string) ([]models.ResourceUser, error)
|
|
|
}
|
|
|
|
|
|
// 全局 Auth 工厂
|
|
|
var Factory = &authFactory{
|
|
|
apps: make(map[string]*appAuth),
|
|
|
}
|
|
|
|
|
|
// VBaseAuth vbase 自身的权限管理实例
|
|
|
// 由 vbase 包在初始化时注入
|
|
|
var VBaseAuth = Factory.New("vb")
|
|
|
|
|
|
func init() {
|
|
|
// 为 VBaseAuth 添加默认角色
|
|
|
VBaseAuth.AddRole("admin", "管理员", "*:*")
|
|
|
VBaseAuth.AddRole("user", "普通用户",
|
|
|
"user:read",
|
|
|
"org:read",
|
|
|
"org:create",
|
|
|
"oauth-client:read",
|
|
|
"oauth-client:create",
|
|
|
"oauth-client:update",
|
|
|
"oauth-client:delete",
|
|
|
)
|
|
|
|
|
|
// 注册权限初始化回调到 cfg 包
|
|
|
// 这样 models.InitDB() 可以在合适的时机调用,避免循环依赖
|
|
|
event.Add("vb.init.auth", Factory.init)
|
|
|
}
|
|
|
|
|
|
type authFactory struct {
|
|
|
apps map[string]*appAuth // appKey -> auth实例
|
|
|
}
|
|
|
|
|
|
// New 创建权限管理实例(注册权限域)
|
|
|
func (f *authFactory) New(scope string) Auth {
|
|
|
if _, exists := f.apps[scope]; exists {
|
|
|
return f.apps[scope]
|
|
|
}
|
|
|
|
|
|
auth := &appAuth{
|
|
|
scope: scope,
|
|
|
roleDefs: make(map[string]roleDefinition),
|
|
|
policies: make(map[string][][2]string),
|
|
|
roleInitDone: make(map[string]bool),
|
|
|
}
|
|
|
f.apps[scope] = auth
|
|
|
return auth
|
|
|
}
|
|
|
|
|
|
var validResourceRegex = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_-]*$`)
|
|
|
|
|
|
func validatePermissionID(permissionID string) {
|
|
|
if permissionID == "*:*" {
|
|
|
return
|
|
|
}
|
|
|
parts := strings.Split(permissionID, ":")
|
|
|
// 允许 app:resource:action 或 resource:action 格式
|
|
|
// 如果是 app:resource:action,则 parts 长度为 3
|
|
|
// 如果是 resource:action,则 parts 长度为 2
|
|
|
if len(parts) != 2 && len(parts) != 3 {
|
|
|
panic(fmt.Sprintf("invalid permission format: %s, expected 'resource:action' or 'app:resource:action'", permissionID))
|
|
|
}
|
|
|
|
|
|
resource := parts[len(parts)-2]
|
|
|
if !validResourceRegex.MatchString(resource) {
|
|
|
panic(fmt.Sprintf("invalid resource identifier: %s, must start with letter and contain only letters, numbers, '-' or '_'", resource))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Init 初始化所有注册的权限配置
|
|
|
// - 检查不同 app 之间是否有冲突
|
|
|
// - 同步 Permission 到数据库
|
|
|
// - 建立预设角色
|
|
|
func (f *authFactory) init() error {
|
|
|
for appKey, auth := range f.apps {
|
|
|
if err := auth.init(); err != nil {
|
|
|
return fmt.Errorf("failed to init auth for %s: %w", appKey, err)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
// roleDefinition 角色定义(内部使用)
|
|
|
type roleDefinition struct {
|
|
|
code string
|
|
|
name string
|
|
|
description string
|
|
|
policies []string // 权限列表: ["resource:action", "*:*"]
|
|
|
}
|
|
|
|
|
|
// appAuth 单个权限域的权限管理
|
|
|
type appAuth struct {
|
|
|
scope string // 权限域标识
|
|
|
roleDefs map[string]roleDefinition // roleCode -> role definition
|
|
|
policies map[string][][2]string // roleCode -> list of [resource, action] pairs
|
|
|
roleInitDone map[string]bool // roleCode -> whether role is initialized in DB
|
|
|
}
|
|
|
|
|
|
// AddRole 添加角色定义
|
|
|
// policies 格式: "resource:action",例如 "user:read", "*:*"
|
|
|
func (a *appAuth) AddRole(roleCode, roleName string, policies ...string) error {
|
|
|
if roleCode == "" || roleName == "" {
|
|
|
return fmt.Errorf("role code and name cannot be empty")
|
|
|
}
|
|
|
|
|
|
// 解析并验证权限格式
|
|
|
parsedPolicies := make([][2]string, 0, len(policies))
|
|
|
for _, policy := range policies {
|
|
|
// 严格检查格式: resource:action
|
|
|
parts := strings.Split(policy, ":")
|
|
|
if len(parts) != 2 {
|
|
|
return fmt.Errorf("invalid policy format: %s, expected 'resource:action'", policy)
|
|
|
}
|
|
|
resource, action := parts[0], parts[1]
|
|
|
|
|
|
// 验证 resource 和 action 不为空
|
|
|
if resource == "" || action == "" {
|
|
|
return fmt.Errorf("resource and action cannot be empty in policy: %s", policy)
|
|
|
}
|
|
|
|
|
|
// 验证 resource 格式(如果不是通配符)
|
|
|
if resource != "*" {
|
|
|
if !validResourceRegex.MatchString(resource) {
|
|
|
return fmt.Errorf("invalid resource identifier: %s in policy: %s, must start with letter and contain only letters, numbers, '-' or '_'", resource, policy)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 验证 action 格式(如果不是通配符)
|
|
|
if action != "*" {
|
|
|
if !validResourceRegex.MatchString(action) {
|
|
|
return fmt.Errorf("invalid action identifier: %s in policy: %s, must start with letter and contain only letters, numbers, '-' or '_'", action, policy)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
parsedPolicies = append(parsedPolicies, [2]string{resource, action})
|
|
|
}
|
|
|
|
|
|
// 存储角色定义
|
|
|
a.roleDefs[roleCode] = roleDefinition{
|
|
|
code: roleCode,
|
|
|
name: roleName,
|
|
|
}
|
|
|
a.policies[roleCode] = parsedPolicies
|
|
|
|
|
|
// 如果已经初始化过,立即同步到数据库
|
|
|
if len(a.roleInitDone) > 0 {
|
|
|
return a.initRole(roleCode)
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
// init 初始化应用的权限配置
|
|
|
func (a *appAuth) init() error {
|
|
|
// 1. 同步权限定义到数据库
|
|
|
for _, permDef := range a.extractPermissions() {
|
|
|
var perm models.Permission
|
|
|
err := cfg.DB().Where("id = ?", permDef.ID).First(&perm).Error
|
|
|
if err != nil {
|
|
|
// 不存在则创建
|
|
|
perm = permDef
|
|
|
if err := cfg.DB().Create(&perm).Error; err != nil {
|
|
|
return fmt.Errorf("failed to create permission %s: %w", permDef.ID, err)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 2. 创建系统预设角色(跳过 _app_info)
|
|
|
for roleCode := range a.roleDefs {
|
|
|
if roleCode == "_app_info" {
|
|
|
continue
|
|
|
}
|
|
|
if err := a.initRole(roleCode); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
// extractPermissions 从角色定义中提取所有权限
|
|
|
func (a *appAuth) extractPermissions() []models.Permission {
|
|
|
permMap := make(map[string]models.Permission)
|
|
|
|
|
|
for roleCode, policies := range a.policies {
|
|
|
if roleCode == "_app_info" {
|
|
|
continue
|
|
|
}
|
|
|
for _, policy := range policies {
|
|
|
resource, action := policy[0], policy[1]
|
|
|
// 跳过通配符权限的特殊处理
|
|
|
if resource == "*" && action == "*" {
|
|
|
continue
|
|
|
}
|
|
|
permID := fmt.Sprintf("%s:%s:%s", a.scope, resource, action)
|
|
|
|
|
|
if _, exists := permMap[permID]; !exists {
|
|
|
permMap[permID] = models.Permission{
|
|
|
ID: permID,
|
|
|
Scope: a.scope,
|
|
|
Resource: resource,
|
|
|
Action: action,
|
|
|
Description: fmt.Sprintf("%s %s on %s", a.scope, action, resource),
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
result := make([]models.Permission, 0, len(permMap))
|
|
|
for _, perm := range permMap {
|
|
|
result = append(result, perm)
|
|
|
}
|
|
|
return result
|
|
|
}
|
|
|
|
|
|
// initRole 初始化系统预设角色
|
|
|
func (a *appAuth) initRole(roleCode string) error {
|
|
|
roleDef, exists := a.roleDefs[roleCode]
|
|
|
if !exists {
|
|
|
return fmt.Errorf("role not found: %s", roleCode)
|
|
|
}
|
|
|
policies, hasPolicies := a.policies[roleCode]
|
|
|
if !hasPolicies {
|
|
|
policies = [][2]string{}
|
|
|
}
|
|
|
|
|
|
// 查找或创建系统角色
|
|
|
var role models.Role
|
|
|
err := cfg.DB().Where("code = ? AND org_id IS NULL", roleDef.code).First(&role).Error
|
|
|
if err != nil {
|
|
|
// 创建新角色
|
|
|
role = models.Role{
|
|
|
OrgID: nil,
|
|
|
Code: roleDef.code,
|
|
|
Name: roleDef.name,
|
|
|
Description: roleDef.description,
|
|
|
IsSystem: true,
|
|
|
Status: 1,
|
|
|
}
|
|
|
if err := cfg.DB().Create(&role).Error; err != nil {
|
|
|
return fmt.Errorf("failed to create role %s: %w", roleDef.code, err)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 同步角色权限
|
|
|
hasWildcard := false
|
|
|
for _, policy := range policies {
|
|
|
resource, action := policy[0], policy[1]
|
|
|
// 处理通配符权限
|
|
|
if resource == "*" && action == "*" {
|
|
|
hasWildcard = true
|
|
|
continue
|
|
|
}
|
|
|
permID := fmt.Sprintf("%s:%s:%s", a.scope, resource, action)
|
|
|
|
|
|
// 检查关联是否存在
|
|
|
var count int64
|
|
|
cfg.DB().Model(&models.RolePermission{}).
|
|
|
Where("role_id = ? AND permission_id = ?", role.ID, permID).
|
|
|
Count(&count)
|
|
|
|
|
|
if count == 0 {
|
|
|
rp := models.RolePermission{
|
|
|
RoleID: role.ID,
|
|
|
PermissionID: permID,
|
|
|
Condition: "none",
|
|
|
}
|
|
|
if err := cfg.DB().Create(&rp).Error; err != nil {
|
|
|
return fmt.Errorf("failed to create role permission: %w", err)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 为通配符权限创建记录
|
|
|
if hasWildcard {
|
|
|
wildcardPermID := fmt.Sprintf("%s:*:*", a.scope)
|
|
|
|
|
|
// 先确保通配符 permission 存在
|
|
|
var perm models.Permission
|
|
|
err := cfg.DB().Where("id = ?", wildcardPermID).First(&perm).Error
|
|
|
if err != nil {
|
|
|
// 创建通配符 permission
|
|
|
perm = models.Permission{
|
|
|
ID: wildcardPermID,
|
|
|
Scope: a.scope,
|
|
|
Resource: "*",
|
|
|
Action: "*",
|
|
|
Description: fmt.Sprintf("%s wildcard permission", a.scope),
|
|
|
}
|
|
|
if err := cfg.DB().Create(&perm).Error; err != nil {
|
|
|
return fmt.Errorf("failed to create wildcard permission: %w", err)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 创建 role_permission 关联
|
|
|
var count int64
|
|
|
cfg.DB().Model(&models.RolePermission{}).
|
|
|
Where("role_id = ? AND permission_id = ?", role.ID, wildcardPermID).
|
|
|
Count(&count)
|
|
|
if count == 0 {
|
|
|
rp := models.RolePermission{
|
|
|
RoleID: role.ID,
|
|
|
PermissionID: wildcardPermID,
|
|
|
Condition: "none",
|
|
|
}
|
|
|
if err := cfg.DB().Create(&rp).Error; err != nil {
|
|
|
return fmt.Errorf("failed to create wildcard role permission: %w", err)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
a.roleInitDone[roleCode] = true
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
// ========== 中间件实现 ==========
|
|
|
|
|
|
func (a *appAuth) Perm(permissionID string) func(*vigo.X) error {
|
|
|
validatePermissionID(permissionID)
|
|
|
return func(x *vigo.X) error {
|
|
|
userID := getUserID(x)
|
|
|
if userID == "" {
|
|
|
return vigo.ErrUnauthorized
|
|
|
}
|
|
|
|
|
|
orgID := getOrgID(x)
|
|
|
if err := a.checkPermission(x.Context(), userID, orgID, permissionID, ""); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func (a *appAuth) PermWithOwner(permissionID, ownerKey string) func(*vigo.X) error {
|
|
|
validatePermissionID(permissionID)
|
|
|
return func(x *vigo.X) error {
|
|
|
userID := getUserID(x)
|
|
|
if userID == "" {
|
|
|
return vigo.ErrUnauthorized
|
|
|
}
|
|
|
|
|
|
orgID := getOrgID(x)
|
|
|
|
|
|
// 获取资源所有者ID
|
|
|
// 优先从Path/Query获取,因为Context中的可能是登录用户ID
|
|
|
ownerID := x.PathParams.Get(ownerKey)
|
|
|
if ownerID == "" {
|
|
|
ownerID = x.Request.URL.Query().Get(ownerKey)
|
|
|
}
|
|
|
if ownerID == "" {
|
|
|
ownerID, _ = x.Get(ownerKey).(string)
|
|
|
}
|
|
|
|
|
|
// 如果是所有者,直接放行
|
|
|
if ownerID == userID {
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
// 不是所有者,检查是否有权限
|
|
|
if err := a.checkPermission(x.Context(), userID, orgID, permissionID, ""); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func (a *appAuth) PermOnResource(permissionID, resourceKey string) func(*vigo.X) error {
|
|
|
validatePermissionID(permissionID)
|
|
|
return func(x *vigo.X) error {
|
|
|
userID := getUserID(x)
|
|
|
if userID == "" {
|
|
|
return vigo.ErrUnauthorized
|
|
|
}
|
|
|
|
|
|
orgID := getOrgID(x)
|
|
|
|
|
|
// 尝试从 PathParams 获取
|
|
|
resourceID := x.PathParams.Get(resourceKey)
|
|
|
if resourceID == "" {
|
|
|
// 尝试从 Query 获取
|
|
|
resourceID = x.Request.URL.Query().Get(resourceKey)
|
|
|
}
|
|
|
|
|
|
if err := a.checkPermission(x.Context(), userID, orgID, permissionID, resourceID); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 内部辅助检查方法,返回 error 以便于统一处理错误响应
|
|
|
func (a *appAuth) checkPermission(ctx context.Context, userID, orgID, permissionID, resourceID string) error {
|
|
|
ok, err := a.CheckPermission(ctx, userID, orgID, permissionID, resourceID)
|
|
|
if err != nil {
|
|
|
return vigo.ErrInternalServer.WithError(err)
|
|
|
}
|
|
|
if !ok {
|
|
|
return vigo.ErrForbidden
|
|
|
}
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func (a *appAuth) PermAny(permissionIDs ...string) func(*vigo.X) error {
|
|
|
for _, pid := range permissionIDs {
|
|
|
validatePermissionID(pid)
|
|
|
}
|
|
|
return func(x *vigo.X) error {
|
|
|
userID := getUserID(x)
|
|
|
if userID == "" {
|
|
|
return vigo.ErrUnauthorized
|
|
|
}
|
|
|
|
|
|
orgID := getOrgID(x)
|
|
|
var lastErr error
|
|
|
for _, pid := range permissionIDs {
|
|
|
if err := a.checkPermission(x.Context(), userID, orgID, pid, ""); err == nil {
|
|
|
return nil
|
|
|
} else {
|
|
|
lastErr = err
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if lastErr != nil {
|
|
|
// 如果是 Forbidden 错误,返回 Forbidden
|
|
|
// 否则返回最后一个错误
|
|
|
// 这里简单处理,如果所有都失败,返回 Forbidden
|
|
|
return vigo.ErrForbidden
|
|
|
}
|
|
|
return vigo.ErrForbidden
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func (a *appAuth) PermAll(permissionIDs ...string) func(*vigo.X) error {
|
|
|
for _, pid := range permissionIDs {
|
|
|
validatePermissionID(pid)
|
|
|
}
|
|
|
return func(x *vigo.X) error {
|
|
|
userID := getUserID(x)
|
|
|
if userID == "" {
|
|
|
return vigo.ErrUnauthorized
|
|
|
}
|
|
|
|
|
|
orgID := getOrgID(x)
|
|
|
|
|
|
for _, pid := range permissionIDs {
|
|
|
if err := a.checkPermission(x.Context(), userID, orgID, pid, ""); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
}
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ========== 权限管理实现 ==========
|
|
|
|
|
|
func (a *appAuth) GrantRole(ctx context.Context, userID, orgID, roleCode string) error {
|
|
|
// 查找角色
|
|
|
var role models.Role
|
|
|
query := cfg.DB().Where("code = ?", roleCode)
|
|
|
if orgID != "" {
|
|
|
query = query.Where("org_id = ?", orgID)
|
|
|
} else {
|
|
|
query = query.Where("org_id IS NULL")
|
|
|
}
|
|
|
|
|
|
if err := query.First(&role).Error; err != nil {
|
|
|
// 如果指定了 OrgID 但没找到,尝试查找全局角色
|
|
|
if orgID != "" {
|
|
|
query = cfg.DB().Where("code = ? AND org_id IS NULL", roleCode)
|
|
|
if err := query.First(&role).Error; err != nil {
|
|
|
return fmt.Errorf("role not found: %s", roleCode)
|
|
|
}
|
|
|
} else {
|
|
|
return fmt.Errorf("role not found: %s", roleCode)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 检查是否已存在
|
|
|
var count int64
|
|
|
roleQuery := cfg.DB().Model(&models.UserRole{}).
|
|
|
Where("user_id = ? AND role_id = ?", userID, role.ID)
|
|
|
if orgID != "" {
|
|
|
roleQuery = roleQuery.Where("org_id = ?", orgID)
|
|
|
} else {
|
|
|
roleQuery = roleQuery.Where("org_id IS NULL")
|
|
|
}
|
|
|
roleQuery.Count(&count)
|
|
|
|
|
|
if count > 0 {
|
|
|
return nil // 已存在
|
|
|
}
|
|
|
|
|
|
var orgIDPtr *string
|
|
|
if orgID != "" {
|
|
|
orgIDPtr = &orgID
|
|
|
}
|
|
|
userRole := models.UserRole{
|
|
|
UserID: userID,
|
|
|
OrgID: orgIDPtr,
|
|
|
RoleID: role.ID,
|
|
|
ExpireAt: nil, // 默认不过期
|
|
|
}
|
|
|
|
|
|
if err := cfg.DB().Create(&userRole).Error; err != nil {
|
|
|
return err
|
|
|
}
|
|
|
incUserPermVersion(userID)
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func (a *appAuth) RevokeRole(ctx context.Context, userID, orgID, roleCode string) error {
|
|
|
var role models.Role
|
|
|
// 优先查找组织特定角色
|
|
|
query := cfg.DB().Where("code = ?", roleCode)
|
|
|
if orgID != "" {
|
|
|
query = query.Where("org_id = ?", orgID)
|
|
|
} else {
|
|
|
query = query.Where("org_id IS NULL")
|
|
|
}
|
|
|
|
|
|
if err := query.First(&role).Error; err != nil {
|
|
|
// 如果没找到,尝试查找全局角色
|
|
|
if orgID != "" {
|
|
|
if err := cfg.DB().Where("code = ? AND org_id IS NULL", roleCode).First(&role).Error; err != nil {
|
|
|
return nil // 角色不存在,无需撤销
|
|
|
}
|
|
|
} else {
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 构建删除条件
|
|
|
deleteQuery := cfg.DB().Where("user_id = ? AND role_id = ?", userID, role.ID)
|
|
|
if orgID != "" {
|
|
|
deleteQuery = deleteQuery.Where("org_id = ?", orgID)
|
|
|
} else {
|
|
|
deleteQuery = deleteQuery.Where("org_id IS NULL")
|
|
|
}
|
|
|
if err := deleteQuery.Delete(&models.UserRole{}).Error; err != nil {
|
|
|
return err
|
|
|
}
|
|
|
incUserPermVersion(userID)
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func (a *appAuth) GrantResourcePerm(ctx context.Context, userID, orgID, permissionID, resourceID string) error {
|
|
|
if strings.Count(permissionID, ":") == 1 {
|
|
|
permissionID = fmt.Sprintf("%s:%s", a.scope, permissionID)
|
|
|
}
|
|
|
// 检查权限是否存在
|
|
|
var perm models.Permission
|
|
|
if err := cfg.DB().Where("id = ?", permissionID).First(&perm).Error; err != nil {
|
|
|
return fmt.Errorf("permission not found: %s", permissionID)
|
|
|
}
|
|
|
|
|
|
// 检查是否已存在
|
|
|
var existing models.UserPermission
|
|
|
query := cfg.DB().Where("user_id = ? AND permission_id = ? AND resource_id = ?",
|
|
|
userID, permissionID, resourceID)
|
|
|
if orgID != "" {
|
|
|
query = query.Where("org_id = ?", orgID)
|
|
|
} else {
|
|
|
query = query.Where("org_id IS NULL")
|
|
|
}
|
|
|
err := query.First(&existing).Error
|
|
|
|
|
|
if err == nil {
|
|
|
// 已存在
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
var orgIDPtr *string
|
|
|
if orgID != "" {
|
|
|
orgIDPtr = &orgID
|
|
|
}
|
|
|
userPerm := models.UserPermission{
|
|
|
UserID: userID,
|
|
|
OrgID: orgIDPtr,
|
|
|
PermissionID: permissionID,
|
|
|
ResourceID: resourceID,
|
|
|
ExpireAt: nil, // 默认不过期
|
|
|
GrantedBy: "", // 默认空
|
|
|
}
|
|
|
|
|
|
if err := cfg.DB().Create(&userPerm).Error; err != nil {
|
|
|
return err
|
|
|
}
|
|
|
incUserPermVersion(userID)
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func (a *appAuth) RevokeResourcePerm(ctx context.Context, userID, orgID, permissionID, resourceID string) error {
|
|
|
if strings.Count(permissionID, ":") == 1 {
|
|
|
permissionID = fmt.Sprintf("%s:%s", a.scope, permissionID)
|
|
|
}
|
|
|
query := cfg.DB().Where("user_id = ? AND permission_id = ? AND resource_id = ?",
|
|
|
userID, permissionID, resourceID)
|
|
|
if orgID != "" {
|
|
|
query = query.Where("org_id = ?", orgID)
|
|
|
} else {
|
|
|
query = query.Where("org_id IS NULL")
|
|
|
}
|
|
|
if err := query.Delete(&models.UserPermission{}).Error; err != nil {
|
|
|
return err
|
|
|
}
|
|
|
incUserPermVersion(userID)
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func (a *appAuth) RevokeAll(ctx context.Context, userID, orgID string) error {
|
|
|
// 删除用户角色
|
|
|
roleQuery := cfg.DB().Where("user_id = ?", userID)
|
|
|
if orgID != "" {
|
|
|
roleQuery = roleQuery.Where("org_id = ?", orgID)
|
|
|
} else {
|
|
|
roleQuery = roleQuery.Where("org_id IS NULL")
|
|
|
}
|
|
|
if err := roleQuery.Delete(&models.UserRole{}).Error; err != nil {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
// 删除用户特定权限
|
|
|
permQuery := cfg.DB().Where("user_id = ?", userID)
|
|
|
if orgID != "" {
|
|
|
permQuery = permQuery.Where("org_id = ?", orgID)
|
|
|
} else {
|
|
|
permQuery = permQuery.Where("org_id IS NULL")
|
|
|
}
|
|
|
if err := permQuery.Delete(&models.UserPermission{}).Error; err != nil {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
incUserPermVersion(userID)
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
// ========== 权限查询实现 ==========
|
|
|
|
|
|
func (a *appAuth) CheckPermission(ctx context.Context, userID, orgID, permissionID, resourceID string) (bool, error) {
|
|
|
if strings.Count(permissionID, ":") == 1 {
|
|
|
permissionID = fmt.Sprintf("%s:%s", a.scope, permissionID)
|
|
|
}
|
|
|
|
|
|
// Check cache
|
|
|
var cacheKey string
|
|
|
if cache.IsEnabled() {
|
|
|
ver := getUserPermVersion(userID)
|
|
|
cacheKey = fmt.Sprintf("auth:check:%s:%s:%s:%s:%s", userID, ver, orgID, permissionID, resourceID)
|
|
|
if val, err := cache.Get(cacheKey); err == nil {
|
|
|
return val == "1", nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
result, err := a.checkPermissionDB(ctx, userID, orgID, permissionID, resourceID)
|
|
|
if err != nil {
|
|
|
return false, err
|
|
|
}
|
|
|
|
|
|
// Cache result
|
|
|
if cache.IsEnabled() {
|
|
|
val := "0"
|
|
|
if result {
|
|
|
val = "1"
|
|
|
}
|
|
|
cache.Set(cacheKey, val, 5*time.Minute)
|
|
|
}
|
|
|
|
|
|
return result, nil
|
|
|
}
|
|
|
|
|
|
func (a *appAuth) checkPermissionDB(ctx context.Context, userID, orgID, permissionID, resourceID string) (bool, error) {
|
|
|
// 1. 检查用户是否有该权限的角色(包括当前组织角色和系统全局角色)
|
|
|
var roleIDs []string
|
|
|
roleQuery := cfg.DB().Model(&models.UserRole{}).
|
|
|
Where("user_id = ? AND (expire_at IS NULL OR expire_at > ?)", userID, time.Now())
|
|
|
|
|
|
if orgID != "" {
|
|
|
roleQuery = roleQuery.Where("org_id = ? OR org_id IS NULL", orgID)
|
|
|
} else {
|
|
|
roleQuery = roleQuery.Where("org_id IS NULL")
|
|
|
}
|
|
|
|
|
|
if err := roleQuery.Pluck("role_id", &roleIDs).Error; err != nil {
|
|
|
return false, err
|
|
|
}
|
|
|
|
|
|
if len(roleIDs) > 0 {
|
|
|
// 构造可能的通配符权限ID
|
|
|
permsToCheck := []string{permissionID}
|
|
|
parts := strings.Split(permissionID, ":")
|
|
|
if len(parts) == 3 {
|
|
|
// app:resource:*
|
|
|
permsToCheck = append(permsToCheck, fmt.Sprintf("%s:%s:*", parts[0], parts[1]))
|
|
|
// app:*:*
|
|
|
permsToCheck = append(permsToCheck, fmt.Sprintf("%s:*:*", parts[0]))
|
|
|
} else if len(parts) == 2 {
|
|
|
// resource:action -> appKey:resource:action
|
|
|
fullPermID := fmt.Sprintf("%s:%s", a.scope, permissionID)
|
|
|
permsToCheck = append(permsToCheck, fullPermID)
|
|
|
permsToCheck = append(permsToCheck, fmt.Sprintf("%s:%s:*", a.scope, parts[0]))
|
|
|
permsToCheck = append(permsToCheck, fmt.Sprintf("%s:*:*", a.scope))
|
|
|
}
|
|
|
|
|
|
// 检查这些角色是否有所需权限
|
|
|
var count int64
|
|
|
if err := cfg.DB().Model(&models.RolePermission{}).
|
|
|
Where("role_id IN ? AND permission_id IN ?", roleIDs, permsToCheck).
|
|
|
Count(&count).Error; err != nil {
|
|
|
return false, err
|
|
|
}
|
|
|
if count > 0 {
|
|
|
return true, nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 2. 检查用户是否有特定的资源权限
|
|
|
// 构造可能的通配符权限ID (同上)
|
|
|
permsToCheck := []string{permissionID}
|
|
|
parts := strings.Split(permissionID, ":")
|
|
|
if len(parts) == 3 {
|
|
|
permsToCheck = append(permsToCheck, fmt.Sprintf("%s:%s:*", parts[0], parts[1]))
|
|
|
permsToCheck = append(permsToCheck, fmt.Sprintf("%s:*:*", parts[0]))
|
|
|
} else if len(parts) == 2 {
|
|
|
// resource:action -> appKey:resource:action
|
|
|
fullPermID := fmt.Sprintf("%s:%s", a.scope, permissionID)
|
|
|
permsToCheck = append(permsToCheck, fullPermID)
|
|
|
permsToCheck = append(permsToCheck, fmt.Sprintf("%s:%s:*", a.scope, parts[0]))
|
|
|
permsToCheck = append(permsToCheck, fmt.Sprintf("%s:*:*", a.scope))
|
|
|
}
|
|
|
|
|
|
var userPermCount int64
|
|
|
query := cfg.DB().Model(&models.UserPermission{}).
|
|
|
Where("user_id = ? AND permission_id IN ? AND (expire_at IS NULL OR expire_at > ?)",
|
|
|
userID, permsToCheck, time.Now())
|
|
|
|
|
|
if orgID != "" {
|
|
|
query = query.Where("org_id = ?", orgID)
|
|
|
} else {
|
|
|
query = query.Where("org_id IS NULL")
|
|
|
}
|
|
|
|
|
|
if resourceID != "" {
|
|
|
query = query.Where("resource_id = ? OR resource_id = '*'", resourceID)
|
|
|
}
|
|
|
|
|
|
if err := query.Count(&userPermCount).Error; err != nil {
|
|
|
return false, err
|
|
|
}
|
|
|
|
|
|
return userPermCount > 0, nil
|
|
|
}
|
|
|
|
|
|
func (a *appAuth) ListUserPermissions(ctx context.Context, userID, orgID string) ([]models.UserPermissionResult, error) {
|
|
|
result := make([]models.UserPermissionResult, 0)
|
|
|
|
|
|
// 1. 获取用户角色对应的权限
|
|
|
var roleIDs []string
|
|
|
if err := cfg.DB().Model(&models.UserRole{}).
|
|
|
Where("user_id = ? AND org_id = ? AND (expire_at IS NULL OR expire_at > ?)",
|
|
|
userID, orgID, time.Now()).
|
|
|
Pluck("role_id", &roleIDs).Error; err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
|
|
|
if len(roleIDs) > 0 {
|
|
|
var permIDs []string
|
|
|
if err := cfg.DB().Model(&models.RolePermission{}).
|
|
|
Where("role_id IN ?", roleIDs).
|
|
|
Pluck("permission_id", &permIDs).Error; err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
|
|
|
for _, permID := range permIDs {
|
|
|
result = append(result, models.UserPermissionResult{
|
|
|
PermissionID: permID,
|
|
|
ResourceID: "*",
|
|
|
Actions: []string{"*"},
|
|
|
})
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 2. 获取用户特定资源权限
|
|
|
var userPerms []models.UserPermission
|
|
|
if err := cfg.DB().Where("user_id = ? AND org_id = ? AND (expire_at IS NULL OR expire_at > ?)",
|
|
|
userID, orgID, time.Now()).
|
|
|
Find(&userPerms).Error; err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
|
|
|
for _, up := range userPerms {
|
|
|
result = append(result, models.UserPermissionResult{
|
|
|
PermissionID: up.PermissionID,
|
|
|
ResourceID: up.ResourceID,
|
|
|
Actions: []string{"*"},
|
|
|
})
|
|
|
}
|
|
|
|
|
|
return result, nil
|
|
|
}
|
|
|
|
|
|
func (a *appAuth) ListResourceUsers(ctx context.Context, orgID, permissionID, resourceID string) ([]models.ResourceUser, error) {
|
|
|
result := make([]models.ResourceUser, 0)
|
|
|
|
|
|
// 查询有该资源权限的用户
|
|
|
var userPerms []models.UserPermission
|
|
|
query := cfg.DB().Where("org_id = ? AND permission_id = ?", orgID, permissionID)
|
|
|
if resourceID != "" {
|
|
|
query = query.Where("resource_id = ? OR resource_id = '*'", resourceID)
|
|
|
}
|
|
|
|
|
|
if err := query.Find(&userPerms).Error; err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
|
|
|
userMap := make(map[string][]string)
|
|
|
for _, up := range userPerms {
|
|
|
userMap[up.UserID] = append(userMap[up.UserID], "*")
|
|
|
}
|
|
|
|
|
|
for userID, actions := range userMap {
|
|
|
result = append(result, models.ResourceUser{
|
|
|
UserID: userID,
|
|
|
Actions: actions,
|
|
|
})
|
|
|
}
|
|
|
|
|
|
return result, nil
|
|
|
}
|
|
|
|
|
|
// ========== 辅助方法 ==========
|
|
|
|
|
|
func (a *appAuth) isAdmin(ctx context.Context, userID, orgID string) (bool, error) {
|
|
|
// 检查用户是否有管理员角色
|
|
|
var adminRoleIDs []string
|
|
|
if err := cfg.DB().Model(&models.Role{}).
|
|
|
Where("code = 'admin'").
|
|
|
Pluck("id", &adminRoleIDs).Error; err != nil {
|
|
|
return false, err
|
|
|
}
|
|
|
|
|
|
if len(adminRoleIDs) == 0 {
|
|
|
return false, nil
|
|
|
}
|
|
|
|
|
|
var count int64
|
|
|
if err := cfg.DB().Model(&models.UserRole{}).
|
|
|
Where("user_id = ? AND org_id = ? AND role_id IN ?", userID, orgID, adminRoleIDs).
|
|
|
Count(&count).Error; err != nil {
|
|
|
return false, err
|
|
|
}
|
|
|
|
|
|
return count > 0, nil
|
|
|
}
|
|
|
|
|
|
// 从 context 获取用户ID
|
|
|
func getUserID(x *vigo.X) string {
|
|
|
if userID, ok := x.Get("user_id").(string); ok {
|
|
|
return userID
|
|
|
}
|
|
|
return ""
|
|
|
}
|
|
|
|
|
|
// 从 context 获取组织ID
|
|
|
func getOrgID(x *vigo.X) string {
|
|
|
if orgID, ok := x.Get("org_id").(string); ok {
|
|
|
return orgID
|
|
|
}
|
|
|
return ""
|
|
|
}
|
|
|
|
|
|
// ========== Cache Helpers ==========
|
|
|
|
|
|
func getUserPermVersion(userID string) string {
|
|
|
if !cache.IsEnabled() {
|
|
|
return "0"
|
|
|
}
|
|
|
key := fmt.Sprintf("auth:user_ver:%s", userID)
|
|
|
ver, err := cache.Client.Get(cache.Ctx, key).Result()
|
|
|
if err != nil {
|
|
|
return "0"
|
|
|
}
|
|
|
return ver
|
|
|
}
|
|
|
|
|
|
func incUserPermVersion(userID string) {
|
|
|
if !cache.IsEnabled() {
|
|
|
return
|
|
|
}
|
|
|
key := fmt.Sprintf("auth:user_ver:%s", userID)
|
|
|
cache.Client.Incr(cache.Ctx, key)
|
|
|
}
|