You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
OneAuth/auth/auth.go

808 lines
22 KiB
Go

1 week ago
//
// 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"
1 week ago
"strings"
"time"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/libs/cache"
1 week ago
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
// Auth 权限管理接口
type Auth interface {
// ========== 中间件生成 ==========
// 基础权限检查
Perm(permissionID string) func(*vigo.X) error
// 资源所有者权限
PermWithOwner(permissionID, ownerKey string) func(*vigo.X) error
1 week ago
// 特定资源权限检查 (自动从 Path/Query 获取资源ID)
PermOnResource(permissionID, resourceKey string) func(*vigo.X) error
1 week ago
// 满足任一权限
PermAny(permissionIDs []string) func(*vigo.X) error
// 满足所有权限
PermAll(permissionIDs []string) func(*vigo.X) error
// ========== 权限管理 ==========
// 授予角色
1 week ago
GrantRole(ctx context.Context, userID, orgID, roleCode string) error
1 week ago
// 撤销角色
RevokeRole(ctx context.Context, userID, orgID, roleCode string) error
// 授予特定资源权限
1 week ago
GrantResourcePerm(ctx context.Context, userID, orgID, permissionID, resourceID string) error
1 week ago
// 撤销特定资源权限
RevokeResourcePerm(ctx context.Context, userID, orgID, permissionID, resourceID string) error
// 撤销用户所有权限
RevokeAll(ctx context.Context, userID, orgID string) error
// ========== 权限查询 ==========
// 检查权限
1 week ago
CheckPermission(ctx context.Context, userID, orgID, permissionID, resourceID string) (bool, error)
1 week ago
// 列出用户权限
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", models.AppConfig{
Name: "VBase",
Description: "VBase 基础设施",
DefaultRoles: []models.RoleDefinition{
{Code: "admin", Name: "管理员", Policies: []string{"*:*"}},
1 week ago
{Code: "user", Name: "普通用户", Policies: []string{
"user:read", "user:update",
"org:read", "org:create",
"oauth-client:read", "oauth-client:create", "oauth-client:update", "oauth-client:delete",
1 week ago
}},
1 week ago
},
})
)
type authFactory struct {
apps map[string]*appAuth // appKey -> auth实例
}
// New 创建权限管理实例(注册应用)
func (f *authFactory) New(appKey string, config models.AppConfig) Auth {
if _, exists := f.apps[appKey]; exists {
return f.apps[appKey]
}
// 验证默认角色中的权限格式
for _, role := range config.DefaultRoles {
for _, policy := range role.Policies {
validatePermissionID(policy)
}
}
1 week ago
auth := &appAuth{
appKey: appKey,
config: config,
}
f.apps[appKey] = 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))
}
}
1 week ago
// 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
}
// appAuth 单个应用的权限管理
type appAuth struct {
appKey string
config models.AppConfig
}
// 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. 创建系统预设角色
for _, roleDef := range a.config.DefaultRoles {
if err := a.initRole(roleDef); err != nil {
return err
}
}
return nil
}
// extractPermissions 从角色定义中提取所有权限
func (a *appAuth) extractPermissions() []models.Permission {
permMap := make(map[string]models.Permission)
for _, roleDef := range a.config.DefaultRoles {
for _, policy := range roleDef.Policies {
// policy 格式: "resource:action" 或 "*:*"
parts := strings.Split(policy, ":")
if len(parts) != 2 {
continue
}
resource, action := parts[0], parts[1]
permID := fmt.Sprintf("%s:%s:%s", a.appKey, resource, action)
if _, exists := permMap[permID]; !exists {
permMap[permID] = models.Permission{
ID: permID,
AppKey: a.appKey,
Resource: resource,
Action: action,
Description: fmt.Sprintf("%s %s on %s", a.config.Name, action, resource),
}
}
}
}
result := make([]models.Permission, 0, len(permMap))
for _, perm := range permMap {
result = append(result, perm)
}
return result
}
// initRole 初始化系统预设角色
func (a *appAuth) initRole(roleDef models.RoleDefinition) error {
// 查找或创建系统角色
var role models.Role
err := cfg.DB().Where("code = ? AND org_id = ''", roleDef.Code).First(&role).Error
if err != nil {
// 创建新角色
role = models.Role{
OrgID: "",
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)
}
}
// 同步角色权限
for _, policy := range roleDef.Policies {
parts := strings.Split(policy, ":")
if len(parts) != 2 {
continue
}
resource, action := parts[0], parts[1]
permID := fmt.Sprintf("%s:%s:%s", a.appKey, 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)
}
}
}
return nil
}
// ========== 中间件实现 ==========
func (a *appAuth) Perm(permissionID string) func(*vigo.X) error {
validatePermissionID(permissionID)
1 week ago
return func(x *vigo.X) error {
userID := getUserID(x)
if userID == "" {
return vigo.ErrNotAuthorized
1 week ago
}
orgID := getOrgID(x)
if err := a.checkPermission(x.Context(), userID, orgID, permissionID, ""); err != nil {
1 week ago
return err
}
return nil
}
}
func (a *appAuth) PermWithOwner(permissionID, ownerKey string) func(*vigo.X) error {
validatePermissionID(permissionID)
1 week ago
return func(x *vigo.X) error {
userID := getUserID(x)
if userID == "" {
return vigo.ErrNotAuthorized
1 week ago
}
orgID := getOrgID(x)
// 检查是否有基本权限
if err := a.checkPermission(x.Context(), userID, orgID, permissionID, ""); err != nil {
1 week ago
return err
}
// 获取资源所有者ID
1 week ago
ownerID, _ := x.Get(ownerKey).(string)
1 week ago
if ownerID == "" {
ownerID = x.PathParams.Get(ownerKey)
}
// 如果是所有者,直接放行
1 week ago
if ownerID == userID {
return nil
}
// 如果不是所有者,且拥有全局管理权限(如admin),也可以放行
// 这里简化为再次检查是否有更高级别的权限,或者该权限本身隐含了管理权
// 实际上CheckPermission 已经检查了用户是否拥有该 permissionID
// 如果设计上 PermWithOwner 意味着 "所有者 OR 拥有该权限的管理员"
// 那么前面的 CheckPermission 已经保证了 "拥有该权限"
// 但通常 Owner 权限是针对特定资源的,而 CheckPermission 检查的是通用权限
// 这里逻辑稍微有点混淆,通常 PermWithOwner 意思是:
// 1. 用户必须登录
// 2. 如果用户是资源所有者,允许
// 3. 如果用户不是所有者,必须拥有特定权限 (permissionID)
// 修正逻辑:
if ownerID == userID {
1 week ago
return nil
}
// 不是所有者,检查是否有权限
if err := a.checkPermission(x.Context(), userID, orgID, permissionID, ""); err != nil {
return err
}
return nil
1 week ago
}
}
1 week ago
func (a *appAuth) PermOnResource(permissionID, resourceKey string) func(*vigo.X) error {
validatePermissionID(permissionID)
1 week ago
return func(x *vigo.X) error {
userID := getUserID(x)
if userID == "" {
return vigo.ErrNotAuthorized
1 week ago
}
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 {
1 week ago
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
}
1 week ago
func (a *appAuth) PermAny(permissionIDs []string) func(*vigo.X) error {
for _, pid := range permissionIDs {
validatePermissionID(pid)
}
1 week ago
return func(x *vigo.X) error {
userID := getUserID(x)
if userID == "" {
return vigo.ErrNotAuthorized
1 week ago
}
orgID := getOrgID(x)
var lastErr error
for _, pid := range permissionIDs {
if err := a.checkPermission(x.Context(), userID, orgID, pid, ""); err == nil {
1 week ago
return nil
} else {
lastErr = err
1 week ago
}
}
if lastErr != nil {
// 如果是 Forbidden 错误,返回 Forbidden
// 否则返回最后一个错误
// 这里简单处理,如果所有都失败,返回 Forbidden
return vigo.ErrForbidden
}
1 week ago
return vigo.ErrForbidden
}
}
func (a *appAuth) PermAll(permissionIDs []string) func(*vigo.X) error {
for _, pid := range permissionIDs {
validatePermissionID(pid)
}
1 week ago
return func(x *vigo.X) error {
userID := getUserID(x)
if userID == "" {
return vigo.ErrNotAuthorized
1 week ago
}
orgID := getOrgID(x)
for _, pid := range permissionIDs {
if err := a.checkPermission(x.Context(), userID, orgID, pid, ""); err != nil {
1 week ago
return err
}
}
return nil
}
}
// ========== 权限管理实现 ==========
1 week ago
func (a *appAuth) GrantRole(ctx context.Context, userID, orgID, roleCode string) error {
1 week ago
// 查找角色
var role models.Role
1 week ago
query := cfg.DB().Where("code = ?", roleCode)
if orgID != "" {
query = query.Where("org_id = ?", orgID)
1 week ago
} else {
query = query.Where("org_id = ''")
}
if err := query.First(&role).Error; err != nil {
1 week ago
// 如果指定了 OrgID 但没找到,尝试查找全局角色
1 week ago
if orgID != "" {
query = cfg.DB().Where("code = ? AND org_id = ''", roleCode)
1 week ago
if err := query.First(&role).Error; err != nil {
1 week ago
return fmt.Errorf("role not found: %s", roleCode)
1 week ago
}
} else {
1 week ago
return fmt.Errorf("role not found: %s", roleCode)
1 week ago
}
1 week ago
}
// 检查是否已存在
var count int64
cfg.DB().Model(&models.UserRole{}).
1 week ago
Where("user_id = ? AND org_id = ? AND role_id = ?", userID, orgID, role.ID).
1 week ago
Count(&count)
if count > 0 {
return nil // 已存在
}
userRole := models.UserRole{
1 week ago
UserID: userID,
OrgID: orgID,
1 week ago
RoleID: role.ID,
1 week ago
ExpireAt: nil, // 默认不过期
1 week ago
}
if err := cfg.DB().Create(&userRole).Error; err != nil {
return err
}
incUserPermVersion(userID)
return nil
1 week ago
}
func (a *appAuth) RevokeRole(ctx context.Context, userID, orgID, roleCode string) error {
var role models.Role
1 week ago
// 优先查找组织特定角色
1 week ago
query := cfg.DB().Where("code = ?", roleCode)
if orgID != "" {
query = query.Where("org_id = ?", orgID)
} else {
query = query.Where("org_id = ''")
}
if err := query.First(&role).Error; err != nil {
1 week ago
// 如果没找到,尝试查找全局角色
if orgID != "" {
if err := cfg.DB().Where("code = ? AND org_id = ''", roleCode).First(&role).Error; err != nil {
return nil // 角色不存在,无需撤销
}
} else {
return nil
}
1 week ago
}
if err := cfg.DB().Where("user_id = ? AND org_id = ? AND role_id = ?", userID, orgID, role.ID).
Delete(&models.UserRole{}).Error; err != nil {
return err
}
incUserPermVersion(userID)
return nil
1 week ago
}
1 week ago
func (a *appAuth) GrantResourcePerm(ctx context.Context, userID, orgID, permissionID, resourceID string) error {
if strings.Count(permissionID, ":") == 1 {
permissionID = fmt.Sprintf("%s:%s", a.appKey, permissionID)
}
1 week ago
// 检查权限是否存在
var perm models.Permission
1 week ago
if err := cfg.DB().Where("id = ?", permissionID).First(&perm).Error; err != nil {
return fmt.Errorf("permission not found: %s", permissionID)
1 week ago
}
// 检查是否已存在
var existing models.UserPermission
err := cfg.DB().Where("user_id = ? AND org_id = ? AND permission_id = ? AND resource_id = ?",
1 week ago
userID, orgID, permissionID, resourceID).
1 week ago
First(&existing).Error
if err == nil {
1 week ago
// 已存在
return nil
1 week ago
}
userPerm := models.UserPermission{
1 week ago
UserID: userID,
OrgID: orgID,
PermissionID: permissionID,
ResourceID: resourceID,
ExpireAt: nil, // 默认不过期
GrantedBy: "", // 默认空
1 week ago
}
if err := cfg.DB().Create(&userPerm).Error; err != nil {
return err
}
incUserPermVersion(userID)
return nil
1 week ago
}
func (a *appAuth) RevokeResourcePerm(ctx context.Context, userID, orgID, permissionID, resourceID string) error {
1 week ago
if strings.Count(permissionID, ":") == 1 {
permissionID = fmt.Sprintf("%s:%s", a.appKey, permissionID)
}
if err := cfg.DB().Where("user_id = ? AND org_id = ? AND permission_id = ? AND resource_id = ?",
1 week ago
userID, orgID, permissionID, resourceID).
Delete(&models.UserPermission{}).Error; err != nil {
return err
}
incUserPermVersion(userID)
return nil
1 week ago
}
func (a *appAuth) RevokeAll(ctx context.Context, userID, orgID string) error {
// 删除用户角色
if err := cfg.DB().Where("user_id = ? AND org_id = ?", userID, orgID).
Delete(&models.UserRole{}).Error; err != nil {
return err
}
// 删除用户特定权限
if err := cfg.DB().Where("user_id = ? AND org_id = ?", userID, orgID).
Delete(&models.UserPermission{}).Error; err != nil {
return err
}
incUserPermVersion(userID)
1 week ago
return nil
}
// ========== 权限查询实现 ==========
1 week ago
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.appKey, 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 week ago
fmt.Printf("[DEBUG] CheckPermission: userID=%s, orgID=%s, permID=%s, resID=%s\n", userID, orgID, permissionID, resourceID)
// 1. 检查用户是否有该权限的角色(包括当前组织角色和系统全局角色)
1 week ago
var roleIDs []string
1 week ago
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 = ''", orgID)
} else {
roleQuery = roleQuery.Where("org_id = ''")
}
if err := roleQuery.Pluck("role_id", &roleIDs).Error; err != nil {
fmt.Printf("[DEBUG] CheckPermission: failed to get roles: %v\n", err)
1 week ago
return false, err
}
1 week ago
fmt.Printf("[DEBUG] CheckPermission: roleIDs=%v\n", roleIDs)
1 week ago
if len(roleIDs) > 0 {
1 week ago
// 构造可能的通配符权限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]))
}
1 week ago
// 检查这些角色是否有所需权限
var count int64
if err := cfg.DB().Model(&models.RolePermission{}).
1 week ago
Where("role_id IN ? AND permission_id IN ?", roleIDs, permsToCheck).
1 week ago
Count(&count).Error; err != nil {
return false, err
}
1 week ago
fmt.Printf("[DEBUG] CheckPermission: role perm count=%d checked=%v\n", count, permsToCheck)
1 week ago
if count > 0 {
return true, nil
}
}
// 2. 检查用户是否有特定的资源权限
1 week ago
// 构造可能的通配符权限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]))
}
1 week ago
var userPermCount int64
query := cfg.DB().Model(&models.UserPermission{}).
1 week ago
Where("user_id = ? AND org_id = ? AND permission_id IN ? AND (expire_at IS NULL OR expire_at > ?)",
userID, orgID, permsToCheck, time.Now())
1 week ago
1 week ago
if resourceID != "" {
query = query.Where("resource_id = ? OR resource_id = '*'", resourceID)
1 week ago
}
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)
}