Some checks failed
CI / Build & Test (push) Failing after 1m31s
- 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
213 lines
5.5 KiB
Go
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
|
|
}
|