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

690 lines
18 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"
"strings"
"time"
"github.com/veypi/vbase/cfg"
"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]
}
auth := &appAuth{
appKey: appKey,
config: config,
}
f.apps[appKey] = auth
return auth
}
// 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 {
return func(x *vigo.X) error {
userID := getUserID(x)
if userID == "" {
return vigo.ErrUnauthorized
}
orgID := getOrgID(x)
ok, err := a.CheckPermission(x.Context(), userID, orgID, permissionID, "")
if err != nil {
return err
}
if !ok {
return vigo.ErrForbidden
}
return nil
}
}
func (a *appAuth) PermWithOwner(permissionID, ownerKey string) func(*vigo.X) error {
return func(x *vigo.X) error {
userID := getUserID(x)
if userID == "" {
return vigo.ErrUnauthorized
}
orgID := getOrgID(x)
// 先检查是否有权限
ok, err := a.CheckPermission(x.Context(), userID, orgID, permissionID, "")
if err != nil {
return err
}
if !ok {
return vigo.ErrForbidden
}
// 检查是否是所有者或管理员
ownerID, _ := x.Get(ownerKey).(string)
if ownerID == "" {
ownerID = x.PathParams.Get(ownerKey)
}
if ownerID == userID {
return nil
}
// 检查是否是管理员
isAdmin, _ := a.isAdmin(x.Context(), userID, orgID)
if isAdmin {
return nil
}
return vigo.ErrForbidden
}
}
func (a *appAuth) PermOnResource(permissionID, resourceKey string) func(*vigo.X) error {
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)
}
// 如果没有获取到 resourceID仍然进行检查 (resourceID="")
// 这意味着检查用户是否拥有该权限的一般访问权 (例如通过角色获得)
// 如果想要强制检查特定资源,调用方应该确保 resourceKey 能获取到值
ok, err := a.CheckPermission(x.Context(), userID, orgID, permissionID, resourceID)
if err != nil {
return err
}
if !ok {
return vigo.ErrForbidden
}
return nil
}
}
func (a *appAuth) PermAny(permissionIDs []string) func(*vigo.X) error {
return func(x *vigo.X) error {
userID := getUserID(x)
if userID == "" {
return vigo.ErrUnauthorized
}
orgID := getOrgID(x)
for _, permID := range permissionIDs {
ok, err := a.CheckPermission(x.Context(), userID, orgID, permID, "")
if err != nil {
return err
}
if ok {
return nil
}
}
return vigo.ErrForbidden
}
}
func (a *appAuth) PermAll(permissionIDs []string) func(*vigo.X) error {
return func(x *vigo.X) error {
userID := getUserID(x)
if userID == "" {
return vigo.ErrUnauthorized
}
orgID := getOrgID(x)
for _, permID := range permissionIDs {
ok, err := a.CheckPermission(x.Context(), userID, orgID, permID, "")
if err != nil {
return err
}
if !ok {
return vigo.ErrForbidden
}
}
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, // 默认不过期
}
return cfg.DB().Create(&userRole).Error
}
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
}
}
return cfg.DB().Where("user_id = ? AND org_id = ? AND role_id = ?", userID, orgID, role.ID).
Delete(&models.UserRole{}).Error
}
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: "", // 默认空
}
return cfg.DB().Create(&userPerm).Error
}
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)
}
return cfg.DB().Where("user_id = ? AND org_id = ? AND permission_id = ? AND resource_id = ?",
userID, orgID, permissionID, resourceID).
Delete(&models.UserPermission{}).Error
}
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
}
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)
}
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 ""
}