update perm

v3
veypi 1 week ago
parent 52f2ae35ab
commit ced7cc6a07

@ -12,6 +12,8 @@ import (
"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"
@ -30,6 +32,8 @@ func init() {
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 {

@ -0,0 +1,162 @@
//
// 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,
},
}
}

@ -0,0 +1,155 @@
//
// 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"
)
// Checker 权限检查器
type Checker struct {
userID string
orgID string
roles []string
org *models.Org
}
// NewChecker 创建权限检查器
func NewChecker(x *vigo.X) *Checker {
c := &Checker{}
if uid, ok := x.Get("user_id").(string); ok {
c.userID = uid
}
if oid, ok := x.Get("org_id").(string); ok {
c.orgID = oid
}
if roles, ok := x.Get("org_roles").(string); ok && roles != "" {
c.roles = strings.Split(roles, ",")
}
return c
}
// IsOrgOwner 检查用户是否是组织所有者
func (c *Checker) IsOrgOwner() bool {
if c.orgID == "" || c.userID == "" {
return false
}
if c.org != nil {
return c.org.OwnerID == c.userID
}
var org models.Org
if err := cfg.DB().First(&org, "id = ?", c.orgID).Error; err != nil {
return false
}
c.org = &org
return org.OwnerID == c.userID
}
// IsOrgAdmin 检查用户是否是组织管理员
func (c *Checker) IsOrgAdmin() bool {
if c.IsOrgOwner() {
return true
}
for _, roleID := range c.roles {
var role models.Role
if err := cfg.DB().First(&role, "id = ?", roleID).Error; err != nil {
continue
}
if role.Code == models.RoleCodeAdmin {
return true
}
}
return false
}
// HasRole 检查用户是否有指定角色
func (c *Checker) HasRole(roleCode string) bool {
for _, roleID := range c.roles {
var role models.Role
if err := cfg.DB().First(&role, "id = ?", roleID).Error; err != nil {
continue
}
if role.Code == roleCode {
return true
}
}
return false
}
// RequireAdmin 要求管理员权限
func (c *Checker) RequireAdmin() error {
if !c.IsOrgAdmin() {
return vigo.ErrForbidden.WithString("admin permission required")
}
return nil
}
// RequireOwner 要求所有者权限
func (c *Checker) RequireOwner() error {
if !c.IsOrgOwner() {
return vigo.ErrForbidden.WithString("owner permission required")
}
return nil
}
// RequireOrg 要求必须在组织上下文中
func (c *Checker) RequireOrg() error {
if c.orgID == "" {
return vigo.ErrArgInvalid.WithString("organization context required")
}
return nil
}
// UserID 获取用户ID
func (c *Checker) UserID() string {
return c.userID
}
// OrgID 获取组织ID
func (c *Checker) OrgID() string {
return c.orgID
}
// GetUserRoles 获取用户角色列表
func (c *Checker) GetUserRoles() []string {
return c.roles
}
// RequireAdmin 中间件:要求管理员权限
func RequireAdmin() func(*vigo.X) error {
return func(x *vigo.X) error {
checker := NewChecker(x)
return checker.RequireAdmin()
}
}
// RequireOwner 中间件:要求组织所有者权限
func RequireOwner() func(*vigo.X) error {
return func(x *vigo.X) error {
checker := NewChecker(x)
return checker.RequireOwner()
}
}
// RequireOrgContext 中间件:要求必须在组织上下文中
func RequireOrgContext() func(*vigo.X) error {
return func(x *vigo.X) error {
checker := NewChecker(x)
return checker.RequireOrg()
}
}

@ -0,0 +1,224 @@
//
// 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")
}
}

@ -0,0 +1,57 @@
//
// 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
}

@ -0,0 +1,35 @@
//
// 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
}

@ -0,0 +1,25 @@
//
// 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
}

@ -0,0 +1,25 @@
//
// 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)
}

@ -0,0 +1,63 @@
//
// 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
}

@ -0,0 +1,53 @@
//
// 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
}

@ -0,0 +1,52 @@
// 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
}

@ -0,0 +1,33 @@
// 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
}

