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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

//
// 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"
)
// 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
// ========== 权限管理 ==========
// 授予角色
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", models.AppConfig{
Name: "VBase",
Description: "VBase 基础设施",
DefaultRoles: []models.RoleDefinition{
{Code: "admin", Name: "管理员", Policies: []string{"*:*"}},
{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",
}},
},
})
)
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)
}
}
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))
}
}
// 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)
return func(x *vigo.X) error {
userID := getUserID(x)
if userID == "" {
return vigo.ErrNotAuthorized
}
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.ErrNotAuthorized
}
orgID := getOrgID(x)
// 检查是否有基本权限
if err := a.checkPermission(x.Context(), userID, orgID, permissionID, ""); err != nil {
return err
}
// 获取资源所有者ID
ownerID, _ := x.Get(ownerKey).(string)
if ownerID == "" {
ownerID = x.PathParams.Get(ownerKey)
}
// 如果是所有者,直接放行
if ownerID == userID {
return nil
}
// 如果不是所有者,且拥有全局管理权限(如admin),也可以放行
// 这里简化为再次检查是否有更高级别的权限,或者该权限本身隐含了管理权
// 实际上CheckPermission 已经检查了用户是否拥有该 permissionID
// 如果设计上 PermWithOwner 意味着 "所有者 OR 拥有该权限的管理员"
// 那么前面的 CheckPermission 已经保证了 "拥有该权限"
// 但通常 Owner 权限是针对特定资源的,而 CheckPermission 检查的是通用权限
// 这里逻辑稍微有点混淆,通常 PermWithOwner 意思是:
// 1. 用户必须登录
// 2. 如果用户是资源所有者,允许
// 3. 如果用户不是所有者,必须拥有特定权限 (permissionID)
// 修正逻辑:
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.ErrNotAuthorized
}
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.ErrNotAuthorized
}
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.ErrNotAuthorized
}
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 = ''")
}
if err := query.First(&role).Error; err != nil {
// 如果指定了 OrgID 但没找到,尝试查找全局角色
if orgID != "" {
query = cfg.DB().Where("code = ? AND org_id = ''", 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
cfg.DB().Model(&models.UserRole{}).
Where("user_id = ? AND org_id = ? AND role_id = ?", userID, orgID, role.ID).
Count(&count)
if count > 0 {
return nil // 已存在
}
userRole := models.UserRole{
UserID: userID,
OrgID: orgID,
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 = ''")
}
if err := query.First(&role).Error; err != nil {
// 如果没找到,尝试查找全局角色
if orgID != "" {
if err := cfg.DB().Where("code = ? AND org_id = ''", roleCode).First(&role).Error; err != nil {
return nil // 角色不存在,无需撤销
}
} else {
return nil
}
}
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
}
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)
}
// 检查权限是否存在
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
err := cfg.DB().Where("user_id = ? AND org_id = ? AND permission_id = ? AND resource_id = ?",
userID, orgID, permissionID, resourceID).
First(&existing).Error
if err == nil {
// 已存在
return nil
}
userPerm := models.UserPermission{
UserID: userID,
OrgID: orgID,
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.appKey, permissionID)
}
if err := cfg.DB().Where("user_id = ? AND org_id = ? AND permission_id = ? AND resource_id = ?",
userID, orgID, permissionID, resourceID).
Delete(&models.UserPermission{}).Error; err != nil {
return err
}
incUserPermVersion(userID)
return nil
}
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)
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.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) {
fmt.Printf("[DEBUG] CheckPermission: userID=%s, orgID=%s, permID=%s, resID=%s\n", userID, orgID, permissionID, resourceID)
// 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 = ''", 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)
return false, err
}
fmt.Printf("[DEBUG] CheckPermission: roleIDs=%v\n", roleIDs)
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]))
}
// 检查这些角色是否有所需权限
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
}
fmt.Printf("[DEBUG] CheckPermission: role perm count=%d checked=%v\n", count, permsToCheck)
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]))
}
var userPermCount int64
query := cfg.DB().Model(&models.UserPermission{}).
Where("user_id = ? AND org_id = ? AND permission_id IN ? AND (expire_at IS NULL OR expire_at > ?)",
userID, orgID, permsToCheck, time.Now())
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)
}