mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 11:30:15 +00:00
Implements multi-tenant infrastructure for organization-based data isolation. Feature is gated behind PULSE_MULTI_TENANT_ENABLED env var and requires Enterprise license - no impact on existing users. Core components: - TenantMiddleware: extracts org ID, validates access, 501/402 responses - AuthorizationChecker: token/user access validation for organizations - MultiTenantChecker: WebSocket upgrade gating with license check - Per-tenant audit logging via LogAuditEventForTenant - Organization model with membership support Gating behavior: - Feature flag disabled: 501 Not Implemented for non-default orgs - Flag enabled, no license: 402 Payment Required - Default org always works regardless of flag/license Documentation added: docs/MULTI_TENANT.md
171 lines
4.3 KiB
Go
171 lines
4.3 KiB
Go
package audit
|
|
|
|
import (
|
|
"path/filepath"
|
|
"sync"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// TenantLoggerManager manages per-tenant audit loggers.
|
|
// Each tenant gets their own isolated audit database at <orgDir>/audit.db
|
|
type TenantLoggerManager struct {
|
|
mu sync.RWMutex
|
|
loggers map[string]Logger
|
|
dataPath string // Base data path
|
|
factory LoggerFactory // Factory for creating tenant loggers
|
|
}
|
|
|
|
// LoggerFactory creates audit loggers for specific paths.
|
|
type LoggerFactory interface {
|
|
// CreateLogger creates a new audit logger at the specified path.
|
|
CreateLogger(dbPath string) (Logger, error)
|
|
}
|
|
|
|
// DefaultLoggerFactory creates console loggers (for OSS).
|
|
type DefaultLoggerFactory struct{}
|
|
|
|
// CreateLogger creates a console logger (doesn't use the path).
|
|
func (f *DefaultLoggerFactory) CreateLogger(dbPath string) (Logger, error) {
|
|
return NewConsoleLogger(), nil
|
|
}
|
|
|
|
// NewTenantLoggerManager creates a new tenant logger manager.
|
|
func NewTenantLoggerManager(dataPath string, factory LoggerFactory) *TenantLoggerManager {
|
|
if factory == nil {
|
|
factory = &DefaultLoggerFactory{}
|
|
}
|
|
return &TenantLoggerManager{
|
|
loggers: make(map[string]Logger),
|
|
dataPath: dataPath,
|
|
factory: factory,
|
|
}
|
|
}
|
|
|
|
// GetLogger returns the audit logger for a specific organization.
|
|
// It lazily initializes the logger if it doesn't exist.
|
|
// For the "default" org, it returns the global logger.
|
|
func (m *TenantLoggerManager) GetLogger(orgID string) Logger {
|
|
// Default org uses the global logger
|
|
if orgID == "" || orgID == "default" {
|
|
return GetLogger()
|
|
}
|
|
|
|
m.mu.RLock()
|
|
logger, exists := m.loggers[orgID]
|
|
m.mu.RUnlock()
|
|
|
|
if exists {
|
|
return logger
|
|
}
|
|
|
|
// Create new logger for tenant
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
// Double-check after acquiring write lock
|
|
if logger, exists = m.loggers[orgID]; exists {
|
|
return logger
|
|
}
|
|
|
|
// Create tenant-specific logger
|
|
dbPath := filepath.Join(m.dataPath, "orgs", orgID, "audit.db")
|
|
logger, err := m.factory.CreateLogger(dbPath)
|
|
if err != nil {
|
|
log.Error().
|
|
Err(err).
|
|
Str("org_id", orgID).
|
|
Str("db_path", dbPath).
|
|
Msg("Failed to create tenant audit logger, using console logger")
|
|
logger = NewConsoleLogger()
|
|
}
|
|
|
|
m.loggers[orgID] = logger
|
|
log.Info().
|
|
Str("org_id", orgID).
|
|
Str("db_path", dbPath).
|
|
Msg("Created tenant audit logger")
|
|
|
|
return logger
|
|
}
|
|
|
|
// Log logs an audit event for a specific organization.
|
|
func (m *TenantLoggerManager) Log(orgID, eventType, user, ip, path string, success bool, details string) error {
|
|
logger := m.GetLogger(orgID)
|
|
event := Event{
|
|
EventType: eventType,
|
|
User: user,
|
|
IP: ip,
|
|
Path: path,
|
|
Success: success,
|
|
Details: details,
|
|
}
|
|
return logger.Log(event)
|
|
}
|
|
|
|
// Query queries audit events for a specific organization.
|
|
func (m *TenantLoggerManager) Query(orgID string, filter QueryFilter) ([]Event, error) {
|
|
logger := m.GetLogger(orgID)
|
|
return logger.Query(filter)
|
|
}
|
|
|
|
// Count counts audit events for a specific organization.
|
|
func (m *TenantLoggerManager) Count(orgID string, filter QueryFilter) (int, error) {
|
|
logger := m.GetLogger(orgID)
|
|
return logger.Count(filter)
|
|
}
|
|
|
|
// Close closes all tenant loggers.
|
|
func (m *TenantLoggerManager) Close() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
for orgID, logger := range m.loggers {
|
|
if closer, ok := logger.(interface{ Close() error }); ok {
|
|
if err := closer.Close(); err != nil {
|
|
log.Error().
|
|
Err(err).
|
|
Str("org_id", orgID).
|
|
Msg("Failed to close tenant audit logger")
|
|
}
|
|
}
|
|
}
|
|
|
|
m.loggers = make(map[string]Logger)
|
|
}
|
|
|
|
// GetAllLoggers returns all initialized loggers (for administrative purposes).
|
|
func (m *TenantLoggerManager) GetAllLoggers() map[string]Logger {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
result := make(map[string]Logger, len(m.loggers))
|
|
for k, v := range m.loggers {
|
|
result[k] = v
|
|
}
|
|
return result
|
|
}
|
|
|
|
// RemoveTenantLogger removes a specific tenant's logger.
|
|
// Useful when an organization is deleted.
|
|
func (m *TenantLoggerManager) RemoveTenantLogger(orgID string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
logger, exists := m.loggers[orgID]
|
|
if !exists {
|
|
return
|
|
}
|
|
|
|
if closer, ok := logger.(interface{ Close() error }); ok {
|
|
if err := closer.Close(); err != nil {
|
|
log.Error().
|
|
Err(err).
|
|
Str("org_id", orgID).
|
|
Msg("Failed to close tenant audit logger during removal")
|
|
}
|
|
}
|
|
|
|
delete(m.loggers, orgID)
|
|
log.Info().Str("org_id", orgID).Msg("Removed tenant audit logger")
|
|
}
|