@ -0,0 +1,36 @@
// 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
}

@ -0,0 +1,28 @@
// 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)
}

@ -0,0 +1,57 @@
// 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
}

@ -0,0 +1,54 @@
// 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
}

@ -0,0 +1,129 @@
// 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
}

@ -0,0 +1,14 @@
//
// init.go
// Copyright (C) 2025 veypi <i@veypi.com>
//
// Distributed under terms of the MIT license.
//
package sms
import "github.com/veypi/vigo"
var Router = vigo.NewRouter()

@ -0,0 +1,105 @@
package sms
import (
"fmt"
"time"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/libs/sms_providers"
"github.com/veypi/vbase/libs/utils"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
"github.com/veypi/vigo/contrib/limiter"
"gorm.io/gorm"
)
// SendCodeRequest 发送验证码请求
type SendCodeRequest struct {
Phone string `json:"phone" src:"json" desc:"手机号"`
Region string `json:"region" src:"json" desc:"区域"`
Purpose string `json:"purpose" src:"json" desc:"用途"`
TemplateID string `json:"template_id" src:"json" desc:"模板ID"`
}
// SendCodeResponse 发送验证码响应
type SendCodeResponse struct {
ID string `json:"id"`
Phone string `json:"phone"`
ExpiresAt time.Time `json:"expires_at"`
Interval int `json:"interval"` // 下次可发送间隔(秒)
}
var _ = Router.Post("/", "发送验证码", vigo.SkipBefore, limiter.NewAdvancedRequestLimiter(time.Minute*3, 5, time.Second*30).Limit, sendCode)
// SendCode 发送验证码
func sendCode(x *vigo.X, req *SendCodeRequest) (*SendCodeResponse, error) {
// 1. 验证手机号
normalizedPhone := utils.NormalizePhoneNumber(req.Phone)
if !utils.ValidatePhoneNumber(normalizedPhone) {
return nil, fmt.Errorf("invalid phone number: %s", req.Phone)
}
// 2. 检查区域配置
provider, err := sms_providers.GetSMSProvider(req.Region)
if err != nil {
return nil, err
}
// 3. 检查发送频率限制
if err := CheckSendInterval(normalizedPhone, req.Region, req.Purpose); err != nil {
return nil, err
}
// 4. 检查每日发送次数限制
if err := CheckDailyLimit(normalizedPhone, req.Region); err != nil {
return nil, err
}
// 5. 生成验证码
code, err := utils.GenerateCode(cfg.Config.SMS.Global.CodeLength)
if err != nil {
return nil, fmt.Errorf("failed to generate code: %w", err)
}
// 6. 创建验证码记录
smsCode := &models.SMSCode{
Phone: normalizedPhone,
Code: code,
Region: req.Region,
Purpose: req.Purpose,
Status: models.CodeStatusPending,
ExpiresAt: time.Now().Add(cfg.Config.SMS.Global.CodeExpiry),
}
// 7. 发送短信
sendReq := &sms_providers.SendSMSRequest{
Phone: normalizedPhone,
TemplateID: req.TemplateID,
Region: req.Region,
Purpose: req.Purpose,
Params: map[string]string{
"code": code,
},
}
err = cfg.DB().Transaction(func(tx *gorm.DB) error {
err := tx.Create(smsCode).Error
if err != nil {
return err
}
smsID, err := provider.SendSMS(x.Context(), sendReq)
LogSMS(normalizedPhone, req.Region, provider.GetName(), smsID, err)
if err != nil || smsID == "" {
return fmt.Errorf("failed to send sms: %v", err)
}
return nil
})
if err != nil {
return nil, err
}
return &SendCodeResponse{
ID: smsCode.ID,
Phone: smsCode.Phone,
ExpiresAt: smsCode.ExpiresAt,
Interval: int(cfg.Config.SMS.Global.SendInterval.Seconds()),
}, nil
}

