// // Copyright (C) 2024 veypi // 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 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) } } // 同步角色权限 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.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) // 检查是否有基本权限 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.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.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 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.appKey, 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.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. 检查用户是否有该权限的角色(包括当前组织角色和系统全局角色) 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])) } // 检查这些角色是否有所需权限 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])) } 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) }