v3
veypi 1 month ago
parent ced7cc6a07
commit 8fa01c4c52

@ -8,12 +8,10 @@
package api
import (
"github.com/veypi/vbase/api/auth"
apiAuth "github.com/veypi/vbase/api/auth"
"github.com/veypi/vbase/api/middleware"
"github.com/veypi/vbase/api/oauth"
"github.com/veypi/vbase/api/org"
"github.com/veypi/vbase/api/policy"
"github.com/veypi/vbase/api/role"
"github.com/veypi/vbase/api/user"
"github.com/veypi/vigo"
"github.com/veypi/vigo/contrib/common"
@ -28,12 +26,10 @@ func init() {
Router.After(common.JsonResponse, common.JsonErrorResponse)
// 子路由挂载
Router.Extend("/auth", auth.Router)
Router.Extend("/auth", apiAuth.Router)
Router.Extend("/users", user.Router)
Router.Extend("/orgs", org.Router)
Router.Extend("/oauth", oauth.Router)
Router.Extend("/policies", policy.Router)
Router.Extend("/roles", role.Router)
// 404 处理
Router.Any("/**", vigo.SkipBefore, "拦截未注册的api请求返回404", func(x *vigo.X) error {

@ -1,162 +0,0 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-03-04 16:08:06
// Distributed under terms of the MIT license.
//
package middleware
import (
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
)
// InitOrgPolicies 为组织初始化默认策略和角色
func InitOrgPolicies(orgID string) error {
// 创建默认策略
policies := getDefaultPolicies()
for _, policy := range policies {
var count int64
cfg.DB().Model(&models.Policy{}).Where("code = ?", policy.Code).Count(&count)
if count == 0 {
if err := cfg.DB().Create(&policy).Error; err != nil {
return err
}
}
}
// 创建默认角色
roles := getDefaultRoles()
for _, role := range roles {
role.OrgID = orgID
var count int64
cfg.DB().Model(&models.Role{}).Where("code = ? AND org_id = ?", role.Code, orgID).Count(&count)
if count == 0 {
if err := cfg.DB().Create(&role).Error; err != nil {
return err
}
}
}
return nil
}
// getDefaultPolicies 获取默认策略列表
func getDefaultPolicies() []models.Policy {
return []models.Policy{
{
Code: "user:read",
Name: "读取用户信息",
Resource: "user",
Action: "read",
Effect: models.PolicyEffectAllow,
Scope: models.PolicyScopeOrg,
},
{
Code: "user:update",
Name: "更新用户信息",
Resource: "user",
Action: "update",
Effect: models.PolicyEffectAllow,
Condition: "owner",
Scope: models.PolicyScopeOrg,
},
{
Code: "role:manage",
Name: "管理角色",
Resource: "role",
Action: "*",
Effect: models.PolicyEffectAllow,
Condition: "admin",
Scope: models.PolicyScopeOrg,
},
{
Code: "policy:manage",
Name: "管理策略",
Resource: "policy",
Action: "*",
Effect: models.PolicyEffectAllow,
Condition: "admin",
Scope: models.PolicyScopeOrg,
},
{
Code: "org:read",
Name: "读取组织信息",
Resource: "org",
Action: "read",
Effect: models.PolicyEffectAllow,
Scope: models.PolicyScopeOrg,
},
{
Code: "org:update",
Name: "更新组织信息",
Resource: "org",
Action: "update",
Effect: models.PolicyEffectAllow,
Condition: "admin",
Scope: models.PolicyScopeOrg,
},
{
Code: "org:delete",
Name: "删除组织",
Resource: "org",
Action: "delete",
Effect: models.PolicyEffectAllow,
Condition: "owner",
Scope: models.PolicyScopeOrg,
},
{
Code: "member:manage",
Name: "管理成员",
Resource: "org_member",
Action: "*",
Effect: models.PolicyEffectAllow,
Condition: "admin",
Scope: models.PolicyScopeOrg,
},
{
Code: "resource:read",
Name: "读取资源",
Resource: "resource",
Action: "read",
Effect: models.PolicyEffectAllow,
Scope: models.PolicyScopeOrg,
},
{
Code: "resource:write",
Name: "写入资源",
Resource: "resource",
Action: "create,update,delete",
Effect: models.PolicyEffectAllow,
Condition: "owner",
Scope: models.PolicyScopeOrg,
},
}
}
// getDefaultRoles 获取默认角色列表
func getDefaultRoles() []models.Role {
return []models.Role{
{
Name: "管理员",
Code: models.RoleCodeAdmin,
Description: "组织管理员,可以管理成员、角色和策略",
Scope: models.PolicyScopeOrg,
IsSystem: true,
},
{
Name: "开发者",
Code: models.RoleCodeDeveloper,
Description: "开发者,可以创建和管理资源",
Scope: models.PolicyScopeOrg,
IsSystem: true,
},
{
Name: "只读用户",
Code: models.RoleCodeViewer,
Description: "只读访问权限",
Scope: models.PolicyScopeOrg,
IsSystem: true,
},
}
}

@ -1,224 +0,0 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-03-04 16:08:06
// Distributed under terms of the MIT license.
//
package middleware
import (
"strings"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
// PolicyEvaluator 策略评估器
type PolicyEvaluator struct {
checker *Checker
}
// NewPolicyEvaluator 创建策略评估器
func (c *Checker) NewPolicyEvaluator() *PolicyEvaluator {
return &PolicyEvaluator{checker: c}
}
// PermissionCheck 权限检查参数
type PermissionCheck struct {
Resource string // 资源: user, org, role, policy, project, etc.
Action string // 操作: create, read, update, delete, list, etc.
OrgID string // 组织ID可选
OwnerID string // 资源所有者ID用于条件判断
}
// HasPermission 检查是否有权限
func (pe *PolicyEvaluator) HasPermission(check PermissionCheck) bool {
if pe.checker.userID == "" {
return false
}
// 平台超级管理员拥有所有权限
if pe.isPlatformAdmin() {
return true
}
// 获取用户所有策略
policies := pe.getUserPolicies(check.OrgID)
// 按优先级排序deny > allow
// 先检查 deny 策略
for _, policy := range policies {
if policy.Effect != models.PolicyEffectDeny {
continue
}
if pe.matchPolicy(policy, check) {
return false
}
}
// 再检查 allow 策略
for _, policy := range policies {
if policy.Effect != models.PolicyEffectAllow {
continue
}
if pe.matchPolicy(policy, check) {
return true
}
}
return false
}
// isPlatformAdmin 检查是否为平台管理员
func (pe *PolicyEvaluator) isPlatformAdmin() bool {
return pe.checker.IsOrgOwner()
}
// getUserPolicies 获取用户拥有的所有策略
func (pe *PolicyEvaluator) getUserPolicies(orgID string) []models.Policy {
var policies []models.Policy
roles := pe.checker.GetUserRoles()
if len(roles) == 0 {
return policies
}
for _, roleID := range roles {
var role models.Role
if err := cfg.DB().First(&role, "id = ?", roleID).Error; err != nil {
continue
}
if role.PolicyIDs == "" {
continue
}
policyIDs := strings.Split(role.PolicyIDs, ",")
var rolePolicies []models.Policy
if err := cfg.DB().Where("id IN ?", policyIDs).Find(&rolePolicies).Error; err != nil {
continue
}
policies = append(policies, rolePolicies...)
}
return policies
}
// matchPolicy 匹配策略
func (pe *PolicyEvaluator) matchPolicy(policy models.Policy, check PermissionCheck) bool {
if !matchPattern(policy.Resource, check.Resource) {
return false
}
if !matchPattern(policy.Action, check.Action) {
return false
}
if policy.Condition != "" {
if !pe.checkCondition(policy.Condition, check) {
return false
}
}
return true
}
// matchPattern 模式匹配(支持通配符 *
func matchPattern(pattern, value string) bool {
if pattern == "*" {
return true
}
if pattern == value {
return true
}
if strings.HasSuffix(pattern, ":*") {
prefix := strings.TrimSuffix(pattern, ":*")
return strings.HasPrefix(value, prefix+":")
}
return false
}
// checkCondition 检查条件
func (pe *PolicyEvaluator) checkCondition(condition string, check PermissionCheck) bool {
conditions := strings.Split(condition, ",")
for _, c := range conditions {
c = strings.TrimSpace(c)
switch c {
case "owner":
if check.OwnerID != "" && pe.checker.UserID() == check.OwnerID {
return true
}
case "org_member":
if pe.checker.OrgID() != "" {
return true
}
case "admin":
if pe.checker.IsOrgAdmin() {
return true
}
}
}
return false
}
// RequirePermission 要求特定权限
func (c *Checker) RequirePermission(resource, action string) error {
pe := c.NewPolicyEvaluator()
if !pe.HasPermission(PermissionCheck{
Resource: resource,
Action: action,
OrgID: c.OrgID(),
}) {
return vigo.ErrForbidden.WithString("permission denied: " + resource + ":" + action)
}
return nil
}
// Permission 基于策略的权限检查中间件
func Permission(resource, action string) func(*vigo.X) error {
return func(x *vigo.X) error {
checker := NewChecker(x)
return checker.RequirePermission(resource, action)
}
}
// PermissionWithOwner 带资源所有者检查的权限中间件
func PermissionWithOwner(resource, action, ownerIDKey string) func(*vigo.X) error {
return func(x *vigo.X) error {
checker := NewChecker(x)
ownerID := ""
if oid, ok := x.Get(ownerIDKey).(string); ok {
ownerID = oid
}
if ownerID != "" && checker.UserID() == ownerID {
return nil
}
return checker.RequirePermission(resource, action)
}
}
// AdminOrOwner 管理员或所有者权限
func AdminOrOwner(ownerIDKey string) func(*vigo.X) error {
return func(x *vigo.X) error {
checker := NewChecker(x)
if checker.IsOrgAdmin() {
return nil
}
ownerID := ""
if oid, ok := x.Get(ownerIDKey).(string); ok {
ownerID = oid
}
if ownerID != "" && checker.UserID() == ownerID {
return nil
}
return vigo.ErrForbidden.WithString("admin or owner permission required")
}
}

@ -1,57 +0,0 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-03-04 16:08:06
// Distributed under terms of the MIT license.
//
package policy
import (
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type CreateRequest struct {
Code string `json:"code" src:"json" desc:"策略代码"`
Name string `json:"name" src:"json" desc:"策略名称"`
Description string `json:"description,omitempty" src:"json" desc:"描述"`
Resource string `json:"resource" src:"json" desc:"资源: user/org/resource/*"`
Action string `json:"action" src:"json" desc:"操作: create/read/update/delete/*"`
Effect string `json:"effect" src:"json" desc:"效果: allow/deny"`
Condition string `json:"condition,omitempty" src:"json" desc:"条件: owner/org_member"`
Scope string `json:"scope" src:"json" desc:"作用域: platform/org/resource"`
}
func create(x *vigo.X, req *CreateRequest) (*models.Policy, error) {
// 检查代码是否已存在
var count int64
cfg.DB().Model(&models.Policy{}).Where("code = ?", req.Code).Count(&count)
if count > 0 {
return nil, vigo.ErrArgInvalid.WithString("policy code already exists")
}
policy := &models.Policy{
Code: req.Code,
Name: req.Name,
Description: req.Description,
Resource: req.Resource,
Action: req.Action,
Effect: req.Effect,
Condition: req.Condition,
Scope: req.Scope,
}
if policy.Effect == "" {
policy.Effect = models.PolicyEffectAllow
}
if policy.Scope == "" {
policy.Scope = models.PolicyScopeOrg
}
if err := cfg.DB().Create(policy).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
return policy, nil
}

@ -1,35 +0,0 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-03-04 16:08:06
// Distributed under terms of the MIT license.
//
package policy
import (
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type DeleteRequest struct {
PolicyID string `src:"path@policy_id" desc:"策略ID"`
}
func del(x *vigo.X, req *DeleteRequest) error {
var policy models.Policy
if err := cfg.DB().First(&policy, "id = ?", req.PolicyID).Error; err != nil {
return vigo.ErrNotFound
}
// 系统策略不允许删除
if policy.Scope == models.PolicyScopePlatform {
return vigo.ErrForbidden.WithString("system policies cannot be deleted")
}
if err := cfg.DB().Delete(&policy).Error; err != nil {
return vigo.ErrInternalServer.WithError(err)
}
return nil
}

@ -1,25 +0,0 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-03-04 16:08:06
// Distributed under terms of the MIT license.
//
package policy
import (
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type GetRequest struct {
PolicyID string `src:"path@policy_id" desc:"策略ID"`
}
func get(x *vigo.X, req *GetRequest) (*models.Policy, error) {
var policy models.Policy
if err := cfg.DB().First(&policy, "id = ?", req.PolicyID).Error; err != nil {
return nil, vigo.ErrNotFound
}
return &policy, nil
}

@ -1,25 +0,0 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-03-04 16:08:06
// Distributed under terms of the MIT license.
//
package policy
import (
"github.com/veypi/vbase/api/middleware"
"github.com/veypi/vigo"
)
var Router = vigo.NewRouter()
func init() {
// 列表和详情 - 需要读取权限
Router.Get("/", middleware.Permission("policy", "list"), "策略列表", list)
Router.Get("/{policy_id}", middleware.Permission("policy", "read"), "获取策略详情", get)
// 创建、更新、删除 - 需要管理权限
Router.Post("/", middleware.Permission("policy", "create"), "创建策略", create)
Router.Patch("/{policy_id}", middleware.Permission("policy", "update"), "更新策略", patch)
Router.Delete("/{policy_id}", middleware.Permission("policy", "delete"), "删除策略", del)
}

@ -1,63 +0,0 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-03-04 16:08:06
// Distributed under terms of the MIT license.
//
package policy
import (
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type ListRequest struct {
Page int `json:"page" src:"query" default:"1"`
PageSize int `json:"page_size" src:"query" default:"20"`
Resource string `json:"resource" src:"query" desc:"资源类型筛选"`
Scope string `json:"scope" src:"query" desc:"作用域筛选"`
}
type ListResponse struct {
Items []models.Policy `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
func list(x *vigo.X, req *ListRequest) (*ListResponse, error) {
db := cfg.DB().Model(&models.Policy{})
if req.Resource != "" {
db = db.Where("resource = ?", req.Resource)
}
if req.Scope != "" {
db = db.Where("scope = ?", req.Scope)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
var policies []models.Policy
offset := (req.Page - 1) * req.PageSize
if err := db.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&policies).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
totalPages := int(total) / req.PageSize
if int(total)%req.PageSize > 0 {
totalPages++
}
return &ListResponse{
Items: policies,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
TotalPages: totalPages,
}, nil
}

@ -1,53 +0,0 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-03-04 16:08:06
// Distributed under terms of the MIT license.
//
package policy
import (
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type PatchRequest struct {
PolicyID string `src:"path@policy_id" desc:"策略ID"`
Name *string `json:"name,omitempty" src:"json" desc:"策略名称"`
Description *string `json:"description,omitempty" src:"json" desc:"描述"`
Effect *string `json:"effect,omitempty" src:"json" desc:"效果: allow/deny"`
Condition *string `json:"condition,omitempty" src:"json" desc:"条件"`
}
func patch(x *vigo.X, req *PatchRequest) (*models.Policy, error) {
var policy models.Policy
if err := cfg.DB().First(&policy, "id = ?", req.PolicyID).Error; err != nil {
return nil, vigo.ErrNotFound
}
// 系统策略不允许修改
if policy.Scope == models.PolicyScopePlatform {
return nil, vigo.ErrForbidden.WithString("system policies cannot be modified")
}
updates := make(map[string]any)
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.Effect != nil {
updates["effect"] = *req.Effect
}
if req.Condition != nil {
updates["condition"] = *req.Condition
}
if err := cfg.DB().Model(&policy).Updates(updates).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
return &policy, nil
}

@ -1,52 +0,0 @@
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-03-04 16:08:06
// Distributed under terms of the MIT license.
package role
import (
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type CreateRequest struct {
OrgID string `json:"org_id" src:"json" desc:"组织ID"`
Name string `json:"name" src:"json" desc:"角色名称"`
Code string `json:"code" src:"json" desc:"角色代码"`
Description string `json:"description,omitempty" src:"json" desc:"描述"`
PolicyIDs []string `json:"policy_ids,omitempty" src:"json" desc:"策略ID列表"`
}
func create(x *vigo.X, req *CreateRequest) (*models.Role, error) {
// 检查同一组织内代码是否已存在
var count int64
cfg.DB().Model(&models.Role{}).Where("org_id = ? AND code = ?", req.OrgID, req.Code).Count(&count)
if count > 0 {
return nil, vigo.ErrArgInvalid.WithString("role code already exists in this organization")
}
// 转换策略ID列表为字符串
policyIDsStr := ""
for i, id := range req.PolicyIDs {
if i > 0 {
policyIDsStr += ","
}
policyIDsStr += id
}
role := &models.Role{
OrgID: req.OrgID,
Name: req.Name,
Code: req.Code,
Description: req.Description,
PolicyIDs: policyIDsStr,
Scope: models.PolicyScopeOrg,
}
if err := cfg.DB().Create(role).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
return role, nil
}

@ -1,33 +0,0 @@
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-03-04 16:08:06
// Distributed under terms of the MIT license.
package role
import (
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type DeleteRequest struct {
RoleID string `src:"path@role_id" desc:"角色ID"`
}
func del(x *vigo.X, req *DeleteRequest) error {
var role models.Role
if err := cfg.DB().First(&role, "id = ?", req.RoleID).Error; err != nil {
return vigo.ErrNotFound
}
// 系统角色不允许删除
if role.IsSystem {
return vigo.ErrForbidden.WithString("system roles cannot be deleted")
}
if err := cfg.DB().Delete(&role).Error; err != nil {
return vigo.ErrInternalServer.WithError(err)
}
return nil
}

@ -1,36 +0,0 @@
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-03-04 16:08:06
// Distributed under terms of the MIT license.
package role
import (
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type GetRequest struct {
RoleID string `src:"path@role_id" desc:"角色ID"`
}
type RoleDetail struct {
models.Role
Policies []models.Policy `json:"policies"`
}
func get(x *vigo.X, req *GetRequest) (*RoleDetail, error) {
var role models.Role
if err := cfg.DB().First(&role, "id = ?", req.RoleID).Error; err != nil {
return nil, vigo.ErrNotFound
}
// 解析策略ID列表
policies := []models.Policy{}
// TODO: 根据 role.PolicyIDs 查询策略列表
return &RoleDetail{
Role: role,
Policies: policies,
}, nil
}

@ -1,28 +0,0 @@
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-03-04 16:08:06
// Distributed under terms of the MIT license.
package role
import (
"github.com/veypi/vbase/api/middleware"
"github.com/veypi/vigo"
)
var Router = vigo.NewRouter()
func init() {
// 列表和详情 - 需要读取权限
Router.Get("/", middleware.Permission("role", "list"), "角色列表", list)
Router.Get("/{role_id}", middleware.Permission("role", "read"), "获取角色详情", get)
// 创建、更新、删除 - 需要管理权限
Router.Post("/", middleware.Permission("role", "create"), "创建角色", create)
Router.Patch("/{role_id}", middleware.Permission("role", "update"), "更新角色", patch)
Router.Delete("/{role_id}", middleware.Permission("role", "delete"), "删除角色", del)
// 角色策略管理 - 需要角色管理权限
Router.Get("/{role_id}/policies", middleware.Permission("role", "read"), "获取角色策略", listPolicies)
Router.Post("/{role_id}/policies", middleware.Permission("role", "update"), "为角色添加策略", addPolicy)
Router.Delete("/{role_id}/policies/{policy_id}", middleware.Permission("role", "update"), "从角色移除策略", removePolicy)
}

@ -1,57 +0,0 @@
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-03-04 16:08:06
// Distributed under terms of the MIT license.
package role
import (
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type ListRequest struct {
Page int `json:"page" src:"query" default:"1"`
PageSize int `json:"page_size" src:"query" default:"20"`
OrgID string `json:"org_id" src:"query" desc:"组织ID筛选"`
}
type ListResponse struct {
Items []models.Role `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
func list(x *vigo.X, req *ListRequest) (*ListResponse, error) {
db := cfg.DB().Model(&models.Role{})
if req.OrgID != "" {
db = db.Where("org_id = ?", req.OrgID)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
var roles []models.Role
offset := (req.Page - 1) * req.PageSize
if err := db.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&roles).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
totalPages := int(total) / req.PageSize
if int(total)%req.PageSize > 0 {
totalPages++
}
return &ListResponse{
Items: roles,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
TotalPages: totalPages,
}, nil
}

@ -1,54 +0,0 @@
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-03-04 16:08:06
// Distributed under terms of the MIT license.
package role
import (
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type PatchRequest struct {
RoleID string `src:"path@role_id" desc:"角色ID"`
Name *string `json:"name,omitempty" src:"json" desc:"角色名称"`
Description *string `json:"description,omitempty" src:"json" desc:"描述"`
PolicyIDs []string `json:"policy_ids,omitempty" src:"json" desc:"策略ID列表"`
}
func patch(x *vigo.X, req *PatchRequest) (*models.Role, error) {
var role models.Role
if err := cfg.DB().First(&role, "id = ?", req.RoleID).Error; err != nil {
return nil, vigo.ErrNotFound
}
// 系统角色不允许修改
if role.IsSystem {
return nil, vigo.ErrForbidden.WithString("system roles cannot be modified")
}
updates := make(map[string]any)
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.PolicyIDs != nil {
policyIDsStr := ""
for i, id := range req.PolicyIDs {
if i > 0 {
policyIDsStr += ","
}
policyIDsStr += id
}
updates["policy_ids"] = policyIDsStr
}
if err := cfg.DB().Model(&role).Updates(updates).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
return &role, nil
}

@ -1,129 +0,0 @@
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-03-04 16:08:06
// Distributed under terms of the MIT license.
package role
import (
"strings"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
// ListPoliciesRequest 获取角色策略请求
type ListPoliciesRequest struct {
RoleID string `src:"path@role_id" desc:"角色ID"`
}
// listPolicies 获取角色关联的策略列表
func listPolicies(x *vigo.X, req *ListPoliciesRequest) ([]models.Policy, error) {
var role models.Role
if err := cfg.DB().First(&role, "id = ?", req.RoleID).Error; err != nil {
return nil, vigo.ErrNotFound
}
if role.PolicyIDs == "" {
return []models.Policy{}, nil
}
// 解析策略ID列表
policyIDs := strings.Split(role.PolicyIDs, ",")
var policies []models.Policy
if err := cfg.DB().Where("id IN ?", policyIDs).Find(&policies).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
return policies, nil
}
// AddPolicyRequest 添加策略请求
type AddPolicyRequest struct {
RoleID string `src:"path@role_id" desc:"角色ID"`
PolicyID string `json:"policy_id" src:"json" desc:"策略ID"`
}
// addPolicy 为角色添加策略
func addPolicy(x *vigo.X, req *AddPolicyRequest) (*models.Role, error) {
var role models.Role
if err := cfg.DB().First(&role, "id = ?", req.RoleID).Error; err != nil {
return nil, vigo.ErrNotFound
}
// 验证策略是否存在
var policy models.Policy
if err := cfg.DB().First(&policy, "id = ?", req.PolicyID).Error; err != nil {
return nil, vigo.ErrArgInvalid.WithString("policy not found")
}
// 解析现有策略ID
existingIDs := map[string]bool{}
if role.PolicyIDs != "" {
for _, id := range strings.Split(role.PolicyIDs, ",") {
existingIDs[id] = true
}
}
// 检查是否已存在
if existingIDs[req.PolicyID] {
return nil, vigo.ErrArgInvalid.WithString("policy already added to this role")
}
// 添加新策略ID
if role.PolicyIDs != "" {
role.PolicyIDs += ","
}
role.PolicyIDs += req.PolicyID
if err := cfg.DB().Model(&role).Update("policy_ids", role.PolicyIDs).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
return &role, nil
}
// RemovePolicyRequest 移除策略请求
type RemovePolicyRequest struct {
RoleID string `src:"path@role_id" desc:"角色ID"`
PolicyID string `src:"path@policy_id" desc:"策略ID"`
}
// removePolicy 从角色移除策略
func removePolicy(x *vigo.X, req *RemovePolicyRequest) (*models.Role, error) {
var role models.Role
if err := cfg.DB().First(&role, "id = ?", req.RoleID).Error; err != nil {
return nil, vigo.ErrNotFound
}
// 解析现有策略ID
if role.PolicyIDs == "" {
return nil, vigo.ErrArgInvalid.WithString("role has no policies")
}
existingIDs := strings.Split(role.PolicyIDs, ",")
newIDs := []string{}
found := false
for _, id := range existingIDs {
if id == req.PolicyID {
found = true
continue
}
newIDs = append(newIDs, id)
}
if !found {
return nil, vigo.ErrArgInvalid.WithString("policy not found in this role")
}
// 更新策略ID列表
role.PolicyIDs = strings.Join(newIDs, ",")
if err := cfg.DB().Model(&role).Update("policy_ids", role.PolicyIDs).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
return &role, nil
}

@ -6,13 +6,16 @@
package user
import "github.com/veypi/vigo"
import (
"github.com/veypi/vbase/auth"
"github.com/veypi/vigo"
)
var Router = vigo.NewRouter()
func init() {
Router.Get("/", "用户列表", list)
Router.Post("/", "创建用户", create)
Router.Get("/", "用户列表", auth.VBaseAuth.Perm("user:read"), list)
Router.Post("/", "创建用户", auth.VBaseAuth.Perm("user:admin"), create)
Router.Get("/{user_id}", "获取用户详情", get)
Router.Patch("/{user_id}", "更新用户", patch)
Router.Delete("/{user_id}", "删除用户", del)

@ -0,0 +1,610 @@
//
// 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
// 满足任一权限
PermAny(permissionIDs []string) func(*vigo.X) error
// 满足所有权限
PermAll(permissionIDs []string) func(*vigo.X) error
// ========== 权限管理 ==========
// 授予角色
GrantRole(ctx context.Context, req models.GrantRoleRequest) error
// 撤销角色
RevokeRole(ctx context.Context, userID, orgID, roleCode string) error
// 授予特定资源权限
GrantResourcePerm(ctx context.Context, req models.GrantResourcePermRequest) error
// 撤销特定资源权限
RevokeResourcePerm(ctx context.Context, userID, orgID, permissionID, resourceID string) error
// 撤销用户所有权限
RevokeAll(ctx context.Context, userID, orgID string) error
// ========== 权限查询 ==========
// 检查权限
CheckPermission(ctx context.Context, req models.CheckPermRequest) (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"}},
},
})
)
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(), models.CheckPermRequest{
UserID: userID,
OrgID: orgID,
PermissionID: 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(), models.CheckPermRequest{
UserID: userID,
OrgID: orgID,
PermissionID: permissionID,
})
if err != nil {
return err
}
if !ok {
return vigo.ErrForbidden
}
// 检查是否是所有者或管理员
ownerID, _ := x.Get(ownerKey).(string)
if ownerID == userID {
return nil
}
// 检查是否是管理员
isAdmin, _ := a.isAdmin(x.Context(), userID, orgID)
if isAdmin {
return nil
}
return vigo.ErrForbidden
}
}
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(), models.CheckPermRequest{
UserID: userID,
OrgID: orgID,
PermissionID: 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(), models.CheckPermRequest{
UserID: userID,
OrgID: orgID,
PermissionID: permID,
})
if err != nil {
return err
}
if !ok {
return vigo.ErrForbidden
}
}
return nil
}
}
// ========== 权限管理实现 ==========
func (a *appAuth) GrantRole(ctx context.Context, req models.GrantRoleRequest) error {
// 查找角色
var role models.Role
query := cfg.DB().Where("code = ?", req.RoleCode)
if req.OrgID != "" {
query = query.Where("org_id = ?", req.OrgID)
} else {
query = query.Where("org_id = ''")
}
if err := query.First(&role).Error; err != nil {
return fmt.Errorf("role not found: %s", req.RoleCode)
}
// 检查是否已存在
var count int64
cfg.DB().Model(&models.UserRole{}).
Where("user_id = ? AND org_id = ? AND role_id = ?", req.UserID, req.OrgID, role.ID).
Count(&count)
if count > 0 {
return nil // 已存在
}
userRole := models.UserRole{
UserID: req.UserID,
OrgID: req.OrgID,
RoleID: role.ID,
ExpireAt: req.ExpireAt,
}
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 {
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, req models.GrantResourcePermRequest) error {
// 检查权限是否存在
var perm models.Permission
if err := cfg.DB().Where("id = ?", req.PermissionID).First(&perm).Error; err != nil {
return fmt.Errorf("permission not found: %s", req.PermissionID)
}
// 检查是否已存在
var existing models.UserPermission
err := cfg.DB().Where("user_id = ? AND org_id = ? AND permission_id = ? AND resource_id = ?",
req.UserID, req.OrgID, req.PermissionID, req.ResourceID).
First(&existing).Error
if err == nil {
// 更新过期时间
existing.ExpireAt = req.ExpireAt
return cfg.DB().Save(&existing).Error
}
userPerm := models.UserPermission{
UserID: req.UserID,
OrgID: req.OrgID,
PermissionID: req.PermissionID,
ResourceID: req.ResourceID,
ExpireAt: req.ExpireAt,
GrantedBy: req.GrantedBy,
}
return cfg.DB().Create(&userPerm).Error
}
func (a *appAuth) RevokeResourcePerm(ctx context.Context, userID, orgID, permissionID, resourceID string) error {
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, req models.CheckPermRequest) (bool, error) {
// 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 > ?)",
req.UserID, req.OrgID, time.Now()).
Pluck("role_id", &roleIDs).Error; err != nil {
return false, err
}
if len(roleIDs) > 0 {
// 检查这些角色是否有所需权限
var count int64
if err := cfg.DB().Model(&models.RolePermission{}).
Where("role_id IN ? AND permission_id = ?", roleIDs, req.PermissionID).
Count(&count).Error; err != nil {
return false, err
}
if count > 0 {
return true, nil
}
}
// 2. 检查用户是否有特定的资源权限
var userPermCount int64
query := cfg.DB().Model(&models.UserPermission{}).
Where("user_id = ? AND org_id = ? AND permission_id = ? AND (expire_at IS NULL OR expire_at > ?)",
req.UserID, req.OrgID, req.PermissionID, time.Now())
if req.ResourceID != "" {
query = query.Where("resource_id = ? OR resource_id = '*'", req.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 ""
}

@ -47,6 +47,11 @@ func main() {
}
func runWeb() error {
// 初始化权限系统
if err := vbase.Auth.Init(); err != nil {
return err
}
server, err := vigo.New(vigo.WithHost(cliOpts.Host), vigo.WithPort(cliOpts.Port))
if err != nil {
return err

@ -0,0 +1,565 @@
# VBase 开发者使用手册
## 架构概述
```
┌─────────────────────────────────────────────────────────────┐
│ vbase/Auth 包 │
│ ├─ New(appKey, config) - 创建权限管理实例 │
│ ├─ Init() - 启动时检查冲突并同步数据库 │
│ └─ Auth 接口 - 权限检查和管理方法 │
└─────────────────────────────────────────────────────────────┘
├─────────────────────┼─────────────────────┐
▼ ▼ ▼
注册权限配置 接口权限检查 编程式授权
(代码中声明) (中间件使用) (运行时调用)
```
## 快速开始
```go
package main
import (
"github.com/veypi/vbase"
"github.com/veypi/vbase/api"
"github.com/veypi/vigo"
)
// 全局权限管理实例
crmAuth := vbase.Auth.New("crm", vbase.AppConfig{
Name: "客户关系管理",
Description: "CRM系统",
})
func main() {
r := vigo.NewRouter()
// 1. 挂载 VBase 基础设施
r.Extend("/api/vb", api.Router)
// 2. 程序启动时初始化所有权限配置
if err := vbase.Auth.Init(); err != nil {
log.Fatal("权限初始化失败:", err)
}
// 3. 创建业务路由
crm := r.SubRouter("/api/crm")
crm.Use(middleware.AuthRequired())
crm.Use(middleware.OrgContext())
// 4. 使用 crmAuth 配置接口权限
crm.Get("/customers", crmAuth.Perm("customer", "list"), "客户列表", listCustomers)
crm.Post("/customers", crmAuth.Perm("customer", "create"), "创建客户", createCustomer)
crm.Get("/customers/{id}", crmAuth.Perm("customer", "read"), "客户详情", getCustomer)
crm.Patch("/customers/{id}", crmAuth.Perm("customer", "update"), "更新客户", updateCustomer)
crm.Delete("/customers/{id}", crmAuth.Perm("customer", "delete"), "删除客户", deleteCustomer)
// 5. 资源所有者权限
crm.Patch("/customers/{id}/private",
crmAuth.PermWithOwner("customer", "update", "owner_id"),
"仅限所有者",
privateUpdate)
vigo.Run(r)
}
```
---
## 1. 权限管理包 vbase/Auth
### 1.1 包结构
```go
package auth
// Auth 权限管理接口
type Auth interface {
// ========== 中间件生成 ==========
// 基础权限检查
Perm(resource, action string) func(*vigo.X) error
// 资源所有者权限
PermWithOwner(resource, action, ownerKey string) func(*vigo.X) error
// 满足任一权限
PermAny(permissions [][2]string) func(*vigo.X) error
// 满足所有权限
PermAll(permissions [][2]string) func(*vigo.X) error
// ========== 权限管理 ==========
// 授予角色
GrantRole(ctx context.Context, req GrantRoleRequest) error
// 撤销角色
RevokeRole(ctx context.Context, userID, orgID, roleCode string) error
// 授予特定资源权限
GrantResourcePerm(ctx context.Context, req GrantResourcePermRequest) error
// 撤销特定资源权限
RevokeResourcePerm(ctx context.Context, userID, orgID, resource, resourceID string) error
// 撤销用户所有权限
RevokeAll(ctx context.Context, userID, orgID string) error
// ========== 权限查询 ==========
// 检查权限
CheckPermission(ctx context.Context, req CheckPermRequest) (bool, error)
// 列出用户权限
ListUserPermissions(ctx context.Context, userID, orgID string) ([]UserPermission, error)
// 列出资源授权用户
ListResourceUsers(ctx context.Context, orgID, resource, resourceID string) ([]ResourceUser, error)
}
// 全局 Auth 工厂
var Auth = &authFactory{}
type authFactory struct {
apps map[string]*appAuth // appKey -> auth实例
}
// New 创建权限管理实例(注册应用)
func (f *authFactory) New(appKey string, config AppConfig) Auth
// Init 初始化所有注册的权限配置
// - 检查不同 app 之间是否有冲突
// - 同步 Policy 到数据库
// - 建立路由匹配缓存
func (f *authFactory) Init() error
```
### 1.2 使用流程
```go
// 阶段1程序初始化全局变量/ init 函数)
package main
// CRM 模块权限
crmAuth := vbase.Auth.New("crm", vbase.AppConfig{
Name: "客户关系管理",
DefaultRoles: []RoleDefinition{
{Code: "admin", Name: "管理员", Policies: []string{"*:*"}},
{Code: "sales", Name: "销售", Policies: []string{"customer:*", "order:read"}},
},
})
// ERP 模块权限
erpAuth := vbase.Auth.New("erp", vbase.AppConfig{
Name: "企业资源计划",
DefaultRoles: []RoleDefinition{
{Code: "admin", Name: "管理员", Policies: []string{"*:*"}},
{Code: "buyer", Name: "采购", Policies: []string{"supplier:read", "purchase:*"}},
},
})
func main() {
// 阶段2程序启动时初始化
if err := vbase.Auth.Init(); err != nil {
log.Fatal(err)
}
// 阶段3运行时使用
// crmAuth.Perm(...)
// erpAuth.GrantRole(...)
}
```
---
## 2. AppConfig 配置
```go
type AppConfig struct {
Name string // 应用名称
Description string // 应用描述
DefaultRoles []RoleDefinition // 预设角色
}
type RoleDefinition struct {
Code string // 角色代码: admin/manager/sales/viewer
Name string // 角色名称
Policies []string // 权限列表: ["customer:read", "customer:create", "*:*"]
}
```
### 多模块隔离示例
```go
// 模块1CRM
crmAuth := vbase.Auth.New("crm", vbase.AppConfig{
Name: "客户关系管理",
DefaultRoles: []vbase.RoleDefinition{
{Code: "admin", Name: "管理员", Policies: []string{"customer:*", "contract:*"}},
{Code: "sales", Name: "销售", Policies: []string{"customer:read", "customer:create", "customer:update"}},
},
})
// 模块2ERP资源名可以重复通过 appKey 隔离)
erpAuth := vbase.Auth.New("erp", vbase.AppConfig{
Name: "企业资源计划",
DefaultRoles: []vbase.RoleDefinition{
{Code: "admin", Name: "管理员", Policies: []string{"customer:*", "supplier:*"}},
// erp:customer:read 和 crm:customer:read 是两个不同权限
},
})
// 启动时统一检查
crmAuth, erpAuth 会被 Init() 同时处理
```
---
## 3. 接口权限配置
### 3.1 基础权限
```go
func main() {
// ... vbase.Auth.Init() ...
api := r.SubRouter("/api/crm")
api.Use(middleware.AuthRequired())
api.Use(middleware.OrgContext())
// 标准 CRUD
api.Get("/customers", crmAuth.Perm("customer", "list"), "客户列表", listHandler)
api.Post("/customers", crmAuth.Perm("customer", "create"), "创建客户", createHandler)
api.Get("/customers/{id}", crmAuth.Perm("customer", "read"), "客户详情", getHandler)
api.Patch("/customers/{id}", crmAuth.Perm("customer", "update"), "更新客户", updateHandler)
api.Delete("/customers/{id}", crmAuth.Perm("customer", "delete"), "删除客户", deleteHandler)
// 自定义操作
api.Post("/customers/{id}/export", crmAuth.Perm("customer", "export"), "导出客户", exportHandler)
api.Post("/customers/{id}/transfer", crmAuth.Perm("customer", "transfer"), "转交客户", transferHandler)
// 通配权限(拥有 customer 任意操作即可)
api.Get("/customers/stats", crmAuth.Perm("customer", "*"), "客户统计", statsHandler)
}
```
### 3.2 资源所有者权限
```go
func getCustomer(x *vigo.X, req *GetReq) (*Customer, error) {
customer := query(req.ID)
x.Set("owner_id", customer.OwnerID) // 必须设置 owner_id
return customer, nil
}
// 只有所有者或管理员能更新
api.Patch("/customers/{id}",
crmAuth.PermWithOwner("customer", "update", "owner_id"),
"更新客户",
updateHandler,
)
// 只有所有者能删除(管理员也不行)
api.Delete("/customers/{id}/danger",
crmAuth.PermWithOwner("customer", "delete", "owner_id"),
"危险删除",
dangerDeleteHandler,
)
```
### 3.3 组合权限
```go
// 满足任一权限即可
api.Patch("/customers/{id}",
crmAuth.PermAny([][2]string{
{"customer", "update"},
{"customer", "admin"},
}),
"更新客户",
updateHandler,
)
// 必须同时满足所有权限
api.Post("/customers/batch-import",
crmAuth.PermAll([][2]string{
{"customer", "create"},
{"customer", "batch"},
}),
"批量导入",
batchImportHandler,
)
```
---
## 4. 编程式权限管理
开发者在自己的业务逻辑中调用这些方法管理权限。
### 4.1 授予角色
```go
func inviteMember(ctx context.Context, req InviteReq) error {
// 检查当前用户是否有邀请权限
ok, _ := crmAuth.CheckPermission(ctx, CheckPermRequest{
UserID: req.CurrentUserID,
OrgID: req.OrgID,
Resource: "org_member",
Action: "invite",
})
if !ok {
return fmt.Errorf("无权邀请")
}
// 创建用户
user, _ := vbase.User.FindOrCreateByEmail(req.Email)
// 加入组织并授予角色
if err := crmAuth.GrantRole(ctx, GrantRoleRequest{
UserID: user.ID,
OrgID: req.OrgID,
Role: req.Role, // "admin" / "sales" / "viewer"
}); err != nil {
return err
}
return nil
}
```
### 4.2 授予特定资源权限
```go
// 场景:将客户转交给销售 B 跟进
func transferCustomer(ctx context.Context, customerID, fromUserID, toUserID, orgID string) error {
// 1. 撤销 A 的权限
if err := crmAuth.RevokeResourcePerm(ctx, fromUserID, orgID, "customer", customerID); err != nil {
return err
}
// 2. 授予 B 权限
if err := crmAuth.GrantResourcePerm(ctx, GrantResourcePermRequest{
UserID: toUserID,
OrgID: orgID,
Resource: "customer",
ResourceID: customerID,
Action: "*", // 所有操作
}); err != nil {
return err
}
return nil
}
// 场景临时授权3天后过期
func tempGrant(ctx context.Context, userID, orgID string) error {
return crmAuth.GrantResourcePerm(ctx, GrantResourcePermRequest{
UserID: userID,
OrgID: orgID,
Resource: "report",
ResourceID: "*",
Action: "export",
ExpireAt: time.Now().Add(3 * 24 * time.Hour),
})
}
```
### 4.3 查询权限
```go
// 检查用户是否能操作某客户
func canEditCustomer(ctx context.Context, userID, orgID, customerID string) bool {
ok, _ := crmAuth.CheckPermission(ctx, CheckPermRequest{
UserID: userID,
OrgID: orgID,
Resource: "customer",
ResourceID: customerID,
Action: "update",
})
return ok
}
// 获取用户在组织的所有权限
perms, _ := crmAuth.ListUserPermissions(ctx, "user-123", "org-456")
// 返回:
// [
// {Resource: "customer", ResourceID: "*", Actions: ["read","list"]},
// {Resource: "customer", ResourceID: "c-789", Actions: ["*"]},
// {Resource: "order", ResourceID: "*", Actions: ["read"]}
// ]
// 获取某客户的所有授权用户
users, _ := crmAuth.ListResourceUsers(ctx, "org-456", "customer", "c-789")
// 返回:
// [
// {UserID: "u1", Actions: ["admin"]},
// {UserID: "u2", Actions: ["read","update"]}
// ]
```
### 4.4 撤销权限
```go
// 撤销角色
crmAuth.RevokeRole(ctx, "user-123", "org-456", "sales")
// 撤销特定资源权限
crmAuth.RevokeResourcePerm(ctx, "user-123", "org-456", "customer", "c-789")
// 撤销用户在组织的所有权限(离职)
crmAuth.RevokeAll(ctx, "user-123", "org-456")
```
---
## 5. 完整示例:多模块应用
```go
package main
import (
"github.com/veypi/vbase"
"github.com/veypi/vbase/api"
"github.com/veypi/vbase/api/middleware"
"github.com/veypi/vigo"
)
// ========== 阶段1定义权限全局==========
// CRM 模块
crmAuth := vbase.Auth.New("crm", vbase.AppConfig{
Name: "客户关系管理",
DefaultRoles: []vbase.RoleDefinition{
{Code: "admin", Name: "管理员", Policies: []string{"*:*"}},
{Code: "manager", Name: "主管", Policies: []string{"customer:*", "contract:*", "report:*"}},
{Code: "sales", Name: "销售", Policies: []string{"customer:read", "customer:create", "customer:update"}},
{Code: "viewer", Name: "查看者", Policies: []string{"customer:read"}},
},
})
// 合同模块(独立,但共用 crm 用户体系)
contractAuth := vbase.Auth.New("contract", vbase.AppConfig{
Name: "合同管理",
DefaultRoles: []vbase.RoleDefinition{
{Code: "admin", Name: "管理员", Policies: []string{"*:*"}},
{Code: "legal", Name: "法务", Policies: []string{"contract:read", "contract:audit", "contract:approve"}},
{Code: "manager", Name: "业务经理", Policies: []string{"contract:read", "contract:create", "contract:sign"}},
},
})
func main() {
// ========== 阶段2初始化 ==========
if err := vbase.Auth.Init(); err != nil {
log.Fatal("权限初始化失败:", err)
}
r := vigo.NewRouter()
// 挂载 VBase
r.Extend("/api/vb", api.Router)
// ========== 阶段3配置路由 ==========
// CRM 路由
crm := r.SubRouter("/api/crm")
crm.Use(middleware.AuthRequired())
crm.Use(middleware.OrgContext())
crm.Get("/customers", crmAuth.Perm("customer", "list"), "客户列表", listCustomers)
crm.Post("/customers", crmAuth.Perm("customer", "create"), "创建客户", createCustomer)
crm.Get("/customers/{id}", crmAuth.Perm("customer", "read"), "客户详情", getCustomer)
crm.Patch("/customers/{id}", crmAuth.PermWithOwner("customer", "update", "owner_id"), "更新客户", updateCustomer)
crm.Delete("/customers/{id}", crmAuth.Perm("customer", "delete"), "删除客户", deleteCustomer)
// 合同路由(独立模块)
contract := r.SubRouter("/api/contract")
contract.Use(middleware.AuthRequired())
contract.Use(middleware.OrgContext())
contract.Get("/contracts", contractAuth.Perm("contract", "list"), "合同列表", listContracts)
contract.Post("/contracts", contractAuth.Perm("contract", "create"), "创建合同", createContract)
contract.Post("/contracts/{id}/audit", contractAuth.Perm("contract", "audit"), "审核合同", auditContract)
contract.Post("/contracts/{id}/sign", contractAuth.Perm("contract", "sign"), "签署合同", signContract)
vigo.Run(r)
}
```
---
## 6. API 参考
### 6.1 全局函数
```go
// 创建权限管理实例
func (f *authFactory) New(appKey string, config AppConfig) Auth
// 初始化所有权限配置
func (f *authFactory) Init() error
```
### 6.2 Auth 接口
```go
// ========== 中间件 ==========
Perm(resource, action string) func(*vigo.X) error
PermWithOwner(resource, action, ownerKey string) func(*vigo.X) error
PermAny(permissions [][2]string) func(*vigo.X) error
PermAll(permissions [][2]string) func(*vigo.X) error
// ========== 授权管理 ==========
GrantRole(ctx context.Context, req GrantRoleRequest) error
RevokeRole(ctx context.Context, userID, orgID, roleCode string) error
GrantResourcePerm(ctx context.Context, req GrantResourcePermRequest) error
RevokeResourcePerm(ctx context.Context, userID, orgID, resource, resourceID string) error
RevokeAll(ctx context.Context, userID, orgID string) error
// ========== 权限查询 ==========
CheckPermission(ctx context.Context, req CheckPermRequest) (bool, error)
ListUserPermissions(ctx context.Context, userID, orgID string) ([]UserPermission, error)
ListResourceUsers(ctx context.Context, orgID, resource, resourceID string) ([]ResourceUser, error)
```
### 6.3 请求/响应结构
```go
type GrantRoleRequest struct {
UserID string // 用户ID
OrgID string // 组织ID
Role string // 角色代码
}
type GrantResourcePermRequest struct {
UserID string // 用户ID
OrgID string // 组织ID
Resource string // 资源类型
ResourceID string // 资源实例ID"*" 表示所有
Action string // 操作类型,"*" 表示所有
ExpireAt time.Time // 过期时间(可选)
}
type CheckPermRequest struct {
UserID string // 用户ID
OrgID string // 组织ID
Resource string // 资源类型
ResourceID string // 资源实例ID可选
Action string // 操作类型
}
type UserPermission struct {
Resource string // 资源类型
ResourceID string // 资源ID
Actions []string // 允许的操作
}
type ResourceUser struct {
UserID string // 用户ID
Actions []string // 允许的操作
}
```
---
**确认此设计后,我将实现底层代码。**

@ -6,127 +6,91 @@
import "github.com/veypi/vbase/api"
func main() {
// 挂载 vbase 路由到 /api/v1/vb
rootRouter.Extend("/api/v1/vb", api.Router)
// 挂载 vbase 路由到 /api/vb
rootRouter.Extend("/api/vb", api.Router)
}
```
## 2. 集成配置
配置自动从 vigo 的 config.toml 读取:
```toml
[vbase]
jwt_secret = "your-secret-key"
jwt_expire = 7200 # token 过期时间(秒)
refresh_expire = 604800 # refresh token 过期时间(秒)
bcrypt_cost = 10 # 密码加密强度
[vbase.redis]
addr = "localhost:6379" # 留空或填 memory 使用内存缓存
password = ""
db = 0
```
或在代码中自定义:
```go
import "github.com/veypi/vbase/cfg"
cfg.Config.JWTSecret = "your-secret"
cfg.Config.JWTExpire = 7200
```
## 3. 配置策略
创建组织时自动初始化默认策略:
## 2. 配置权限中间件
```go
import "github.com/veypi/vbase/api/middleware"
// 创建组织后调用
middleware.InitOrgPolicies(orgID)
```
默认创建的策略:
| 策略 | 资源 | 操作 | 条件 | 说明 |
|------|------|------|------|------|
| policy:manage | policy | * | admin | 管理策略 |
| role:manage | role | * | admin | 管理角色 |
| user:update | user | update | owner | 只能改自己 |
// 全局启用路由权限检查(推荐)
router.Use(middleware.Perm("your-domain"))
自定义策略:
```go
import "github.com/veypi/vbase/models"
policy := &models.Policy{
Code: "project:delete",
Name: "删除项目",
Resource: "project",
Action: "delete",
Effect: models.PolicyEffectAllow,
Condition: "owner", // 只有所有者能删
Scope: models.PolicyScopeOrg,
}
cfg.DB().Create(policy)
// 未配置的接口默认拒绝
// 无权限时返回 X-Required-URL 响应头
```
## 4. 使用鉴权
## 3. 注册路由权限
### 4.1 全局中间件(已内置
### 3.1 自动注册(推荐)
```go
// api/init.go 已自动配置:
Router.Use(middleware.AuthRequired()) // JWT 认证
Router.Use(middleware.OrgContext()) // 组织上下文
```
// 启动时自动扫描并注册所有路由
middleware.InitVBaseRoutePolicies()
### 4.2 公开接口(跳过认证)
```go
Router.Get("/public", vigo.SkipBefore, "公开接口", handler)
// 首次启动后,在数据库中修改权限配置即可
```
### 4.3 接口级权限控制
### 3.2 手动注册
```go
import "github.com/veypi/vbase/api/middleware"
// 公开接口
middleware.RegisterRoutePolicy("myapp", "/api", "/public", "GET", "public", "read", true)
// 需要管理员权限
Router.Post("/users", middleware.RequireAdmin(), "创建用户", createUser)
// 需要权限的接口
middleware.RegisterRoutePolicy("myapp", "/api", "/users", "GET", "user", "list", false)
middleware.RegisterRoutePolicy("myapp", "/api", "/users/{id}", "PATCH", "user", "update", false)
// 基于 Policy 的细粒度控制
Router.Post("/projects", middleware.Permission("project", "create"), "创建项目", createProject)
// 带所有者检查(用户只能改自己的数据)
Router.Patch("/users/{id}", middleware.PermissionWithOwner("user", "update", "owner_id"), "更新用户", updateUser)
// 重新加载缓存
middleware.ReloadRoutePolicies()
```
// 管理员或所有者
Router.Delete("/projects/{id}", middleware.AdminOrOwner("owner_id"), "删除项目", deleteProject)
## 4. 数据库配置权限
### 4.1 路由权限表 (route_policies)
| 字段 | 说明 | 示例 |
|------|------|------|
| domain | 领域标识 | "myapp" |
| prefix | 路由前缀 | "/api" |
| pattern | 路由模式 | "/users/{id}" |
| method | HTTP方法 | "GET" |
| resource | 资源类型 | "user" |
| action | 操作类型 | "read" |
| effect | 效果 | "allow"/"deny" |
| condition | 条件 | "owner"/"admin" |
| required_url | 申请地址 | "/apply-perm?r=user&a=read" |
| is_public | 是否公开 | true/false |
### 4.2 用户权限表 (policies)
```sql
-- 用户拥有 user:read 权限
INSERT INTO policies (code, resource, action, effect)
VALUES ('user-read', 'user', 'read', 'allow');
-- 关联到角色
INSERT INTO roles (code, name, policy_ids)
VALUES ('viewer', '查看者', 'policy-id-1,policy-id-2');
```
### 4.4 代码中手动检查
## 5. 在 Handler 中设置资源所有者
```go
func myHandler(x *vigo.X, req *Req) error {
checker := middleware.NewChecker(x)
func getUser(x *vigo.X, req *GetUserReq) (*User, error) {
user := queryUser(req.ID)
// 检查是否为管理员
if !checker.IsOrgAdmin() {
return vigo.ErrForbidden
}
// 设置所有者到上下文,用于 condition=owner 检查
x.Set("owner_id", user.ID)
// 检查具体权限
if err := checker.RequirePermission("resource", "write"); err != nil {
return err
}
return nil
return user, nil
}
```
## 5. 完整示例
## 6. 完整示例
```go
package main
@ -143,15 +107,51 @@ func main() {
// 1. 挂载 vbase
r.Extend("/api/vb", api.Router)
// 2. 业务路由加权限
project := r.SubRouter("/projects")
project.Use(middleware.AuthRequired())
// 2. 业务路由
api := r.SubRouter("/api/v1")
// 3. 启用权限检查
api.Use(middleware.AuthRequired())
api.Use(middleware.OrgContext())
api.Use(middleware.Perm("myapp"))
project.Get("/", middleware.Permission("project", "list"), "项目列表", listProjects)
project.Post("/", middleware.Permission("project", "create"), "创建项目", createProject)
project.Patch("/{id}", middleware.PermissionWithOwner("project", "update", "owner_id"), "更新项目", updateProject)
project.Delete("/{id}", middleware.AdminOrOwner("owner_id"), "删除项目", deleteProject)
// 4. 注册权限(只需一次,后续在数据库中配置)
middleware.RegisterRoutePolicy("myapp", "/api/v1", "/projects", "GET", "project", "list", false)
middleware.RegisterRoutePolicy("myapp", "/api/v1", "/projects", "POST", "project", "create", false)
middleware.RegisterRoutePolicy("myapp", "/api/v1", "/projects/{id}", "GET", "project", "read", false)
middleware.RegisterRoutePolicy("myapp", "/api/v1", "/projects/{id}", "PATCH", "project", "update", false)
middleware.RegisterRoutePolicy("myapp", "/api/v1", "/projects/{id}", "DELETE", "project", "delete", false)
// 5. 无需在 handler 中加权限代码
api.Get("/projects", "项目列表", listProjects)
api.Post("/projects", "创建项目", createProject)
api.Get("/projects/{id}", "项目详情", getProject)
api.Patch("/projects/{id}", "更新项目", updateProject)
api.Delete("/projects/{id}", "删除项目", deleteProject)
vigo.Run(r)
}
```
## 7. 权限检查流程
```
请求 → AuthRequired(认证) → OrgContext(组织) → Perm(权限检查) → Handler
查找 route_policies
匹配 domain + method + path
检查 resource:action 权限
无权限 → 返回 X-Required-URL
```
## 8. 配置热更新
```go
// 修改数据库后,调用 API 刷新缓存
middleware.ReloadRoutePolicies()
```
无需重启服务,权限配置实时生效。

@ -0,0 +1,285 @@
# VBase 权限模型设计文档
## 核心原则
1. **Policy策略**:定义"什么权限可以访问哪些接口"
2. **Role角色**Policy 的集合,简化授权
3. **授权关系**:用户通过 Role 或直接与 Policy 关联,并携带数据范围
---
## 表结构关系
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Policy │◄────┤ PolicyRoute │ │ Role │
│ (策略定义) │ │ (策略路由绑定) │ │ (角色定义) │
└────────┬────────┘ └──────────────────┘ └────────┬────────┘
│ │
│ Many-to-Many │ Many-to-Many
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ UserPermission (用户授权) │
│ ├─ user_id: 用户ID │
│ ├─ policy_code: 策略代码 (或直接关联 Policy) │
│ ├─ role_id: 角色ID (可选,与 policy_code 二选一) │
│ ├─ resource: 资源类型 (project/org/...) │
│ ├─ resource_id: 具体资源ID (proj-123, * 表示所有) │
│ ├─ scope: 权限范围 (owner/admin/member) │
│ └─ expire_at: 过期时间 (可选) │
└─────────────────────────────────────────────────────────────────┘
```
---
## 详细表设计
### 1. Policy策略表
定义权限的本质:对某资源可以执行什么操作
| 字段 | 类型 | 说明 |
|------|------|------|
| id | string | UUID |
| code | string | **唯一标识**,如 `project:read`, `project:write`, `project:admin` |
| resource | string | 资源类型:`user`, `org`, `project`, `*`(所有) |
| action | string | 操作类型:`create`, `read`, `update`, `delete`, `list`, `*`, `admin` |
| effect | string | `allow` / `deny` |
| scope | string | 作用域:`platform`(平台级) / `org`(组织级) |
| description | string | 描述 |
**示例数据:**
```sql
-- 项目相关权限
('p1', 'project:create', 'project', 'create', 'allow', 'org', '创建项目')
('p2', 'project:read', 'project', 'read', 'allow', 'org', '查看项目')
('p3', 'project:write', 'project', 'write', 'allow', 'org', '编辑项目(包含read)')
('p4', 'project:delete', 'project', 'delete', 'allow', 'org', '删除项目')
('p5', 'project:admin', 'project', 'admin', 'allow', 'org', '项目管理员(包含所有)')
-- 通配权限
('p9', 'project:*', 'project', '*', 'allow', 'org', '所有项目权限')
('p0', '*:*', '*', '*', 'allow', 'platform', '超级管理员')
```
**层级关系:**
```
project:admin > project:write > project:read
│ │ └─ project:create, project:delete
└──────────────┴──────────────────────────────────────────────
```
---
### 2. PolicyRoute策略路由绑定表
Policy 与具体接口的绑定关系。一个 Policy 可以绑定多个路由。
| 字段 | 类型 | 说明 |
|------|------|------|
| id | string | UUID |
| policy_id | string | 关联 Policy.id |
| domain | string | 应用域,如 `vbase`, `myapp` |
| prefix | string | 路由前缀,如 `/api/v1` |
| pattern | string | 路由模式,如 `/projects`, `/projects/{id}` |
| method | string | HTTP 方法:`GET`, `POST`, `*`, ... |
| resource_id_param | string | 从 URL 提取资源ID的参数名`id` |
| description | string | 接口描述 |
**示例数据:**
```sql
-- project:read 权限可以访问以下接口
('r1', 'p2', 'myapp', '/api/v1', '/projects', 'GET', '', '项目列表')
('r2', 'p2', 'myapp', '/api/v1', '/projects/{id}', 'GET', 'id', '项目详情')
('r3', 'p2', 'myapp', '/api/v1', '/projects/{id}/logs', 'GET', 'id', '项目日志')
-- project:write 权限
('r4', 'p3', 'myapp', '/api/v1', '/projects/{id}', 'PATCH', 'id', '更新项目')
('r5', 'p3', 'myapp', '/api/v1', '/projects/{id}/env', 'PUT', 'id', '更新环境变量')
-- project:create 权限
('r6', 'p1', 'myapp', '/api/v1', '/projects', 'POST', '', '创建项目')
-- project:admin 权限(包含所有)
('r7', 'p5', 'myapp', '/api/v1', '/projects/{id}/members', 'GET', 'id', '成员管理')
('r8', 'p5', 'myapp', '/api/v1', '/projects/{id}/settings', '*', 'id', '设置管理')
```
---
### 3. Role角色表
Policy 的集合,用于批量授权。
| 字段 | 类型 | 说明 |
|------|------|------|
| id | string | UUID |
| org_id | string | 组织ID系统角色为空 |
| code | string | 角色代码:`admin`, `developer`, `viewer`, `owner` |
| name | string | 角色名称 |
| policy_codes | string | 关联的 Policy code 列表,逗号分隔 |
| is_system | bool | 是否系统预设角色 |
**示例数据:**
```sql
-- 系统预设角色
('r1', '', 'owner', '所有者', 'project:admin,org:admin,*:*', true)
('r2', '', 'admin', '管理员', 'project:write,org:read,user:read', true)
('r3', '', 'developer', '开发者', 'project:write,resource:read', true)
('r4', '', 'viewer', '只读用户', 'project:read', true)
-- 组织自定义角色
('r5', 'org-123', 'pm', '项目经理', 'project:admin', false)
```
---
### 4. UserPermission用户授权表
核心授权表,记录"谁对什么资源有什么权限"。
| 字段 | 类型 | 说明 |
|------|------|------|
| id | string | UUID |
| user_id | string | 用户ID |
| org_id | string | 组织ID可选 |
| **policy_code** | string | **策略代码**(如 `project:read` |
| **resource** | string | **资源类型**(如 `project` |
| **resource_id** | string | **具体资源ID**(如 `proj-123``*` |
| granted_by | string | 授权来源:`role`(通过角色) / `direct`(直接授权) |
| source_id | string | 来源IDRole ID 或留空) |
| expire_at | timestamp | 过期时间(可选,永久有效为空) |
| created_at | timestamp | 创建时间 |
**关键设计:**
- `resource_id = *` 表示对该类型所有资源的权限
- 数据级权限通过具体的 `resource_id` 实现
- 用户最终权限 = 直接授权 + 角色授权 的并集
**示例数据:**
```sql
-- 场景:用户 A
-- 1. 是项目 1 的 admin直接授权
('up1', 'user-a', 'org-1', 'project:admin', 'project', 'proj-1', 'direct', '', null)
-- 2. 是项目 2 的只读成员(直接授权)
('up2', 'user-a', 'org-1', 'project:read', 'project', 'proj-2', 'direct', '', null)
-- 3. 通过 developer 角色获得对所有项目的 write 权限
('up3', 'user-a', 'org-1', 'project:write', 'project', '*', 'role', 'role-dev-id', null)
-- 4. 临时授权3天后过期
('up4', 'user-a', 'org-1', 'project:admin', 'project', 'proj-3', 'direct', '', '2025-02-20 00:00:00')
```
---
## 权限检查流程
```
请求: GET /api/v1/projects/proj-123/settings
Header: X-Org-ID: org-1
┌─────────────────────────────────────┐
│ 1. 路由匹配 │
│ domain=myapp + method=GET + │
│ pattern=/projects/{id}/settings │
│ │
│ 找到: PolicyRoute.r8 │
│ 对应: Policy.p5 (project:admin) │
│ 提取: resource_id = "proj-123" │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ 2. 获取用户权限列表 │
│ SELECT * FROM user_permissions │
│ WHERE user_id = 'user-a' │
│ AND resource = 'project' │
│ AND (resource_id = 'proj-123' │
│ OR resource_id = '*') │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ 3. 权限层级匹配 │
│ 需要的权限: project:admin │
│ │
│ 用户有的权限: │
│ - project:admin (proj-1) ❌ │
│ - project:read (proj-2) ❌ │
│ - project:write (*) ❌ │
│ (write < admin)
│ │
│ 结果: 拒绝 ❌ │
└─────────────────────────────────────┘
拒绝,返回 X-Required-URL: /apply-perm?resource=project&action=admin&id=proj-123
```
---
## 场景覆盖
### 场景1用户是项目 owner拥有所有权限
```sql
INSERT INTO user_permissions (user_id, policy_code, resource, resource_id)
VALUES ('user-a', 'project:admin', 'project', 'proj-1')
-- 自动包含 project:read/write/delete/create
```
### 场景2平台管理员拥有所有资源的所有权限
```sql
-- 方案一:通配符
INSERT INTO user_permissions (user_id, policy_code, resource, resource_id)
VALUES ('admin', '*:*', '*', '*')
-- 方案二:绑定 owner 角色
-- Role.owner 包含 policy_codes: '*:*'
```
### 场景3组织管理员拥有组织内所有项目权限
```sql
-- 组织 admin 角色包含 project:*
-- 在组织上下文内生效
INSERT INTO user_permissions (user_id, org_id, policy_code, resource, resource_id, granted_by)
VALUES ('user-a', 'org-1', 'project:*', 'project', '*', 'role')
```
### 场景4用户加入多个项目权限不同
```sql
-- 项目1: admin
INSERT INTO user_permissions VALUES ('u1', 'project:admin', 'project', 'proj-1', ...)
-- 项目2: read
INSERT INTO user_permissions VALUES ('u1', 'project:read', 'project', 'proj-2', ...)
-- 项目3: write
INSERT INTO user_permissions VALUES ('u1', 'project:write', 'project', 'proj-3', ...)
```
---
## 与旧方案对比
| 维度 | 旧方案 (RoutePolicy) | 新方案 (Policy + UserPermission) |
|------|---------------------|--------------------------------|
| 核心思想 | 接口白名单 | 资源操作权限 |
| 配置粒度 | 每个接口单独配置 | 按 Resource + Action 配置 |
| 数据级权限 | 难实现 | 通过 resource_id 字段 |
| 通配授权 | 不支持 | 支持 `*` |
| 层级权限 | 无 | admin > write > read |
| 新增接口成本 | 高(需给所有角色加策略) | 低(绑定到 Policy 即可)|
| 角色数量 | 多(每项目配角色) | 少(复用系统角色 + 数据范围)|
---
请确认此设计后,我开始修改代码。

@ -8,16 +8,16 @@ package vbase
import (
"github.com/veypi/vbase/api"
"github.com/veypi/vbase/auth"
"github.com/veypi/vigo"
"github.com/veypi/vigo/contrib/cors"
)
var Router = vigo.NewRouter()
// Auth 全局权限工厂
var Auth = auth.Factory
func init() {
// 挂载 API 路由
Router.Extend("/api", api.Router)
// CORS 支持
Router.SubRouter("/**").Use(cors.AllowAny)
}

@ -0,0 +1,146 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-02-14 16:08:06
// Distributed under terms of the MIT license.
//
package models
import (
"time"
"gorm.io/gorm"
)
// 角色代码常量
const (
RoleCodeAdmin = "admin"
RoleCodeUser = "user"
RoleCodeViewer = "viewer"
)
// Permission 权限定义表(权限字典)
// ID 格式: app:resource:action (例如: crm:customer:read)
type Permission struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
ID string `json:"id" gorm:"primaryKey;size:100" desc:"权限ID格式: app:resource:action"`
AppKey string `json:"app_key" gorm:"index;size:50" desc:"应用标识"`
Resource string `json:"resource" gorm:"index;size:50" desc:"资源类型"`
Action string `json:"action" gorm:"index;size:50" desc:"操作类型"`
Description string `json:"description" desc:"权限描述"`
}
func (Permission) TableName() string {
return "permissions"
}
// Role 角色表(不关联 app可跨应用
type Role struct {
Base
OrgID string `json:"org_id" gorm:"index;size:36" desc:"组织ID空=系统预设"`
Code string `json:"code" gorm:"index;size:50" desc:"角色代码"`
Name string `json:"name" desc:"角色名称"`
Description string `json:"description" desc:"角色描述"`
IsSystem bool `json:"is_system" desc:"是否系统预设角色"`
Status int `json:"status" gorm:"default:1" desc:"状态: 1=启用, 0=禁用"`
}
func (Role) TableName() string {
return "roles"
}
// RolePermission 角色权限关联表
type RolePermission struct {
Base
RoleID string `json:"role_id" gorm:"index;size:36" desc:"角色ID"`
PermissionID string `json:"permission_id" gorm:"index;size:100" desc:"权限ID"`
Condition string `json:"condition" gorm:"size:20;default:'none'" desc:"权限条件: none/owner/admin"`
}
func (RolePermission) TableName() string {
return "role_permissions"
}
// UserRole 用户角色关联表
type UserRole struct {
Base
UserID string `json:"user_id" gorm:"index;size:36" desc:"用户ID"`
OrgID string `json:"org_id" gorm:"index;size:36" desc:"组织ID"`
RoleID string `json:"role_id" gorm:"index;size:36" desc:"角色ID"`
ExpireAt time.Time `json:"expire_at" desc:"过期时间(可选)"`
}
func (UserRole) TableName() string {
return "user_roles"
}
// UserPermission 用户特定资源权限表(数据级权限)
type UserPermission struct {
Base
UserID string `json:"user_id" gorm:"index;size:36" desc:"用户ID"`
OrgID string `json:"org_id" gorm:"index;size:36" desc:"组织ID"`
PermissionID string `json:"permission_id" gorm:"index;size:100" desc:"权限ID"`
ResourceID string `json:"resource_id" gorm:"index;size:100" desc:"具体资源ID* 表示所有"`
ExpireAt time.Time `json:"expire_at" desc:"过期时间(可选)"`
GrantedBy string `json:"granted_by" gorm:"size:36" desc:"授权人ID"`
}
func (UserPermission) TableName() string {
return "user_permissions"
}
// AppConfig 应用配置(用于权限初始化)
type AppConfig struct {
Name string `json:"name" desc:"应用名称"`
Description string `json:"description" desc:"应用描述"`
DefaultRoles []RoleDefinition `json:"default_roles" desc:"预设角色"`
}
// RoleDefinition 角色定义(配置用)
type RoleDefinition struct {
Code string `json:"code" desc:"角色代码"`
Name string `json:"name" desc:"角色名称"`
Description string `json:"description" desc:"角色描述"`
Policies []string `json:"policies" desc:"权限列表: ["customer:read", "*:*"]"`
}
// GrantRoleRequest 授予角色请求
type GrantRoleRequest struct {
UserID string `json:"user_id" desc:"用户ID"`
OrgID string `json:"org_id" desc:"组织ID"`
RoleCode string `json:"role_code" desc:"角色代码"`
ExpireAt time.Time `json:"expire_at" desc:"过期时间(可选)"`
}
// GrantResourcePermRequest 授予资源权限请求
type GrantResourcePermRequest struct {
UserID string `json:"user_id" desc:"用户ID"`
OrgID string `json:"org_id" desc:"组织ID"`
PermissionID string `json:"permission_id" desc:"权限ID"`
ResourceID string `json:"resource_id" desc:"资源实例ID* 表示所有"`
ExpireAt time.Time `json:"expire_at" desc:"过期时间(可选)"`
GrantedBy string `json:"granted_by" desc:"授权人ID"`
}
// CheckPermRequest 检查权限请求
type CheckPermRequest struct {
UserID string `json:"user_id" desc:"用户ID"`
OrgID string `json:"org_id" desc:"组织ID"`
PermissionID string `json:"permission_id" desc:"权限ID"`
ResourceID string `json:"resource_id" desc:"资源实例ID可选"`
}
// UserPermissionResult 用户权限结果
type UserPermissionResult struct {
PermissionID string `json:"permission_id" desc:"权限ID"`
ResourceID string `json:"resource_id" desc:"资源ID* 表示所有"`
Actions []string `json:"actions" desc:"允许的操作"`
}
// ResourceUser 资源授权用户
type ResourceUser struct {
UserID string `json:"user_id" desc:"用户ID"`
Actions []string `json:"actions" desc:"允许的操作"`
}

@ -20,9 +20,15 @@ func init() {
AllModels.Add(&Session{})
AllModels.Add(&Org{})
AllModels.Add(&OrgMember{})
AllModels.Add(&Policy{})
// Auth 模块模型
AllModels.Add(&Permission{})
AllModels.Add(&Role{})
AllModels.Add(&RolePolicy{})
AllModels.Add(&RolePermission{})
AllModels.Add(&UserRole{})
AllModels.Add(&UserPermission{})
// OAuth 模型
AllModels.Add(&OAuthClient{})
AllModels.Add(&OAuthAuthorizationCode{})
AllModels.Add(&OAuthToken{})

@ -1,68 +0,0 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-03-04 16:08:06
// Distributed under terms of the MIT license.
//
package models
// Policy 策略定义
type Policy struct {
Base
Code string `json:"code" gorm:"uniqueIndex;size:50;not null"`
Name string `json:"name" gorm:"size:50;not null"`
Description string `json:"description" gorm:"size:200"`
Resource string `json:"resource" gorm:"size:100;not null"` // 资源: user/org/resource/*
Action string `json:"action" gorm:"size:50;not null"` // 操作: create/read/update/delete/*
Effect string `json:"effect" gorm:"size:10;not null"` // 效果: allow/deny
Condition string `json:"condition" gorm:"size:500"` // 条件: "owner", "org_member"
Scope string `json:"scope" gorm:"size:20;not null"` // 作用域: platform/org/resource
}
func (Policy) TableName() string {
return "policies"
}
// Role 角色定义
type Role struct {
Base
OrgID string `json:"org_id" gorm:"index;not null"`
Name string `json:"name" gorm:"size:50;not null"`
Code string `json:"code" gorm:"size:50;not null"`
Description string `json:"description" gorm:"size:200"`
PolicyIDs string `json:"policy_ids" gorm:"size:500"` // 逗号分隔的策略ID
Scope string `json:"scope" gorm:"size:20;default:'org'"` // platform/org
IsSystem bool `json:"is_system" gorm:"default:false"` // 是否系统预设角色
}
func (Role) TableName() string {
return "roles"
}
// RolePolicy 角色策略关联表
type RolePolicy struct {
Base
RoleID string `json:"role_id" gorm:"uniqueIndex:idx_role_policy;not null"`
PolicyID string `json:"policy_id" gorm:"uniqueIndex:idx_role_policy;not null"`
}
func (RolePolicy) TableName() string {
return "role_policies"
}
// 预设策略常量
const (
PolicyEffectAllow = "allow"
PolicyEffectDeny = "deny"
PolicyScopePlatform = "platform"
PolicyScopeOrg = "org"
PolicyScopeResource = "resource"
)
// 预设角色常量
const (
RoleCodeOwner = "owner"
RoleCodeAdmin = "admin"
RoleCodeDeveloper = "developer"
RoleCodeViewer = "viewer"
)
Loading…
Cancel
Save