@ -0,0 +1,90 @@
//
// utils.go
// Copyright (C) 2025 veypi <i@veypi.com>
//
// Distributed under terms of the MIT license.
//
package sms
import (
"fmt"
"time"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"gorm.io/gorm"
)
// checkDailyLimit 检查每日发送次数限制
func CheckDailyLimit(phone, region string) error {
today := time.Now().Truncate(24 * time.Hour)
tomorrow := today.Add(24 * time.Hour)
var count int64
err := cfg.DB().Model(&models.SMSCode{}).
Where("phone = ? AND region = ? AND created_at >= ? AND created_at < ?",
phone, region, today, tomorrow).
Count(&count).Error
if err != nil {
return fmt.Errorf("failed to check daily limit: %w", err)
}
if count >= int64(cfg.Config.SMS.Global.MaxDailyCount) {
return fmt.Errorf("daily sms limit exceeded")
}
return nil
}
// checkSendInterval 检查发送间隔
func CheckSendInterval(phone, region, purpose string) error {
var lastCode models.SMSCode
err := cfg.DB().Where("phone = ? AND region = ? AND purpose = ?", phone, region, purpose).
Order("created_at DESC").
First(&lastCode).Error
if err != nil && err != gorm.ErrRecordNotFound {
return fmt.Errorf("failed to check send interval: %w", err)
}
if err == nil {
timeSinceLastSend := time.Since(lastCode.CreatedAt)
if timeSinceLastSend < cfg.Config.SMS.Global.SendInterval {
remaining := cfg.Config.SMS.Global.SendInterval - timeSinceLastSend
return fmt.Errorf("please wait %d seconds before sending again", int(remaining.Seconds()))
}
}
return nil
}
// logSMS 记录短信发送日志
func LogSMS(phone, region, provider, messageID string, err error) {
log := &models.SMSLog{
Phone: phone,
Region: region,
Provider: provider,
MessageID: messageID,
Status: "ok",
}
if err != nil {
log.Status = "failed"
log.Error = err.Error()
}
cfg.DB().Create(log)
}
// CleanupExpiredCodes 清理过期的验证码
func CleanupExpiredCodes() error {
result := cfg.DB().Model(&models.SMSCode{}).
Where("status = ? AND expires_at < ?", models.CodeStatusPending, time.Now()).
Update("status", models.CodeStatusExpired)
if result.Error != nil {
return fmt.Errorf("failed to cleanup expired codes: %w", result.Error)
}
return nil
}

@ -0,0 +1,73 @@
//
// validate.go
// Copyright (C) 2025 veypi <i@veypi.com>
//
// Distributed under terms of the MIT license.
//
package sms
import (
"fmt"
"time"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/libs/utils"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
"gorm.io/gorm"
)
// VerifyCode 验证验证码
func VerifyCode(Phone, Code, Region, Purpose string) error {
normalizedPhone := utils.NormalizePhoneNumber(Phone)
// 1. 查找最新的待验证码
var smsCode models.SMSCode
err := cfg.DB().Where("phone = ? AND region = ? AND purpose = ? AND status = ?",
normalizedPhone, Region, Purpose, models.CodeStatusPending).
Order("created_at DESC").
First(&smsCode).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return vigo.NewError("verification code not found or already used").WithCode(404)
}
return vigo.NewError("failed to query sms code").WithError(err)
}
// 2. 检查验证码是否过期
if smsCode.IsExpired() {
cfg.DB().Model(&smsCode).Updates(map[string]any{
"status": models.CodeStatusExpired,
})
return fmt.Errorf("verification code has expired")
}
// 3. 检查是否已达到最大尝试次数
if !smsCode.CanRetry(cfg.Config.SMS.Global.MaxAttempts) {
cfg.DB().Model(&smsCode).Updates(map[string]any{
"status": models.CodeStatusFailed,
})
return fmt.Errorf("verification failed too many times")
}
// 4. 验证码不匹配
if smsCode.Code != Code {
cfg.DB().Model(&smsCode).Updates(map[string]any{
"attempts": smsCode.Attempts + 1,
})
remaining := cfg.Config.SMS.Global.MaxAttempts - smsCode.Attempts - 1
return fmt.Errorf("verification code incorrect, %d attempts remaining", remaining)
}
// 5. 验证成功
now := time.Now()
cfg.DB().Model(&smsCode).Updates(map[string]any{
"status": models.CodeStatusUsed,
"used_at": &now,
})
return nil
}

