diff --git a/auth/auth.go b/auth/auth.go index 5d75d4f..784f148 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -32,10 +32,15 @@ type Auth interface { PermOnResource(permissionID, resourceKey string) func(*vigo.X) error // 满足任一权限 - PermAny(permissionIDs []string) func(*vigo.X) error + PermAny(permissionIDs ...string) func(*vigo.X) error // 满足所有权限 - PermAll(permissionIDs []string) func(*vigo.X) error + PermAll(permissionIDs ...string) func(*vigo.X) error + + // ========== 角色管理 ========== + // 添加角色定义 + // policies 格式: "resource:action",例如 "user:read", "*:*" + AddRole(roleCode, roleName string, policies ...string) error // ========== 权限管理 ========== // 授予角色 @@ -71,43 +76,46 @@ var Factory = &authFactory{ // VBaseAuth vbase 自身的权限管理实例 // 由 vbase 包在初始化时注入 -var ( - VBaseAuth = Factory.New("vb", models.AppConfig{ - Name: "VBase", - Description: "VBase 基础设施", - DefaultRoles: []models.RoleDefinition{ - {Code: "admin", Name: "管理员", Policies: []string{"*:*"}}, - {Code: "user", Name: "普通用户", Policies: []string{ - "user:read", "user:update", - "org:read", "org:create", - "oauth-client:read", "oauth-client:create", "oauth-client:update", "oauth-client:delete", - }}, - }, - }) -) +var VBaseAuth = Factory.New("vb") + +func init() { + // 为 VBaseAuth 添加默认角色 + VBaseAuth.AddRole("admin", "管理员", "*:*") + VBaseAuth.AddRole("user", "普通用户", + "user:read", + "user:update", + "org:read", + "org:create", + "oauth-client:read", + "oauth-client:create", + "oauth-client:update", + "oauth-client:delete", + ) +} type authFactory struct { apps map[string]*appAuth // appKey -> auth实例 } -// New 创建权限管理实例(注册应用) -func (f *authFactory) New(appKey string, config models.AppConfig) Auth { - if _, exists := f.apps[appKey]; exists { - return f.apps[appKey] - } - - // 验证默认角色中的权限格式 - for _, role := range config.DefaultRoles { - for _, policy := range role.Policies { - validatePermissionID(policy) - } +// New 创建权限管理实例(注册权限域) +func (f *authFactory) New(scope string) Auth { + if _, exists := f.apps[scope]; exists { + return f.apps[scope] } auth := &appAuth{ - appKey: appKey, - config: config, - } - f.apps[appKey] = auth + scope: scope, + roleDefs: make(map[string]roleDefinition), + policies: make(map[string][][2]string), + roleInitDone: make(map[string]bool), + } + // 设置权限域信息 + auth.roleDefs["_scope_info"] = roleDefinition{ + code: "_scope_info", + name: scope, + description: scope + " scope", + } + f.apps[scope] = auth return auth } @@ -145,10 +153,77 @@ func (f *authFactory) Init() error { return nil } -// appAuth 单个应用的权限管理 +// roleDefinition 角色定义(内部使用) +type roleDefinition struct { + code string + name string + description string + policies []string // 权限列表: ["resource:action", "*:*"] +} + +// appAuth 单个权限域的权限管理 type appAuth struct { - appKey string - config models.AppConfig + scope string // 权限域标识 + roleDefs map[string]roleDefinition // roleCode -> role definition + policies map[string][][2]string // roleCode -> list of [resource, action] pairs + roleInitDone map[string]bool // roleCode -> whether role is initialized in DB +} + +// AddRole 添加角色定义 +// policies 格式: "resource:action",例如 "user:read", "*:*" +func (a *appAuth) AddRole(roleCode, roleName string, policies ...string) error { + if roleCode == "" || roleName == "" { + return fmt.Errorf("role code and name cannot be empty") + } + if roleCode == "_scope_info" { + return fmt.Errorf("reserved role code: _scope_info") + } + + // 解析并验证权限格式 + parsedPolicies := make([][2]string, 0, len(policies)) + for _, policy := range policies { + // 严格检查格式: resource:action + parts := strings.Split(policy, ":") + if len(parts) != 2 { + return fmt.Errorf("invalid policy format: %s, expected 'resource:action'", policy) + } + resource, action := parts[0], parts[1] + + // 验证 resource 和 action 不为空 + if resource == "" || action == "" { + return fmt.Errorf("resource and action cannot be empty in policy: %s", policy) + } + + // 验证 resource 格式(如果不是通配符) + if resource != "*" { + if !validResourceRegex.MatchString(resource) { + return fmt.Errorf("invalid resource identifier: %s in policy: %s, must start with letter and contain only letters, numbers, '-' or '_'", resource, policy) + } + } + + // 验证 action 格式(如果不是通配符) + if action != "*" { + if !validResourceRegex.MatchString(action) { + return fmt.Errorf("invalid action identifier: %s in policy: %s, must start with letter and contain only letters, numbers, '-' or '_'", action, policy) + } + } + + parsedPolicies = append(parsedPolicies, [2]string{resource, action}) + } + + // 存储角色定义 + a.roleDefs[roleCode] = roleDefinition{ + code: roleCode, + name: roleName, + } + a.policies[roleCode] = parsedPolicies + + // 如果已经初始化过,立即同步到数据库 + if len(a.roleInitDone) > 0 { + return a.initRole(roleCode) + } + + return nil } // init 初始化应用的权限配置 @@ -166,9 +241,12 @@ func (a *appAuth) init() error { } } - // 2. 创建系统预设角色 - for _, roleDef := range a.config.DefaultRoles { - if err := a.initRole(roleDef); err != nil { + // 2. 创建系统预设角色(跳过 _app_info) + for roleCode := range a.roleDefs { + if roleCode == "_app_info" { + continue + } + if err := a.initRole(roleCode); err != nil { return err } } @@ -180,24 +258,25 @@ func (a *appAuth) init() error { 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 { + for roleCode, policies := range a.policies { + if roleCode == "_app_info" { + continue + } + for _, policy := range policies { + resource, action := policy[0], policy[1] + // 跳过通配符权限的特殊处理 + if resource == "*" && action == "*" { continue } - - resource, action := parts[0], parts[1] - permID := fmt.Sprintf("%s:%s:%s", a.appKey, resource, action) + permID := fmt.Sprintf("%s:%s:%s", a.scope, resource, action) if _, exists := permMap[permID]; !exists { permMap[permID] = models.Permission{ ID: permID, - AppKey: a.appKey, + Scope: a.scope, Resource: resource, Action: action, - Description: fmt.Sprintf("%s %s on %s", a.config.Name, action, resource), + Description: fmt.Sprintf("%s %s on %s", a.scope, action, resource), } } } @@ -211,34 +290,44 @@ func (a *appAuth) extractPermissions() []models.Permission { } // initRole 初始化系统预设角色 -func (a *appAuth) initRole(roleDef models.RoleDefinition) error { +func (a *appAuth) initRole(roleCode string) error { + roleDef, exists := a.roleDefs[roleCode] + if !exists { + return fmt.Errorf("role not found: %s", roleCode) + } + policies, hasPolicies := a.policies[roleCode] + if !hasPolicies { + policies = [][2]string{} + } + // 查找或创建系统角色 var role models.Role - err := cfg.DB().Where("code = ? AND org_id IS NULL", roleDef.Code).First(&role).Error + err := cfg.DB().Where("code = ? AND org_id IS NULL", roleDef.code).First(&role).Error if err != nil { // 创建新角色 role = models.Role{ OrgID: nil, - Code: roleDef.Code, - Name: roleDef.Name, - Description: roleDef.Description, + 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) + 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 { + hasWildcard := false + for _, policy := range policies { + resource, action := policy[0], policy[1] + // 处理通配符权限 + if resource == "*" && action == "*" { + hasWildcard = true continue } - - resource, action := parts[0], parts[1] - permID := fmt.Sprintf("%s:%s:%s", a.appKey, resource, action) + permID := fmt.Sprintf("%s:%s:%s", a.scope, resource, action) // 检查关联是否存在 var count int64 @@ -258,6 +347,26 @@ func (a *appAuth) initRole(roleDef models.RoleDefinition) error { } } + // 为通配符权限创建特殊记录 + if hasWildcard { + wildcardPermID := fmt.Sprintf("%s:*:*", a.scope) + var count int64 + cfg.DB().Model(&models.RolePermission{}). + Where("role_id = ? AND permission_id = ?", role.ID, wildcardPermID). + Count(&count) + if count == 0 { + rp := models.RolePermission{ + RoleID: role.ID, + PermissionID: wildcardPermID, + Condition: "none", + } + if err := cfg.DB().Create(&rp).Error; err != nil { + return fmt.Errorf("failed to create wildcard role permission: %w", err) + } + } + } + + a.roleInitDone[roleCode] = true return nil } @@ -366,7 +475,7 @@ func (a *appAuth) checkPermission(ctx context.Context, userID, orgID, permission return nil } -func (a *appAuth) PermAny(permissionIDs []string) func(*vigo.X) error { +func (a *appAuth) PermAny(permissionIDs ...string) func(*vigo.X) error { for _, pid := range permissionIDs { validatePermissionID(pid) } @@ -396,7 +505,7 @@ func (a *appAuth) PermAny(permissionIDs []string) func(*vigo.X) error { } } -func (a *appAuth) PermAll(permissionIDs []string) func(*vigo.X) error { +func (a *appAuth) PermAll(permissionIDs ...string) func(*vigo.X) error { for _, pid := range permissionIDs { validatePermissionID(pid) } @@ -511,7 +620,7 @@ func (a *appAuth) RevokeRole(ctx context.Context, userID, orgID, roleCode string func (a *appAuth) GrantResourcePerm(ctx context.Context, userID, orgID, permissionID, resourceID string) error { if strings.Count(permissionID, ":") == 1 { - permissionID = fmt.Sprintf("%s:%s", a.appKey, permissionID) + permissionID = fmt.Sprintf("%s:%s", a.scope, permissionID) } // 检查权限是否存在 var perm models.Permission @@ -557,7 +666,7 @@ func (a *appAuth) GrantResourcePerm(ctx context.Context, userID, orgID, permissi func (a *appAuth) RevokeResourcePerm(ctx context.Context, userID, orgID, permissionID, resourceID string) error { if strings.Count(permissionID, ":") == 1 { - permissionID = fmt.Sprintf("%s:%s", a.appKey, permissionID) + permissionID = fmt.Sprintf("%s:%s", a.scope, permissionID) } query := cfg.DB().Where("user_id = ? AND permission_id = ? AND resource_id = ?", userID, permissionID, resourceID) @@ -604,7 +713,7 @@ func (a *appAuth) RevokeAll(ctx context.Context, userID, orgID string) error { func (a *appAuth) CheckPermission(ctx context.Context, userID, orgID, permissionID, resourceID string) (bool, error) { if strings.Count(permissionID, ":") == 1 { - permissionID = fmt.Sprintf("%s:%s", a.appKey, permissionID) + permissionID = fmt.Sprintf("%s:%s", a.scope, permissionID) } // Check cache @@ -659,6 +768,12 @@ func (a *appAuth) checkPermissionDB(ctx context.Context, userID, orgID, permissi permsToCheck = append(permsToCheck, fmt.Sprintf("%s:%s:*", parts[0], parts[1])) // app:*:* permsToCheck = append(permsToCheck, fmt.Sprintf("%s:*:*", parts[0])) + } else if len(parts) == 2 { + // resource:action -> appKey:resource:action + fullPermID := fmt.Sprintf("%s:%s", a.scope, permissionID) + permsToCheck = append(permsToCheck, fullPermID) + permsToCheck = append(permsToCheck, fmt.Sprintf("%s:%s:*", a.scope, parts[0])) + permsToCheck = append(permsToCheck, fmt.Sprintf("%s:*:*", a.scope)) } // 检查这些角色是否有所需权限 @@ -680,12 +795,24 @@ func (a *appAuth) checkPermissionDB(ctx context.Context, userID, orgID, permissi if len(parts) == 3 { permsToCheck = append(permsToCheck, fmt.Sprintf("%s:%s:*", parts[0], parts[1])) permsToCheck = append(permsToCheck, fmt.Sprintf("%s:*:*", parts[0])) + } else if len(parts) == 2 { + // resource:action -> appKey:resource:action + fullPermID := fmt.Sprintf("%s:%s", a.scope, permissionID) + permsToCheck = append(permsToCheck, fullPermID) + permsToCheck = append(permsToCheck, fmt.Sprintf("%s:%s:*", a.scope, parts[0])) + permsToCheck = append(permsToCheck, fmt.Sprintf("%s:*:*", a.scope)) } var userPermCount int64 query := cfg.DB().Model(&models.UserPermission{}). - Where("user_id = ? AND org_id = ? AND permission_id IN ? AND (expire_at IS NULL OR expire_at > ?)", - userID, orgID, permsToCheck, time.Now()) + Where("user_id = ? AND permission_id IN ? AND (expire_at IS NULL OR expire_at > ?)", + userID, permsToCheck, time.Now()) + + if orgID != "" { + query = query.Where("org_id = ?", orgID) + } else { + query = query.Where("org_id IS NULL") + } if resourceID != "" { query = query.Where("resource_id = ? OR resource_id = '*'", resourceID) diff --git a/auth/auth_test.go b/auth/auth_test.go index cc9f3a3..8edea4d 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -31,43 +31,21 @@ func TestMain(m *testing.M) { } func getTestAuth() Auth { - // 每次获取一个新的实例名,避免测试间冲突(虽然内存库是共享的,但数据清理可能不完全) - // 为了简单起见,我们在 TestMain 中只初始化一次 DB,但可以通过应用名区分 - appKey := fmt.Sprintf("test_app_%d", time.Now().UnixNano()) - - a := Factory.New(appKey, models.AppConfig{ - Name: "Test App", - DefaultRoles: []models.RoleDefinition{ - { - Code: "admin", - Name: "Administrator", - Policies: []string{"*:*"}, - }, - { - Code: "editor", - Name: "Editor", - Policies: []string{ - "article:create", - "article:read", - "article:update", - }, - }, - { - Code: "viewer", - Name: "Viewer", - Policies: []string{ - "article:read", - }, - }, - { - Code: "deleter", - Name: "Deleter", - Policies: []string{ - "article:delete", - }, - }, - }, - }) + // 每次获取一个新的 scope,避免测试间冲突(虽然内存库是共享的,但数据清理可能不完全) + // 为了简单起见,我们在 TestMain 中只初始化一次 DB,但可以通过 scope 区分 + scope := fmt.Sprintf("test_scope_%d", time.Now().UnixNano()) + + a := Factory.New(scope) + + // 添加角色定义 + a.AddRole("admin", "Administrator", "*:*") + a.AddRole("editor", "Editor", + "article:create", + "article:read", + "article:update", + ) + a.AddRole("viewer", "Viewer", "article:read") + a.AddRole("deleter", "Deleter", "article:delete") // 初始化 if err := a.(*appAuth).init(); err != nil { @@ -228,10 +206,10 @@ func TestResourcePermission(t *testing.T) { resID := "doc_123" // 需要先创建权限定义,因为 GrantResourcePerm 会检查权限是否存在 - permID := fmt.Sprintf("%s:doc:read", a.(*appAuth).appKey) + permID := fmt.Sprintf("%s:doc:read", a.(*appAuth).scope) perm := models.Permission{ ID: permID, - AppKey: a.(*appAuth).appKey, + Scope: a.(*appAuth).scope, Resource: "doc", Action: "read", Description: "Read Doc", diff --git a/auth/middleware.go b/auth/middleware.go index 3696f01..3a1ec01 100644 --- a/auth/middleware.go +++ b/auth/middleware.go @@ -5,7 +5,10 @@ package auth import ( + "encoding/json" + "fmt" "strings" + "time" "github.com/veypi/vbase/cfg" "github.com/veypi/vbase/libs/cache" @@ -14,9 +17,16 @@ import ( "github.com/veypi/vigo" ) +// orgMemberCache 组织成员身份缓存结构 +type orgMemberCache struct { + IsMember bool `json:"is_member"` + RoleCodes []string `json:"role_codes"` +} + // AuthMiddleware 统一认证中间件 // 1. JWT认证: 解析token,验证有效性,设置用户信息 // 2. 组织上下文: 如果请求包含org_id,验证用户成员身份,设置组织信息 +// 使用Redis缓存组织成员身份和角色信息,减少数据库查询 func AuthMiddleware() func(*vigo.X) error { return func(x *vigo.X) error { // === 1. JWT 认证部分 === @@ -59,21 +69,57 @@ func AuthMiddleware() func(*vigo.X) error { return nil } - // 验证用户是否为组织成员 - var member models.OrgMember - if err := cfg.DB().Where("org_id = ? AND user_id = ? AND status = ?", - orgID, claims.UserID, models.MemberStatusActive).First(&member).Error; err != nil { + // 尝试从缓存获取组织成员信息 + var roleCodes []string + var isMember bool + + if cache.IsEnabled() { + cacheKey := fmt.Sprintf("auth:org_member:%s:%s", claims.UserID, orgID) + cachedData, err := cache.Get(cacheKey) + if err == nil && cachedData != "" { + var cached orgMemberCache + if err := json.Unmarshal([]byte(cachedData), &cached); err == nil { + isMember = cached.IsMember + roleCodes = cached.RoleCodes + } + } + } + + // 缓存未命中,查询数据库 + if roleCodes == nil { + // 验证用户是否为组织成员 + var member models.OrgMember + err := cfg.DB().Where("org_id = ? AND user_id = ? AND status = ?", + orgID, claims.UserID, models.MemberStatusActive).First(&member).Error + isMember = err == nil + + if isMember { + // 查询用户的角色 + cfg.DB().Model(&models.UserRole{}). + Joins("JOIN roles ON user_roles.role_id = roles.id"). + Where("user_roles.user_id = ? AND user_roles.org_id = ?", claims.UserID, orgID). + Pluck("roles.code", &roleCodes) + } + + // 写入缓存 + if cache.IsEnabled() { + cacheData := orgMemberCache{ + IsMember: isMember, + RoleCodes: roleCodes, + } + if data, err := json.Marshal(cacheData); err == nil { + cacheKey := fmt.Sprintf("auth:org_member:%s:%s", claims.UserID, orgID) + // 缓存5分钟 + cache.Set(cacheKey, string(data), 5*time.Minute) + } + } + } + + if !isMember { return vigo.ErrForbidden.WithString("you are not a member of this organization") } x.Set("org_id", orgID) - - // 从 UserRole 表查询用户的角色 - var roleCodes []string - cfg.DB().Model(&models.UserRole{}). - Joins("JOIN roles ON user_roles.role_id = roles.id"). - Where("user_roles.user_id = ? AND user_roles.org_id = ?", claims.UserID, orgID). - Pluck("roles.code", &roleCodes) x.Set("org_roles", roleCodes) return nil diff --git a/docs/integration.md b/docs/integration.md index 9b0a06c..9f8c6e6 100644 --- a/docs/integration.md +++ b/docs/integration.md @@ -77,29 +77,19 @@ VBase 提供了强大的 RBAC (基于角色的访问控制) 权限系统。 在您的应用中,使用 `vbase.Auth.New` 创建应用专属的权限实例。 ```go -import ( - "github.com/veypi/vbase" - "github.com/veypi/vbase/models" -) +import "github.com/veypi/vbase" // 定义您的应用权限实例 -var AppAuth = vbase.Auth.New("my_app", models.AppConfig{ - Name: "My Application", - Description: "我的应用描述", +var AppAuth = vbase.Auth.New("my_app") + +func init() { // 定义应用的默认角色 - DefaultRoles: []models.RoleDefinition{ - { - Code: "admin", - Name: "管理员", - Policies: []string{"*:*"}, // 拥有所有权限 - }, - { - Code: "editor", - Name: "编辑", - Policies: []string{"article:create", "article:update"}, - }, - }, -}) + AppAuth.AddRole("admin", "管理员", "*:*") // 拥有所有权限 + AppAuth.AddRole("editor", "编辑", + "article:create", + "article:update", + ) +} ``` #### 2.2.2 使用权限中间件 diff --git a/models/auth.go b/models/auth.go index c8d08fc..d96c187 100644 --- a/models/auth.go +++ b/models/auth.go @@ -20,12 +20,12 @@ const ( ) // Permission 权限定义表(权限字典) -// ID 格式: app:resource:action (例如: crm:customer:read) +// ID 格式: scope:resource:action (例如: vb:user:read) type Permission struct { - ID string `json:"id" gorm:"primaryKey;size:100" desc:"权限ID,格式: app:resource:action"` + ID string `json:"id" gorm:"primaryKey;size:100" desc:"权限ID,格式: scope:resource:action"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - AppKey string `json:"app_key" gorm:"index;size:50" desc:"应用标识"` + Scope string `json:"scope" 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:"权限描述"` @@ -107,21 +107,6 @@ 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"`