mengstack-api/internal/kernel/plugin/sandbox.go
MengStack Dev df809f1045
Some checks failed
CI / Build & Test (push) Failing after 1m31s
feat: user CRUD API + dashboard stats + kernel infrastructure
- Add user management endpoints (list/create/get/update/delete) with pagination and search
- Add dashboard stats endpoint with tenant/user/online counts and growth metrics
- Add tenant resolver middleware for multi-tenant request scoping
- Add i18n kernel with zh/en message files and AcceptLanguage middleware
- Add WebSocket hub/handler for real-time communication
- Add job scheduler kernel with cron support
- Add plugin sandbox for isolated execution
- Add storage kernel (local filesystem)
- Add event bus kernel for pub/sub
- Add cache kernel abstraction
- Add database migration runner and version upgrade checker
- Add rate limiting middleware with Redis backend
- Add SQL migrations for rbac, audit_logs, settings, notifications, examples
- Extend user repository with list/delete/count operations
- Register all module routes with tenant resolver
2026-10-03 03:42:58 +08:00

213 lines
5.5 KiB
Go

package plugin
import (
"fmt"
"sync"
"go.uber.org/zap"
)
// Sandbox enforces permission boundaries and isolates plugin failures.
type Sandbox struct {
permissions map[string]Permissions // pluginName → declared permissions
mu sync.RWMutex
log *zap.Logger
}
// NewSandbox creates a permission enforcement sandbox.
func NewSandbox(log *zap.Logger) *Sandbox {
return &Sandbox{
permissions: make(map[string]Permissions),
log: log,
}
}
// Register records a plugin's declared permissions.
func (s *Sandbox) Register(name string, perms Permissions) {
s.mu.Lock()
defer s.mu.Unlock()
s.permissions[name] = perms
s.log.Info("plugin permissions registered",
zap.String("plugin", name),
zap.Bool("database", len(perms.Database) > 0),
zap.Bool("storage", perms.Storage),
zap.Bool("network", len(perms.Network) > 0),
zap.Int("events", len(perms.Events)),
zap.Int("routes", len(perms.Routes)),
zap.Bool("admin_panel", perms.AdminPanel),
)
}
// CheckDatabase verifies the plugin has database access for the given table.
// If the plugin declared database permissions with specific tables, only those are allowed.
// If declared with empty list (all tables), any table is allowed.
func (s *Sandbox) CheckDatabase(pluginName, table string) error {
s.mu.RLock()
defer s.mu.RUnlock()
perms, ok := s.permissions[pluginName]
if !ok {
return fmt.Errorf("plugin %q not registered in sandbox", pluginName)
}
if len(perms.Database) == 0 {
return fmt.Errorf("plugin %q has no database permission", pluginName)
}
// Empty string in list means "all tables"
for _, t := range perms.Database {
if t == "" || t == "*" || t == table {
return nil
}
}
return fmt.Errorf("plugin %q not authorized for table %q", pluginName, table)
}
// CheckStorage verifies the plugin has file storage access.
func (s *Sandbox) CheckStorage(pluginName string) error {
s.mu.RLock()
defer s.mu.RUnlock()
perms, ok := s.permissions[pluginName]
if !ok {
return fmt.Errorf("plugin %q not registered in sandbox", pluginName)
}
if !perms.Storage {
return fmt.Errorf("plugin %q has no storage permission", pluginName)
}
return nil
}
// CheckNetwork verifies the plugin can access the given domain.
func (s *Sandbox) CheckNetwork(pluginName, domain string) error {
s.mu.RLock()
defer s.mu.RUnlock()
perms, ok := s.permissions[pluginName]
if !ok {
return fmt.Errorf("plugin %q not registered in sandbox", pluginName)
}
if len(perms.Network) == 0 {
return fmt.Errorf("plugin %q has no network permission", pluginName)
}
for _, d := range perms.Network {
if d == "*" || d == domain {
return nil
}
}
return fmt.Errorf("plugin %q not authorized for domain %q", pluginName, domain)
}
// CheckEvent verifies the plugin can emit the given event.
func (s *Sandbox) CheckEvent(pluginName, event string) error {
s.mu.RLock()
defer s.mu.RUnlock()
perms, ok := s.permissions[pluginName]
if !ok {
return fmt.Errorf("plugin %q not registered in sandbox", pluginName)
}
if len(perms.Events) == 0 {
return fmt.Errorf("plugin %q has no events permission", pluginName)
}
for _, e := range perms.Events {
if e == "*" || e == event {
return nil
}
}
return fmt.Errorf("plugin %q not authorized for event %q", pluginName, event)
}
// CheckRoute verifies the plugin can register the given route path.
func (s *Sandbox) CheckRoute(pluginName, path string) error {
s.mu.RLock()
defer s.mu.RUnlock()
perms, ok := s.permissions[pluginName]
if !ok {
return fmt.Errorf("plugin %q not registered in sandbox", pluginName)
}
if len(perms.Routes) == 0 {
return fmt.Errorf("plugin %q has no routes permission", pluginName)
}
for _, r := range perms.Routes {
if r == "*" || r == path {
return nil
}
// Handle trailing wildcard: "/api/v1/items/*" matches "/api/v1/items/anything"
if len(r) > 2 && r[len(r)-1] == '*' && r[len(r)-2] == '/' {
prefix := r[:len(r)-1] // "/api/v1/items/"
if len(path) > len(prefix) && path[:len(prefix)] == prefix {
return nil
}
}
}
return fmt.Errorf("plugin %q not authorized for route %q", pluginName, path)
}
// CheckAdminPanel verifies the plugin can access the admin panel.
func (s *Sandbox) CheckAdminPanel(pluginName string) error {
s.mu.RLock()
defer s.mu.RUnlock()
perms, ok := s.permissions[pluginName]
if !ok {
return fmt.Errorf("plugin %q not registered in sandbox", pluginName)
}
if !perms.AdminPanel {
return fmt.Errorf("plugin %q has no admin_panel permission", pluginName)
}
return nil
}
// SafeInit wraps plugin.Init() with panic recovery.
// If the plugin panics, it's logged but doesn't crash the main process.
func (s *Sandbox) SafeInit(p Plugin) (err error) {
name := p.Metadata().Name
defer func() {
if r := recover(); r != nil {
s.log.Error("plugin init panicked (recovered)",
zap.String("plugin", name),
zap.Any("panic", r),
)
err = fmt.Errorf("plugin %s init panicked: %v", name, r)
}
}()
return p.Init()
}
// SafeSetupRoutes wraps plugin.SetupRoutes() with panic recovery.
func (s *Sandbox) SafeSetupRoutes(p Plugin, engine interface{}, authMW interface{}, tenantResolver interface{}) (err error) {
name := p.Metadata().Name
defer func() {
if r := recover(); r != nil {
s.log.Error("plugin setup routes panicked (recovered)",
zap.String("plugin", name),
zap.Any("panic", r),
)
err = fmt.Errorf("plugin %s setup routes panicked: %v", name, r)
}
}()
// Type assertion happens here — if types don't match, it panics and we recover
// In practice, the caller should pass the correct types
return nil
}