# Namespace Isolation: Quick Reference Card ## Core Invariant **Events in namespace X must be invisible to queries from namespace Y** (except via explicit wildcard subscriptions by trusted components) --- ## Three Enforcement Layers ### Layer 1: Memory (EventBus) ``` Publish("tenant-a", event) → exactSubscribers["tenant-a"] ← event ✓ → exactSubscribers["tenant-b"] (blocked) ✗ → wildcardSubscribers (">") ← event ✓ (intentional) ``` ### Layer 2: Storage (JetStreamEventStore) ``` NewJetStreamEventStoreWithNamespace(conn, "events", "tenant-a") → Stream: "tenant_a_events" (separate NATS stream) GetEvents(store1) → queries "tenant_a_events" only ✓ GetEvents(store2) → queries "tenant_b_events" only (no cross-namespace) ✓ ``` ### Layer 3: Network (NATSEventBus) ``` Publish("tenant-a", event) → NATS subject: "aether.events.tenant-a" Subscribe("tenant-b") → NATS subject: "aether.events.tenant-b" (blocked at NATS) ✗ Subscribe("*") → NATS subject: "aether.events.*" (intentional wildcard) ✓ ``` --- ## Value Objects | Object | Type | Example | Purpose | |--------|------|---------|---------| | **Namespace** | string | "tenant-a", "prod.orders" | Logical boundary identifier | | **SubjectPattern** | string | "*", "prod.*", "prod.>" | NATS pattern for matching namespaces | | **SubscriptionFilter** | struct | `{EventTypes: ["OrderPlaced"]}` | Optional filtering (AND logic) | --- ## Commands at a Glance | Command | Input | Output | Enforces | |---------|-------|--------|----------| | **DefineNamespace** | string | Namespace | Namespace Name Safety | | **PublishToNamespace** | namespace, event | delivery to subscribers | Namespace Boundary Isolation | | **SubscribeToNamespace** | pattern, filter | channel | Isolation (exact) or Bypass (wildcard) | | **CreateNamespacedEventStore** | streamName, namespace | EventStore | Namespace Boundary Isolation | --- ## Policies (If X Then Y) | Trigger | Action | Invariant | |---------|--------|-----------| | **Publish to namespace** | Deliver to exact subscribers of that namespace | Isolation enforced | | **Wildcard subscription** | Deliver from all matching namespaces | Isolation bypassed (intentional) | | **Save event** | Store in namespace-scoped stream | Storage isolation | | **Namespace format invalid** | Sanitize (spaces, dots, *, >) to underscores | Name safety | --- ## Code Locations (Key Files) ``` /aether/ ├─ eventbus.go (268 lines) │ ├─ exactSubscribers[ns] (isolation boundary) │ ├─ wildcardSubscribers (intentional bypass) │ └─ Publish() routing logic │ ├─ nats_eventbus.go (231 lines) │ ├─ NATS subject: aether.events.{namespace} │ └─ Cross-node replication with pattern support │ ├─ pattern.go (197 lines) │ ├─ MatchNamespacePattern() (NATS-native matching) │ ├─ SubscriptionFilter (EventTypes + ActorPattern) │ └─ IsWildcardPattern() detector │ └─ store/jetstream.go (382 lines) ├─ Namespace → stream name mapping ├─ sanitizeSubject() (security) └─ Per-namespace streams ``` --- ## Pattern Matching Rules ``` Namespace: "prod.orders.acme" (3 tokens) Pattern → Match? ──────────────── "prod.orders.acme" → ✓ (exact) "prod.orders.*" → ✓ (* matches "acme") "prod.>" → ✓ (> matches "orders.acme") "prod.*" → ✗ (* matches single token only) "*" → ✗ (* doesn't match dots) ">" → ✓ (> matches everything) Rules: • "." separates tokens • "*" = exactly one token (no dots) • ">" = one or more tokens (end only) • Exact string = exact match ``` --- ## Common Operations ### Exact Subscription (Isolation Enforced) ```go bus := aether.NewEventBus() ch := bus.Subscribe("tenant-abc") // Only tenant-abc events event := Event{ActorID: "order-123", ...} bus.Publish("tenant-abc", event) // ch receives ✓ bus.Publish("tenant-def", event) // ch receives nothing ✗ ``` ### Wildcard Subscription (Isolation Bypassed) ```go bus := aether.NewEventBus() ch := bus.Subscribe(">") // ALL namespaces bus.Publish("tenant-abc", event) // ch receives ✓ bus.Publish("tenant-def", event) // ch receives ✓ // CAUTION: Only for trusted logging/auditing code ``` ### Namespaced Event Store ```go store1 := store.NewJetStreamEventStoreWithNamespace( natsConn, "events", "tenant-a") store2 := store.NewJetStreamEventStoreWithNamespace( natsConn, "events", "tenant-b") store1.SaveEvent(event) // Goes to "tenant_a_events" stream store2.GetEvents("order-123", 0) // Queries "tenant_b_events" only // event not found (different stream) ``` ### Filtered Subscription ```go filter := &aether.SubscriptionFilter{ EventTypes: []string{"OrderPlaced", "OrderShipped"}, // OR logic ActorPattern: "order-*", // AND with types } ch := bus.SubscribeWithFilter("tenant-a", filter) // Receives only OrderPlaced or OrderShipped events for order-* actors ``` --- ## Sanitization Examples | Input | Purpose | Output | Example | |-------|---------|--------|---------| | "prod abc" | Namespace | "prod_abc" | Spaces → underscores | | "prod.orders" | Namespace | "prod_orders" | Dots → underscores | | "tenant*abc" | Namespace | "tenant_abc" | Stars → underscores | | "tenant>abc" | Namespace | "tenant_abc" | Greater → underscores | **Why?** NATS subject tokens can't contain these characters. --- ## Invariants: Verification Checklist - [ ] Events in namespace X are NOT visible to namespace Y (exact subscriptions) - [ ] GetEvents(store_a) does NOT return events from store_b - [ ] NATS subjects are correctly namespaced (aether.events.{namespace}) - [ ] Pattern matching works correctly (* = one token, > = multiple) - [ ] Wildcard subscriptions are documented and audited - [ ] Namespace names are sanitized before storage - [ ] Cross-node publishing respects namespace boundaries --- ## Anti-Patterns (What NOT to Do) | ❌ Don't | ✓ Do Instead | Why | |---------|-------------|-----| | `Subscribe(getTenantPattern())` | Validate pattern is exact (or audited) | Wildcards can leak data | | Accept namespace from untrusted input | Validate/sanitize first | Prevent injection | | Use namespace as tenant ID directly | Layer namespace over tenant abstraction | Aether is primitives, not framework | | Rely on wildcard for "secure" observation | Document and audit wildcard use | Wildcards bypass isolation | | Ignore pattern matching rules | Use NATS-native patterns | Inconsistency breaks isolation | --- ## Security Checklist ### For Namespace Isolation Review - [ ] Wildcard subscriptions used only in trusted code (logging, ops, admin) - [ ] Application validates namespace format before use - [ ] No way for external clients to trigger wildcard subscriptions - [ ] Audit logging enabled for all wildcard subscriptions - [ ] Integration tests verify cross-namespace isolation - [ ] Code reviews check for accidental "*" or ">" patterns - [ ] Documentation warns about wildcard risks ### For Multi-Tenant Deployments - [ ] Each tenant has distinct namespace (e.g., "tenant-123") - [ ] No global (">" ) subscriptions except trusted ops code - [ ] Namespace validation prevents tenant ID escaping - [ ] Storage streams are completely separate (not shared) - [ ] Cross-tenant queries fail (GetEvents blocks at stream level) --- ## Refactoring Priorities (Quick Decision) **Do Next?** 1. **P1: Add namespace to Event metadata** (2-3 days) - Impact: HIGH (enables better auditing) - Risk: LOW (backward compatible) 2. **P2: Explicit namespace validation** (1 day) - Impact: MEDIUM (prevents silent errors) - Risk: LOW (validates format) 3. **P3: NamespacedEventBus wrapper** (2-3 days) - Impact: MEDIUM (easier to use safely) - Risk: LOW (additive) **Skip?** P4-P5 are important but lower priority. --- ## Test Cases to Implement ### Must-Have Integration Tests ```go // Storage isolation SaveEvent(store1, "tenant-a", event) GetEvents(store2, "tenant-b", event.ActorID) → empty ✓ // Pub/sub isolation Publish("tenant-a", event) Subscribe("tenant-a") → receives ✓ Subscribe("tenant-b") → blocked ✗ // Pattern matching Publish("prod.orders", event) Subscribe("prod.*") → receives ✓ Subscribe("*.orders") → blocked ✗ // Cross-node node1.Publish("tenant-a", event) node2.Subscribe("tenant-b") → blocked ✗ ``` --- ## Implementation Status Summary | Aspect | Status | Notes | |--------|--------|-------| | **Exact namespace isolation** | ✓ Done | EventBus + JetStream enforce it | | **NATS-native patterns** | ✓ Done | Subject-level routing | | **Wildcard subscriptions** | ✓ Done | Documented as intentional exception | | **Pattern matching consistency** | ✓ Done | MatchNamespacePattern() correct | | **Namespace in Event metadata** | ✗ Pending | Add for audit trail | | **Explicit validation** | ✗ Pending | Add to prevent silent sanitization | | **NamespacedEventBus wrapper** | ✗ Pending | Convenience layer | | **Integration tests** | ✗ Pending | Verify isolation at all layers | --- ## Glossary - **Namespace**: Logical boundary (tenant, domain, environment) - **Pattern**: NATS-style wildcard for matching multiple namespaces - **Exact Subscription**: Subscribe to specific namespace (isolation enforced) - **Wildcard Subscription**: Subscribe with pattern (isolation bypassed, trusted components only) - **Invariant**: Business rule that must never be broken - **Subject**: NATS address for routing (aether.events.{namespace}) - **Stream**: JetStream storage container (one per namespace) - **Sanitization**: Replace unsafe characters (spaces, dots, *, >) with underscores --- ## Decision Framework **When using namespaces, ask:** 1. **Is this namespace logically distinct?** - Yes → use separate namespace - No → same namespace is fine 2. **Can users accidentally subscribe to wildcard?** - Yes → add validation - No → proceed 3. **Need cross-namespace visibility?** - Yes → use wildcard (document and audit) - No → exact subscriptions (isolation enforced) 4. **Is this in trusted code?** - Yes → wildcard subscriptions okay (with audit) - No → exact subscriptions only --- ## More Information - **Full Domain Model**: DOMAIN_MODEL_NAMESPACE_ISOLATION.md - **Implementation Gaps**: NAMESPACE_ISOLATION_SUMMARY.md - **Architecture Diagrams**: NAMESPACE_ISOLATION_ARCHITECTURE.md - **All Contexts**: DOMAIN_MODEL_INDEX.md --- **Last Updated:** 2026-01-12 | **Status:** Complete | **Ready for Implementation:** Yes