diff --git a/api/init.go b/api/init.go index bf85cfd..d56d14f 100644 --- a/api/init.go +++ b/api/init.go @@ -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 { diff --git a/api/middleware/init.go b/api/middleware/init.go deleted file mode 100644 index 470f531..0000000 --- a/api/middleware/init.go +++ /dev/null @@ -1,162 +0,0 @@ -// -// Copyright (C) 2024 veypi -// 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, - }, - } -} diff --git a/api/middleware/policy.go b/api/middleware/policy.go deleted file mode 100644 index 84ced3c..0000000 --- a/api/middleware/policy.go +++ /dev/null @@ -1,224 +0,0 @@ -// -// Copyright (C) 2024 veypi -// 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") - } -} diff --git a/api/policy/create.go b/api/policy/create.go deleted file mode 100644 index 49ad29f..0000000 --- a/api/policy/create.go +++ /dev/null @@ -1,57 +0,0 @@ -// -// Copyright (C) 2024 veypi -// 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 -} diff --git a/api/policy/del.go b/api/policy/del.go deleted file mode 100644 index 8abca72..0000000 --- a/api/policy/del.go +++ /dev/null @@ -1,35 +0,0 @@ -// -// Copyright (C) 2024 veypi -// 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 -} diff --git a/api/policy/get.go b/api/policy/get.go deleted file mode 100644 index 3d090c6..0000000 --- a/api/policy/get.go +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright (C) 2024 veypi -// 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 -} diff --git a/api/policy/init.go b/api/policy/init.go deleted file mode 100644 index 24b4151..0000000 --- a/api/policy/init.go +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright (C) 2024 veypi -// 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) -} diff --git a/api/policy/list.go b/api/policy/list.go deleted file mode 100644 index 8efa1fb..0000000 --- a/api/policy/list.go +++ /dev/null @@ -1,63 +0,0 @@ -// -// Copyright (C) 2024 veypi -// 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 -} diff --git a/api/policy/patch.go b/api/policy/patch.go deleted file mode 100644 index 4bbc35e..0000000 --- a/api/policy/patch.go +++ /dev/null @@ -1,53 +0,0 @@ -// -// Copyright (C) 2024 veypi -// 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 -} diff --git a/api/role/create.go b/api/role/create.go deleted file mode 100644 index bbf73ce..0000000 --- a/api/role/create.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (C) 2024 veypi -// 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 -} diff --git a/api/role/del.go b/api/role/del.go deleted file mode 100644 index 44fd77f..0000000 --- a/api/role/del.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (C) 2024 veypi -// 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 -} diff --git a/api/role/get.go b/api/role/get.go deleted file mode 100644 index 8a92b2d..0000000 --- a/api/role/get.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (C) 2024 veypi -// 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 -} diff --git a/api/role/init.go b/api/role/init.go deleted file mode 100644 index 94fc2f8..0000000 --- a/api/role/init.go +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (C) 2024 veypi -// 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) -} diff --git a/api/role/list.go b/api/role/list.go deleted file mode 100644 index 9474493..0000000 --- a/api/role/list.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (C) 2024 veypi -// 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 -} diff --git a/api/role/patch.go b/api/role/patch.go deleted file mode 100644 index d437c1c..0000000 --- a/api/role/patch.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (C) 2024 veypi -// 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 -} diff --git a/api/role/policy.go b/api/role/policy.go deleted file mode 100644 index 06fd962..0000000 --- a/api/role/policy.go +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (C) 2024 veypi -// 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 -} diff --git a/api/user/init.go b/api/user/init.go index 695e0c5..36329b9 100644 --- a/api/user/init.go +++ b/api/user/init.go @@ -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) diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000..6af82a8 --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,610 @@ +// +// Copyright (C) 2024 veypi +// 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 "" +} diff --git a/cli/main.go b/cli/main.go index 9327288..564adc8 100644 --- a/cli/main.go +++ b/cli/main.go @@ -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 diff --git a/doc/developer_guide.md b/doc/developer_guide.md new file mode 100644 index 0000000..5052dcd --- /dev/null +++ b/doc/developer_guide.md @@ -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 +// 模块1:CRM +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"}}, + }, +}) + +// 模块2:ERP(资源名可以重复,通过 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 // 允许的操作 +} +``` + +--- + +**确认此设计后,我将实现底层代码。** diff --git a/doc/integration.md b/doc/integration.md index 339e53e..3a11732 100644 --- a/doc/integration.md +++ b/doc/integration.md @@ -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() +``` + +无需重启服务,权限配置实时生效。 diff --git a/doc/permission_design.md b/doc/permission_design.md new file mode 100644 index 0000000..6f6411f --- /dev/null +++ b/doc/permission_design.md @@ -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 | 来源ID(Role 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 即可)| +| 角色数量 | 多(每项目配角色) | 少(复用系统角色 + 数据范围)| + +--- + +请确认此设计后,我开始修改代码。 diff --git a/init.go b/init.go index 446f968..aea4bfe 100644 --- a/init.go +++ b/init.go @@ -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) } diff --git a/models/auth.go b/models/auth.go new file mode 100644 index 0000000..79119b5 --- /dev/null +++ b/models/auth.go @@ -0,0 +1,146 @@ +// +// Copyright (C) 2024 veypi +// 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:"允许的操作"` +} diff --git a/models/init.go b/models/init.go index e8a4b82..53a85c1 100644 --- a/models/init.go +++ b/models/init.go @@ -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{}) diff --git a/models/policy.go b/models/policy.go deleted file mode 100644 index f936154..0000000 --- a/models/policy.go +++ /dev/null @@ -1,68 +0,0 @@ -// -// Copyright (C) 2024 veypi -// 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" -)