@ -51,7 +51,7 @@ VBase 是一个 **产品化的 IAM + BaaS 平台**,你可以将其**售卖/部
docker-compose up -d
# 或使用源码
go run ./cmd/server
go run ./cli/main.go
```
### 2. 获取初始管理员密码

@ -41,7 +41,7 @@ VBase 是一个**产品化**的解决方案,你可以将 VBase **售卖/部署
#### 目标客户类型
| 客户类型 | 场景说明 | VBase 提供的价值 |
|----------|----------|-----------------|
| ---------------- | ------------------------------ | -------------------------------- |
| **2C 产品公司** | 社交 App、电商平台、内容社区等 | 用户认证、个人数据管理、权限控制 |
| **2B 产品公司** | SaaS 软件、企业管理系统 | 多租户隔离、组织架构、RBAC 权限 |
| **混合产品公司** | 既有 C 端用户又有 B 端管理 | 统一身份管理、分级权限控制 |
@ -59,7 +59,7 @@ VBase 是一个**产品化**的解决方案,你可以将 VBase **售卖/部署
### 1.3 技术栈
| 层级 | 技术 |
|------|------|
| -------- | ------------------------------- |
| 框架 | Vigo (Go Web Framework) |
| ORM | GORM |
| 数据库 | MySQL / PostgreSQL / SQLite |
@ -72,7 +72,7 @@ VBase 是一个**产品化**的解决方案,你可以将 VBase **售卖/部署
#### 三层级角色体系
| 层级 | 角色 | 说明 | 权限范围 |
|------|------|------|----------|
| ---------- | ---------- | -------------------------- | ------------------------- |
| **平台层** | 平台管理员 | VBase 产品提供商(你) | 管理所有 VBase 实例 |
| **实例层** | 实例管理员 | 客户的技术负责人 | 管理自己的 VBase 实例配置 |
| **业务层** | 终端用户 | 客户的 C 端用户或 B 端成员 | 使用客户的产品功能 |
@ -82,21 +82,25 @@ VBase 是一个**产品化**的解决方案,你可以将 VBase **售卖/部署
在一个独立的 VBase 实例内部,包含以下核心概念:
**用户 (User)**: VBase 实例中的注册用户
- 可以创建多个项目
- 可以被邀请加入其他项目
- 在不同项目中可以有不同的角色
**项目/组织 (Project/Org)**: 资源管理的基本单元
- 对 2C 场景:对应一个 App 或产品
- 对 2B 场景:对应一个企业或部门
- 企业可以创建多个项目,也可以使用组织层级管理
**资源 (Resource)**: 项目内的后端资源
- 数据库、存储、函数、API 等
- 属于特定项目
- 通过项目权限控制访问
**角色 (Role)**: 项目内的权限集合
- 所有者 (Owner): 项目全部权限
- 管理员 (Admin): 管理项目和成员
- 开发者 (Developer): 读写资源
@ -181,6 +185,7 @@ VBase 设计为可私有化部署的产品,支持两种交付模式:
```
**特点:**
- 你在自己的云基础设施上为客户部署独立的 VBase 实例
- 每个实例完全隔离(独立数据库、独立 Redis
- 你负责运维,客户只管使用
@ -208,6 +213,7 @@ VBase 设计为可私有化部署的产品,支持两种交付模式:
```
**特点:**
- 客户在自己的环境中部署 VBase云服务器、私有云、K8s
- 数据完全属于客户,与你无关
- 客户自己运维,或者购买你的运维支持服务
@ -218,7 +224,7 @@ VBase 设计为可私有化部署的产品,支持两种交付模式:
### 3.3 两种模式的对比
| 维度 | 托管部署 | 客户自托管 |
|------|----------|------------|
| ------------ | ---------------- | --------------- |
| **部署位置** | 你的云基础设施 | 客户的基础设施 |
| **数据归属** | 客户拥有,你代管 | 客户完全拥有 |
| **运维责任** | 你负责 | 客户负责 |
@ -226,27 +232,8 @@ VBase 设计为可私有化部署的产品,支持两种交付模式:
| **定制化** | 受限 | 完全可定制 |
| **网络要求** | 需要公网访问 | 可内网部署 |
### 3.4 关键设计:完全隔离
无论哪种模式,**每个 VBase 实例都是完全独立的**
```go
// 实例级别的完全隔离
// - 独立的数据库连接
// - 独立的 Redis 命名空间
// - 独立的 JWT 密钥
// - 独立的配置
type VBaseInstance struct {
InstanceID string // 实例唯一标识
DatabaseDSN string // 独立数据库
RedisPrefix string // Redis 前缀隔离
JWTSecret string // 独立 JWT 密钥
Config InstanceConfig
}
```
这种设计确保:
1. 客户 A 的数据与客户 B 完全隔离
2. 一个实例的故障不影响其他实例
3. 客户可以独立升级、备份、迁移自己的实例
@ -289,6 +276,21 @@ vbase/
│ │ ├── revoke.go # 撤销令牌
│ │ ├── oidc.go # OIDC 支持
│ │ └── client.go # 客户端管理
│ ├── policy/ # 策略管理RBAC
│ │ ├── init.go
│ │ ├── list.go
│ │ ├── get.go
│ │ ├── create.go
│ │ ├── patch.go
│ │ └── del.go
│ ├── role/ # 角色管理RBAC
│ │ ├── init.go
│ │ ├── list.go
│ │ ├── get.go
│ │ ├── create.go
│ │ ├── patch.go
│ │ ├── del.go
│ │ └── policy.go # 角色策略关联
│ └── middleware/ # 中间件
│ ├── auth.go # JWT 认证
│ ├── org.go # 组织上下文
@ -326,11 +328,13 @@ vbase/
### 使用方式
**独立运行:**
```bash
go run cli/main.go
```
**集成到其他项目:**
```go
import "github.com/veypi/vbase"
@ -338,6 +342,43 @@ import "github.com/veypi/vbase"
router.Extend("vb", vbase.Router) // 所有 vbase API 将通过 /vb/api/... 访问
```
### Vigo 路由规范
**父路由挂载子路由模块** - 使用 `Extend`
```go
// api/init.go
Router.Extend("/auth", auth.Router) // 将 auth.Router 挂载到 /api/auth
```
**同级创建子路由** - 使用 `SubRouter`
```go
// api/oauth/init.go
clientRouter := Router.SubRouter("/clients") // 创建 /oauth/clients 子路由
clientRouter.Get("/", "列表", listClients)
```
**跳过中间件** - 使用 `vigo.SkipBefore`
```go
// 公开端点跳过认证中间件
Router.Post("/login", "登录", vigo.SkipBefore, login)
```
### 上下文键名
中间件设置的上下文键名:
| 键名 | 类型 | 说明 |
| -------------- | -------------- | --------------------------------- |
| `user_id` | string | 当前用户 ID |
| `user_name` | string | 当前用户名 |
| `user_orgs` | []jwt.OrgClaim | 用户所属组织列表 |
| `token_claims` | \*jwt.Claims | 完整的 JWT 声明 |
| `org_id` | string | 当前组织 ID如果请求指定了组织 |
| `org_roles` | string | 当前组织角色 ID 列表 |
---
## 4. 核心模块设计
@ -401,6 +442,7 @@ VBase 采用三级权限体系:
#### 权限模型: RBAC + ABAC
**策略模型:**
```go
type Policy struct {
Resource string // 资源: user/org/resource/*
@ -461,7 +503,7 @@ type Policy struct {
VBase 中的 "Org" 概念灵活适配不同场景:
#### 场景1: 个人项目 (2C)
#### 场景 1: 个人项目 (2C)
```
个人开发者
@ -473,7 +515,7 @@ VBase 中的 "Org" 概念灵活适配不同场景:
- 资源完全隔离
- 可以邀请其他用户协作
#### 场景2: 团队协作 (2B)
#### 场景 2: 团队协作 (2B)
```
创业公司团队
@ -486,7 +528,7 @@ VBase 中的 "Org" 概念灵活适配不同场景:
└── ...
```
#### 场景3: 企业多层级 (2B Enterprise)
#### 场景 3: 企业多层级 (2B Enterprise)
```
企业层级 (可选功能)
@ -503,7 +545,7 @@ VBase 中的 "Org" 概念灵活适配不同场景:
以下示例展示 VBase 作为产品交付的完整链路:
#### 示例1: 2C 产品公司(社交 App
#### 示例 1: 2C 产品公司(社交 App
```
VBase 提供商)
@ -529,11 +571,12 @@ VBase 中的 "Org" 概念灵活适配不同场景:
```
**关键点:**
- 社交App公司的用户数据完全在自己的 VBase 实例中
- 社交 App 公司的用户数据完全在自己的 VBase 实例中
- 你作为 VBase 提供商,接触不到终端用户数据
- 社交App公司按需购买 License 或订阅服务
- 社交 App 公司按需购买 License 或订阅服务
#### 示例2: 2B 产品公司SaaS 软件)
#### 示例 2: 2B 产品公司SaaS 软件)
```
VBase 提供商)
@ -557,11 +600,12 @@ VBase 中的 "Org" 概念灵活适配不同场景:
```
**关键点:**
- SaaS 公司用 VBase 做租户隔离(多租户架构)
- 你帮 SaaS 公司运维 VBase 实例,按实例数/用户量收费
- SaaS 公司的终端企业用户与你无直接关系
#### 示例3: 混合产品公司2C + 2B
#### 示例 3: 混合产品公司2C + 2B
```
VBase 提供商)
@ -584,6 +628,7 @@ VBase 中的 "Org" 概念灵活适配不同场景:
```
**关键点:**
- 同一客户的不同业务线可以用独立 VBase 实例
- 完全物理隔离,符合数据合规要求
- 你提供统一的管理控制台给客户技术团队
@ -602,7 +647,7 @@ VBase 中的 "Org" 概念灵活适配不同场景:
#### 资源类型
| 资源类型 | 说明 | 典型操作 |
|----------|------|----------|
| ------------ | --------------- | ------------------------------ |
| **Database** | 数据库表、集合 | 创建表、查询、修改结构、删除表 |
| **Storage** | 文件存储 | 上传、下载、删除、管理存储桶 |
| **Function** | 边缘函数/云函数 | 部署、调用、更新、删除 |
@ -646,7 +691,7 @@ func AccessResource(userID, orgID, resourceType, action string) error {
#### 支持的授权流程
| 流程 | 说明 | 适用场景 |
|------|------|----------|
| ------------------ | ---------- | -------------------- |
| Authorization Code | 授权码模式 | 服务端应用,最安全 |
| Implicit | 简化模式 | SPA/移动端(不推荐) |
| Client Credentials | 客户端凭证 | 服务间调用 |
@ -657,7 +702,7 @@ func AccessResource(userID, orgID, resourceType, action string) error {
- **PKCE**: 防止授权码拦截攻击
- **State**: CSRF 防护
- **Redirect URI 白名单**: 严格匹配
- **Token 过期**: Access Token 1小时Refresh Token 30天
- **Token 过期**: Access Token 1 小时Refresh Token 30
---
@ -736,22 +781,26 @@ func AccessResource(userID, orgID, resourceType, action string) error {
### 5.2 关键模型说明
**User (用户)**
- 平台级唯一用户账号
- 可同时参与多个项目
- 支持第三方账号绑定
**Org (项目/组织)**
- 资源管理的基本单元
- 对2C场景: 一个项目 = 一个 Org
- 对2B场景: 可以是项目或企业部门
- 对 2C 场景: 一个项目 = 一个 Org
- 对 2B 场景: 可以是项目或企业部门
- 支持层级结构(可选)
**OrgMember (项目成员)**
- 维护用户与项目的关联
- 记录角色分配
- 支持多种成员状态(待审核、正常、禁用)
**Role (角色)**
- 项目内定义的权限集合
- 系统预置角色: owner, admin, developer, viewer
- 支持自定义角色
@ -769,6 +818,7 @@ func AccessResource(userID, orgID, resourceType, action string) error {
### 6.2 请求/响应规范
**成功响应:**
```json
{
"code": 200,
@ -778,6 +828,7 @@ func AccessResource(userID, orgID, resourceType, action string) error {
```
**错误响应:**
```json
{
"code": 400,

@ -0,0 +1,157 @@
# VBase 集成指南
## 1. 引入路由
```go
import "github.com/veypi/vbase/api"
func main() {
// 挂载 vbase 路由到 /api/v1/vb
rootRouter.Extend("/api/v1/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. 配置策略
创建组织时自动初始化默认策略:
```go
import "github.com/veypi/vbase/api/middleware"
// 创建组织后调用
middleware.InitOrgPolicies(orgID)
```
默认创建的策略:
| 策略 | 资源 | 操作 | 条件 | 说明 |
|------|------|------|------|------|
| policy:manage | policy | * | admin | 管理策略 |
| role:manage | role | * | admin | 管理角色 |
| user:update | user | update | owner | 只能改自己 |
自定义策略:
```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)
```
## 4. 使用鉴权
### 4.1 全局中间件(已内置)
```go
// api/init.go 已自动配置:
Router.Use(middleware.AuthRequired()) // JWT 认证
Router.Use(middleware.OrgContext()) // 组织上下文
```
### 4.2 公开接口(跳过认证)
```go
Router.Get("/public", vigo.SkipBefore, "公开接口", handler)
```
### 4.3 接口级权限控制
```go
import "github.com/veypi/vbase/api/middleware"
// 需要管理员权限
Router.Post("/users", middleware.RequireAdmin(), "创建用户", createUser)
// 基于 Policy 的细粒度控制
Router.Post("/projects", middleware.Permission("project", "create"), "创建项目", createProject)
// 带所有者检查(用户只能改自己的数据)
Router.Patch("/users/{id}", middleware.PermissionWithOwner("user", "update", "owner_id"), "更新用户", updateUser)
// 管理员或所有者
Router.Delete("/projects/{id}", middleware.AdminOrOwner("owner_id"), "删除项目", deleteProject)
```
### 4.4 代码中手动检查
```go
func myHandler(x *vigo.X, req *Req) error {
checker := middleware.NewChecker(x)
// 检查是否为管理员
if !checker.IsOrgAdmin() {
return vigo.ErrForbidden
}
// 检查具体权限
if err := checker.RequirePermission("resource", "write"); err != nil {
return err
}
return nil
}
```
## 5. 完整示例
```go
package main
import (
"github.com/veypi/vbase/api"
"github.com/veypi/vbase/api/middleware"
"github.com/veypi/vigo"
)
func main() {
r := vigo.NewRouter()
// 1. 挂载 vbase
r.Extend("/api/vb", api.Router)
// 2. 业务路由加权限
project := r.SubRouter("/projects")
project.Use(middleware.AuthRequired())
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)
vigo.Run(r)
}
```

@ -387,23 +387,23 @@ policy := &model.Policy{
Scope: "project", // 项目级策略
OrgID: "project-123", // 所属项目
}
model.DB.Create(policy)
cfg.DB().Create(policy)
// 将策略添加到角色
role := &model.Role{
role := &models.Role{
OrgID: "project-123",
Name: "DBAdmin",
PolicyIDs: "custom:db:admin",
}
model.DB.Create(role)
cfg.DB().Create(role)
// 分配角色给用户
member := &model.OrgMember{
member := &models.OrgMember{
OrgID: "project-123",
UserID: "user-456",
RoleIDs: role.ID,
}
model.DB.Create(member)
cfg.DB().Create(member)
```
---
@ -984,7 +984,7 @@ func ListResources(userID, orgID string) ([]Resource, error) {
// 查询该项目的资源(自动隔离)
var resources []Resource
err = model.DB.Where("org_id = ?", orgID).Find(&resources).Error
err = cfg.DB().Where("org_id = ?", orgID).Find(&resources).Error
return resources, err
}
```

Loading…
Cancel
Save