refactor(auth): Add permission validation and update design docs

- Add validatePermission function to check depth/level consistency
    - Validate permission codes in Perm, Grant, and Check methods
    - LevelCreate requires odd depth, other levels require even depth
    - Update design.md examples from org to app/role model
    - Add RoleID field to Permission struct documentation
master
veypi 3 weeks ago
parent 438a84d9fc
commit b00e36ca80

@ -226,6 +226,10 @@ func (a *appAuth) Perm(code string, level int) PermFunc {
} }
// 检查权限 // 检查权限
if err := validatePermission(permID, level); err != nil {
panic(err)
}
if !a.Check(x.Context(), userID, permID, level) { if !a.Check(x.Context(), userID, permID, level) {
return vigo.ErrNoPermission.WithString(fmt.Sprintf("requires permission: %s (level %d)", permID, level)) return vigo.ErrNoPermission.WithString(fmt.Sprintf("requires permission: %s (level %d)", permID, level))
} }
@ -234,13 +238,39 @@ func (a *appAuth) Perm(code string, level int) PermFunc {
} }
} }
func (a *appAuth) PermCreate(code string) PermFunc { return a.Perm(code, LevelCreate) } func (a *appAuth) PermCreate(code string) PermFunc {
func (a *appAuth) PermRead(code string) PermFunc { return a.Perm(code, LevelRead) } if err := validatePermission(code, LevelCreate); err != nil {
func (a *appAuth) PermWrite(code string) PermFunc { return a.Perm(code, LevelWrite) } panic(err)
func (a *appAuth) PermAdmin(code string) PermFunc { return a.Perm(code, LevelAdmin) } }
return a.Perm(code, LevelCreate)
}
func (a *appAuth) PermRead(code string) PermFunc {
if err := validatePermission(code, LevelRead); err != nil {
panic(err)
}
return a.Perm(code, LevelRead)
}
func (a *appAuth) PermWrite(code string) PermFunc {
if err := validatePermission(code, LevelWrite); err != nil {
panic(err)
}
return a.Perm(code, LevelWrite)
}
func (a *appAuth) PermAdmin(code string) PermFunc {
if err := validatePermission(code, LevelAdmin); err != nil {
panic(err)
}
return a.Perm(code, LevelAdmin)
}
// Grant 授予权限 // Grant 授予权限
func (a *appAuth) Grant(ctx context.Context, userID, permissionID string, level int) error { func (a *appAuth) Grant(ctx context.Context, userID, permissionID string, level int) error {
if err := validatePermission(permissionID, level); err != nil {
return err
}
// 检查是否存在 // 检查是否存在
var count int64 var count int64
cfg.DB().Model(&models.Permission{}). cfg.DB().Model(&models.Permission{}).
@ -305,6 +335,9 @@ func (a *appAuth) RevokeRole(ctx context.Context, userID, roleCode string) error
// Check 检查权限 // Check 检查权限
func (a *appAuth) Check(ctx context.Context, userID, permissionID string, level int) bool { func (a *appAuth) Check(ctx context.Context, userID, permissionID string, level int) bool {
if err := validatePermission(permissionID, level); err != nil {
panic(err)
}
// 1. 获取用户在该作用域下的所有权限(直接权限 + 角色权限) // 1. 获取用户在该作用域下的所有权限(直接权限 + 角色权限)
perms, err := a.getUserPermissions(userID) perms, err := a.getUserPermissions(userID)
if err != nil { if err != nil {
@ -357,11 +390,18 @@ func (a *appAuth) ListUsers(ctx context.Context, permissionID string) (map[strin
var perms []models.Permission var perms []models.Permission
db := cfg.DB().Where("scope = ?", a.scope) db := cfg.DB().Where("scope = ?", a.scope)
query := db.Where("permission_id = ?", permissionID) conditions := []string{"permission_id = ?"}
args := []interface{}{permissionID}
if len(parents) > 0 { if len(parents) > 0 {
query = query.Or("permission_id IN ? AND level = ?", parents, LevelAdmin) conditions = append(conditions, "(permission_id IN ? AND level = ?)")
args = append(args, parents, LevelAdmin)
} }
query = query.Or("permission_id = ? AND level = ?", "*", LevelAdmin)
conditions = append(conditions, "(permission_id = ? AND level = ?)")
args = append(args, "*", LevelAdmin)
query := db.Where(strings.Join(conditions, " OR "), args...)
if err := query.Find(&perms).Error; err != nil { if err := query.Find(&perms).Error; err != nil {
return nil, err return nil, err
@ -575,3 +615,27 @@ func extractToken(x *vigo.X) string {
} }
return x.Request.URL.Query().Get("access_token") return x.Request.URL.Query().Get("access_token")
} }
func validatePermission(code string, level int) error {
if code == "*" {
if level != LevelAdmin {
return fmt.Errorf("wildcard * requires LevelAdmin")
}
return nil
}
parts := strings.Split(code, ":")
depth := len(parts)
if level == LevelCreate {
if depth%2 == 0 {
return fmt.Errorf("LevelCreate requires odd depth (resource type), got %d for %s", depth, code)
}
} else {
// Level 2, 4, 6, 7
if depth%2 != 0 {
return fmt.Errorf("Level %d requires even depth (resource instance), got %d for %s", level, depth, code)
}
}
return nil
}

@ -15,10 +15,10 @@
### 1.2 示例 ### 1.2 示例
``` ```
org → 第1层 (奇数) - 资源类型 app → 第1层 (奇数) - 资源类型
org:orgA → 第2层 (偶数) - 实例 app:vbase → 第2层 (偶数) - 实例
org:orgA:project → 第3层 (奇数) - 资源类型 app:vbase:role → 第3层 (奇数) - 资源类型
org:orgA:project:projB → 第4层 (偶数) - 实例 app:vbase:role:admin → 第4层 (偶数) - 实例
``` ```
--- ---
@ -61,59 +61,48 @@ org:orgA:project:projB → 第4层 (偶数) - 实例
``` ```
创建资源 (level 1) 创建资源 (level 1)
→ 检查当前 permissionID 对应的奇数层 → 检查当前 permissionID 对应的奇数层
→ 例: "org:{orgID}:project" 检查 "org:{orgID}:project" 层 → 例: "app:{appID}:role" 检查 "app:{appID}:role" 层
读取/更新/删除资源 (level 2,4,6,7) 读取/更新/删除资源 (level 2,4,6,7)
→ 检查当前 permissionID 对应的偶数层 → 检查当前 permissionID 对应的偶数层
→ 如无权限,递归向上检查父实例层 → 如无权限,递归向上检查父实例层
→ 注意:只有 Level 7 (管理员) 权限才会向下继承Level 2,4,6 不会继承 → 注意:只有 Level 7 (管理员) 权限才会向下继承Level 2,4,6 不会继承
→ 例: "org:{orgID}:project:{projectID}" 先检查实例层,再检查 "org:{orgID}" → 例: "app:{appID}:role:{roleID}" 先检查实例层,再检查 "app:{appID}"
``` ```
--- ---
## 四、权限流程示例 ## 四、权限流程示例
### 场景一:用户 A 创建组织 ### 场景一:用户 A 创建应用
``` ```
1. 用户A创建组织 "公司A" 1. 用户A创建应用 "VBase"
2. 自动创建权限: 2. 自动创建权限:
- PermissionID: "org:org_companyA" - PermissionID: "app:vbase"
- Level: 7 (创建者完全控制) - Level: 7 (创建者完全控制)
``` ```
### 场景二:用户 A 邀请用户 B 加入组织 ### 场景二:用户 A 邀请用户 B 加入应用
``` ```
1. 用户A授予用户B: org:org_companyA level 2 (读) 1. 用户A授予用户B: app:vbase level 2 (读)
2. 用户B权限表: 2. 用户B权限表:
- org:org_companyA level 2 - app:vbase level 2
3. 用户B可执行: 3. 用户B可执行:
- ✓ 读取 org_companyA - ✓ 读取 vbase 信息
- ✗ 修改/删除 - ✗ 修改/删除
``` ```
### 场景三:用户 B 创建项目 ### 场景三:用户 B 创建角色
``` ```
前置: 用户B有 org:org_companyA level 2 (读),需要额外授权 前置: 用户B有 app:vbase level 2 (读),需要额外授权
1. 用户A授予用户B: org:org_companyA:project level 1 (创建项目) 1. 用户A授予用户B: app:vbase:role level 1 (创建角色)
2. 用户B创建项目 "项目X" 2. 用户B创建角色 "Editor"
3. 自动创建权限: 3. 自动创建权限:
- PermissionID: "org:org_companyA:project:project_X" - PermissionID: "app:vbase:role:editor"
- Level: 7
```
### 场景四:用户 C 加入项目并创建文档
```
1. 用户B授予用户C: org:org_companyA:project:project_X level 2 (读)
2. 用户C需要额外授权才能创建文档
3. 用户C创建文档 "文档Y"
4. 自动创建权限:
- PermissionID: "org:org_companyA:project:project_X:doc:doc_Y"
- Level: 7 - Level: 7
``` ```
@ -159,11 +148,11 @@ type Auth interface {
// Perm 检查权限 // Perm 检查权限
// code: 权限码,支持动态解析 // code: 权限码,支持动态解析
// - 固定写法: "org:orgA" // - 固定写法: "app:vbase"
// - 动态解析: "org:{orgID}" 从 path 获取 // - 动态解析: "app:{appID}" 从 path 获取
// "org:{orgID@query}" 从 query 获取 // "app:{appID@query}" 从 query 获取
// "org:{orgID@header}" 从 header 获取 // "app:{appID@header}" 从 header 获取
// "org:{orgID@ctx}" 从 ctx 获取 // "app:{appID@ctx}" 从 ctx 获取
// level: 需要的权限等级 // level: 需要的权限等级
Perm(code string, level int) PermFunc Perm(code string, level int) PermFunc
@ -185,7 +174,7 @@ type Auth interface {
// Grant 授予权限 // Grant 授予权限
// 在创建资源、被授权等业务逻辑中调用 // 在创建资源、被授权等业务逻辑中调用
// permissionID: 权限码,如 "org:orgA" // permissionID: 权限码,如 "app:vbase"
// level: 权限等级 // level: 权限等级
Grant(ctx context.Context, userID, permissionID string, level int) error Grant(ctx context.Context, userID, permissionID string, level int) error
@ -195,21 +184,21 @@ type Auth interface {
// ========== 权限查询 ========== // ========== 权限查询 ==========
// Check 检查权限 不支持动态解析 // Check 检查权限 不支持动态解析
// permissionID: 完整的权限码,如 "org:orgA" // permissionID: 完整的权限码,如 "app:vbase"
Check(ctx context.Context, userID, permissionID string, level int) bool Check(ctx context.Context, userID, permissionID string, level int) bool
// ========== 资源列表查询 ========== // ========== 资源列表查询 ==========
// ListResources 查询用户在特定资源类型下的详细权限信息 // ListResources 查询用户在特定资源类型下的详细权限信息
// 用于解决 "查询我有权限的 org 列表" 等场景 // 用于解决 "查询我有权限的 app 列表" 等场景
// userID: 用户ID // userID: 用户ID
// resourceType: 资源类型 (奇数层),如 "org" 或 "org:{orgID}:project" // resourceType: 资源类型 (奇数层),如 "app" 或 "app:{appID}:role"
// 返回: map[实例ID]权限等级 (如 {"orgA": 2, "orgB": 7}) // 返回: map[实例ID]权限等级 (如 {"vbase": 2, "other": 7})
ListResources(ctx context.Context, userID, resourceType string) (map[string]int, error) ListResources(ctx context.Context, userID, resourceType string) (map[string]int, error)
// ListUsers 查询特定资源的所有协作者及其权限 // ListUsers 查询特定资源的所有协作者及其权限
// 用于解决 "查看这个项目有哪些成员" 等场景 // 用于解决 "查看这个应用有哪些成员" 等场景
// permissionID: 资源实例权限码,如 "org:orgA" // permissionID: 资源实例权限码,如 "app:vbase"
// 返回: map[用户ID]权限等级 (如 {"user1": 2, "user2": 7}) // 返回: map[用户ID]权限等级 (如 {"user1": 2, "user2": 7})
ListUsers(ctx context.Context, permissionID string) (map[string]int, error) ListUsers(ctx context.Context, permissionID string) (map[string]int, error)
} }
@ -220,6 +209,7 @@ type Auth interface {
type Permission struct { type Permission struct {
ID string `json:"id"` ID string `json:"id"`
UserID string `json:"user_id"` UserID string `json:"user_id"`
RoleID string `json:"role_id"`
PermissionID string `json:"permission_id"` PermissionID string `json:"permission_id"`
Level int `json:"level"` Level int `json:"level"`
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
@ -237,8 +227,8 @@ type Permission struct {
var Router = vigo.NewRouter() var Router = vigo.NewRouter()
func init() { func init() {
// 创建组织 - 需要系统级 org 权限 // 创建应用 - 需要系统级 app 权限
Router.Post("/orgs", cfg.Auth.PermCreate("org"), CreateOrg) Router.Post("/apps", cfg.Auth.PermCreate("app"), CreateApp)
// 超级管理员接口 // 超级管理员接口
Router.Get("/admin/users", cfg.Auth.PermAdmin("*"), AdminListUsers) Router.Get("/admin/users", cfg.Auth.PermAdmin("*"), AdminListUsers)
@ -249,19 +239,19 @@ func init() {
```go ```go
func init() { func init() {
// 从路径参数获取 orgID (默认) // 从路径参数获取 appID (默认)
// GET /orgs/{orgID} // GET /apps/{appID}
Router.Get("/orgs/{orgID}", cfg.Auth.PermRead("org:{orgID}"), GetOrg) Router.Get("/apps/{appID}", cfg.Auth.PermRead("app:{appID}"), GetApp)
// 从 query 参数获取 // 从 query 参数获取
// GET /orgs?orgID=xxx // GET /apps?appID=xxx
Router.Get("/orgs", cfg.Auth.PermRead("org:{orgID@query}"), GetOrg) Router.Get("/apps", cfg.Auth.PermRead("app:{appID@query}"), GetApp)
// 多层嵌套 // 多层嵌套
// GET /orgs/{orgID}/projects/{projectID} // GET /apps/{appID}/roles/{roleID}
Router.Get("/orgs/{orgID}/projects/{projectID}", Router.Get("/apps/{appID}/roles/{roleID}",
cfg.Auth.PermRead("org:{orgID}:project:{projectID}"), cfg.Auth.PermRead("app:{appID}:role:{roleID}"),
GetProject, GetRole,
) )
} }
``` ```
@ -272,26 +262,22 @@ func init() {
var Router = vigo.NewRouter().Use(cfg.Auth.Login()) var Router = vigo.NewRouter().Use(cfg.Auth.Login())
func init() { func init() {
// 创建组织 - 系统级权限 // 创建应用 - 系统级权限
Router.Post("/orgs", cfg.Auth.PermCreate("org"), CreateOrg) Router.Post("/apps", cfg.Auth.PermCreate("app"), CreateApp)
// 列出我的组织 - 只需登录 // 列出我的应用 - 只需登录
Router.Get("/orgs", ListMyOrgs) Router.Get("/apps", ListMyApps)
// 组织操作 - 从路径获取 // 应用操作 - 从路径获取
Router.Get("/orgs/{orgID}", cfg.Auth.PermRead("org:{orgID}"), GetOrg) Router.Get("/apps/{appID}", cfg.Auth.PermRead("app:{appID}"), GetApp)
Router.Put("/orgs/{orgID}", cfg.Auth.PermWrite("org:{orgID}"), UpdateOrg) Router.Put("/apps/{appID}", cfg.Auth.PermWrite("app:{appID}"), UpdateApp)
Router.Delete("/orgs/{orgID}", cfg.Auth.PermAdmin("org:{orgID}"), DeleteOrg) Router.Delete("/apps/{appID}", cfg.Auth.PermAdmin("app:{appID}"), DeleteApp)
// 项目操作 - 嵌套资源 // 角色操作 - 嵌套资源
Router.Post("/orgs/{orgID}/projects", cfg.Auth.PermCreate("org:{orgID}:project"), CreateProject) Router.Post("/apps/{appID}/roles", cfg.Auth.PermCreate("app:{appID}:role"), CreateRole)
Router.Get("/orgs/{orgID}/projects/{projectID}", cfg.Auth.PermRead("org:{orgID}:project:{projectID}"), GetProject) Router.Get("/apps/{appID}/roles/{roleID}", cfg.Auth.PermRead("app:{appID}:role:{roleID}"), GetRole)
Router.Put("/orgs/{orgID}/projects/{projectID}", cfg.Auth.PermWrite("org:{orgID}:project:{projectID}"), UpdateProject) Router.Put("/apps/{appID}/roles/{roleID}", cfg.Auth.PermWrite("app:{appID}:role:{roleID}"), UpdateRole)
Router.Delete("/orgs/{orgID}/projects/{projectID}", cfg.Auth.PermAdmin("org:{orgID}:project:{projectID}"), DeleteProject) Router.Delete("/apps/{appID}/roles/{roleID}", cfg.Auth.PermAdmin("app:{appID}:role:{roleID}"), DeleteRole)
// 文档操作
Router.Post("/orgs/{orgID}/projects/{projectID}/docs", cfg.Auth.PermCreate("org:{orgID}:project:{projectID}:doc"), CreateDoc)
Router.Get("/orgs/{orgID}/projects/{projectID}/docs/{docID}", cfg.Auth.PermRead("org:{orgID}:project:{projectID}:doc:{docID}"), GetDoc)
} }
``` ```
@ -299,10 +285,10 @@ func init() {
| 语法 | 来源 | 示例 | | 语法 | 来源 | 示例 |
|------|------|------| |------|------|------|
| `{key}` | path 参数 | `{orgID}` | | `{key}` | path 参数 | `{appID}` |
| `{key@query}` | query 参数 | `{orgID@query}` | | `{key@query}` | query 参数 | `{appID@query}` |
| `{key@header}` | header | `{orgID@header}` | | `{key@header}` | header | `{appID@header}` |
| `{key@ctx}` | context | `{orgID@ctx}` | | `{key@ctx}` | context | `{appID@ctx}` |
--- ---
@ -319,15 +305,15 @@ func init() {
### 7.2 Grant 调用时机 ### 7.2 Grant 调用时机
```go ```go
// 创建组织 // 创建应用
func CreateOrg(x *vigo.X, req *CreateOrgReq) (*OrgResp, error) { func CreateApp(x *vigo.X, req *CreateAppReq) (*AppResp, error) {
org := models.Org{Name: req.Name} app := models.App{Name: req.Name}
db.Create(&org) db.Create(&app)
// 授予创建者完全控制权限 // 授予创建者完全控制权限
cfg.Auth.Grant(x.Context(), userID, "org:"+org.ID, auth.LevelAdmin) cfg.Auth.Grant(x.Context(), userID, "app:"+app.ID, auth.LevelAdmin)
return &OrgResp{Org: org}, nil return &AppResp{App: app}, nil
} }
``` ```
@ -336,25 +322,20 @@ func CreateOrg(x *vigo.X, req *CreateOrgReq) (*OrgResp, error) {
对于资源列表List或搜索接口推荐以下设计模式 对于资源列表List或搜索接口推荐以下设计模式
1. **全量管理接口**(如后台管理系统): 1. **全量管理接口**(如后台管理系统):
- 使用 `PermAdmin("*")``PermAdmin("org:*")`。 - 使用 `PermAdmin("*")``PermAdmin("app:*")`。
- 这类接口返回所有数据,必须严格控制权限。 - 这类接口返回所有数据,必须严格控制权限。
2. **用户侧列表/搜索**(如“我的项目”): 2. **用户侧列表/搜索**(如“我的应用”):
- **方式一(仅所有者)** - **方式一(仅所有者)**
- 使用 `Login()` 确保登录。 - 使用 `Login()` 确保登录。
- 业务层:`db.Where("owner_id = ?", userID).Find(&orgs)` - 业务层:`db.Where("owner_id = ?", userID).Find(&apps)`
- **方式二(协作模式 - 使用 ListResources** - **方式二(协作模式 - 使用 ListResources**
- 调用 Auth 接口获取有权限的 ID 列表。 - 调用 Auth 接口获取有权限的 ID 列表。
- `perms, _ := auth.ListResources(ctx, userID, "org")` - `perms, _ := auth.ListResources(ctx, userID, "app")`
- `ids := keys(perms)` - `ids := keys(perms)`
- 业务层:`db.Where("id IN ?", ids).Find(&orgs)` - 业务层:`db.Where("id IN ?", ids).Find(&apps)`
- **方式三(混合模式)** - **方式三(混合模式)**
- 同时查询 owner_id 和 授权列表。 - 同时查询 owner_id 和 授权列表。
- `perms, _ := auth.ListResources(...)` - `perms, _ := auth.ListResources(...)`
- `ids := keys(perms)` - `ids := keys(perms)`
- `db.Where("owner_id = ? OR id IN ?", userID, ids).Find(&orgs)` - `db.Where("owner_id = ? OR id IN ?", userID, ids).Find(&apps)`
3. **`PermXXX` 适用场景**
- 针对 **特定资源实例** 的操作URL 中包含 ID`/projects/{id}`)。
- 针对 **共享资源** 的访问控制。
- 针对 **管理功能** 的鉴权。

Loading…
Cancel
Save