package api import ( "context" "encoding/json" "net/http" "os" "strings" "sync" "github.com/rcourtman/pulse-go-rewrite/internal/websocket" ) // Multi-tenant feature flag (default: disabled) // Set PULSE_MULTI_TENANT_ENABLED=true to enable multi-tenant functionality. // This is separate from licensing - the feature must be explicitly enabled // AND properly licensed for non-default organizations to work. var multiTenantEnabled = strings.EqualFold(os.Getenv("PULSE_MULTI_TENANT_ENABLED"), "true") // IsMultiTenantEnabled returns whether multi-tenant functionality is enabled. func IsMultiTenantEnabled() bool { return multiTenantEnabled } // DefaultMultiTenantChecker implements websocket.MultiTenantChecker for use with the WebSocket hub. type DefaultMultiTenantChecker struct { hostedMode bool } // CheckMultiTenant checks if multi-tenant is enabled (feature flag) and licensed for the org. // In hosted mode, tenant routing uses subscription lifecycle checks instead of FeatureMultiTenant. // Uses the LicenseServiceProvider for proper per-tenant license lookup. func (c *DefaultMultiTenantChecker) CheckMultiTenant(ctx context.Context, orgID string) websocket.MultiTenantCheckResult { // Default org is always allowed if orgID == "" || orgID == "default" { return websocket.MultiTenantCheckResult{ Allowed: true, FeatureEnabled: true, Licensed: true, } } if c.hostedMode { // Hosted mode: tenant routing is infrastructure. Check subscription lifecycle // instead of FeatureMultiTenant so Cloud customers can connect. checkCtx := context.WithValue(ctx, OrgIDContextKey, orgID) if isHostedSubscriptionValid(checkCtx) { return websocket.MultiTenantCheckResult{ Allowed: true, FeatureEnabled: true, Licensed: true, } } return websocket.MultiTenantCheckResult{ Allowed: false, FeatureEnabled: true, Licensed: false, Reason: "Cloud subscription is not active", } } // Self-hosted mode: check feature flag first if !multiTenantEnabled { return websocket.MultiTenantCheckResult{ Allowed: false, FeatureEnabled: false, Licensed: false, Reason: "Multi-tenant functionality is not enabled", } } // Feature is enabled, check license using the provider service := getLicenseServiceForContext(ctx) if !hasMultiTenantLicense(service) { return websocket.MultiTenantCheckResult{ Allowed: false, FeatureEnabled: true, Licensed: false, Reason: "Multi-tenant access requires an Enterprise license", } } return websocket.MultiTenantCheckResult{ Allowed: true, FeatureEnabled: true, Licensed: true, } } // NewMultiTenantChecker creates a new DefaultMultiTenantChecker. func NewMultiTenantChecker(hostedMode bool) *DefaultMultiTenantChecker { return &DefaultMultiTenantChecker{hostedMode: hostedMode} } // SetMultiTenantEnabled allows programmatic control of the feature flag (for testing). // It also keeps the env-backed dev-mode entitlement path aligned so tests exercise // the same multi-tenant state through both middleware and license-service checks. func SetMultiTenantEnabled(enabled bool) { multiTenantEnabled = enabled if enabled { _ = os.Setenv("PULSE_MULTI_TENANT_ENABLED", "true") return } _ = os.Unsetenv("PULSE_MULTI_TENANT_ENABLED") } // LicenseServiceProvider provides license service for a given context. // This allows the middleware to use the properly initialized per-tenant services. type LicenseServiceProvider interface { Service(ctx context.Context) *licenseService } var ( licenseServiceProvider LicenseServiceProvider licenseServiceMu sync.RWMutex ) // SetLicenseServiceProvider sets the provider for license services. // This should be called during router initialization with LicenseHandlers. func SetLicenseServiceProvider(provider LicenseServiceProvider) { licenseServiceMu.Lock() defer licenseServiceMu.Unlock() licenseServiceProvider = provider } // getLicenseServiceForContext returns the license service for the given context. // Falls back to a new service if no provider is set (shouldn't happen in production). func getLicenseServiceForContext(ctx context.Context) *licenseService { licenseServiceMu.RLock() provider := licenseServiceProvider licenseServiceMu.RUnlock() if provider != nil { return provider.Service(ctx) } // Fallback: create a new service (won't have persisted license) return newLicenseService() } // hasMultiTenantFeatureForContext checks if the multi-tenant feature is licensed for the context. func hasMultiTenantFeatureForContext(ctx context.Context) bool { service := getLicenseServiceForContext(ctx) return hasMultiTenantLicense(service) } // RequireMultiTenant returns a middleware that checks if the multi-tenant feature is licensed. // It allows access to the "default" organization without a license, but requires // an Enterprise license for non-default organizations. func RequireMultiTenant(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { orgID := GetOrgID(r.Context()) // Default org is always allowed (backward compatibility) if orgID == "" || orgID == "default" { next(w, r) return } // Feature flag check - multi-tenant must be explicitly enabled if !multiTenantEnabled { writeMultiTenantDisabledError(w) return } // Non-default orgs require multi-tenant license if !hasMultiTenantFeatureForContext(r.Context()) { writeMultiTenantRequiredError(w) return } next(w, r) } } // writeMultiTenantRequiredError writes a 402 Payment Required response // indicating that multi-tenant requires an Enterprise license. func writeMultiTenantRequiredError(w http.ResponseWriter) { writePaymentRequired(w, map[string]interface{}{ "error": "license_required", "message": "Multi-tenant access requires an Enterprise license", "feature": featureMultiTenantKey, "tier": "enterprise", }) } // writeMultiTenantDisabledError writes a 501 Not Implemented response // indicating that multi-tenant functionality is not enabled. func writeMultiTenantDisabledError(w http.ResponseWriter) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotImplemented) json.NewEncoder(w).Encode(map[string]interface{}{ "error": "feature_disabled", "message": "Multi-tenant functionality is not enabled. Set PULSE_MULTI_TENANT_ENABLED=true to enable.", }) } // CheckMultiTenantLicense checks if multi-tenant is enabled and licensed for the given org ID. // Returns true if: // - The org ID is "default" or empty (always allowed) // - The feature flag is enabled AND the multi-tenant feature is licensed func CheckMultiTenantLicense(ctx context.Context, orgID string) bool { if orgID == "" || orgID == "default" { return true } // Feature flag must be enabled if !multiTenantEnabled { return false } return hasMultiTenantFeatureForContext(ctx) }