# Namespace Isolation: Summary and Implementation Gaps ## Executive Summary The Namespace Isolation bounded context provides logical boundaries for event visibility and storage in Aether. The implementation is **mostly complete** with strong primitives for exact namespace matching and NATS-native wildcard support. The main gaps are **metadata tracking** (namespace field on events) and **application-layer validation** (explicit namespace format checking). **Status:** Core invariants are enforced; refactoring needed for observability and safety. --- ## Core Invariants: Enforcement Status | Invariant | Status | Evidence | |-----------|--------|----------| | **Namespace Boundary Isolation** | ✓ Enforced | EventBus separates exactSubscribers by namespace; JetStream streams are per-namespace | | **Namespace Name Safety** | ✓ Enforced | sanitizeSubject() applied at storage layer; NATS subject injection prevented | | **Wildcard Subscriptions Bypass Isolation** | ✓ Documented | Explicit in code comments and docstrings (eventbus.go lines 15-16, 101-102) | | **Subject Pattern Matching Consistency** | ✓ Enforced | MatchNamespacePattern() consistent with NATS token-based matching | --- ## Implementation Alignment ### EventBus: Exact and Wildcard Routing **Architecture:** ``` EventBus ├── exactSubscribers: map[namespace] → [subscriber] (isolation enforced) ├── wildcardSubscribers: [subscriber] (cross-boundary) └── Publish(namespace) → delivers to both groups (with filter matching) ``` **Lines:** eventbus.go 69-71, 131-137, 186-205 **Invariant Enforced:** Namespace Boundary Isolation **Strengths:** - Exact subscribers only receive from their namespace - Wildcard subscribers are separate list (intentional, documented) - Filter matching is AND logic (events must pass all filters) **Gap Identified:** - No explicit comment explaining separation strategy - Could document: "exactSubscribers enforce isolation; wildcardSubscribers are intentional exceptions" --- ### NATSEventBus: Cross-Node Replication **Architecture:** ``` NATSEventBus (extends EventBus) ├── Subscribe(pattern) → creates NATS subject "aether.events.{pattern}" ├── Publish(namespace, event) → publishes to "aether.events.{namespace}" └── handleNATSEvent() → deduplicates local events, delivers to wildcard subscribers ``` **Lines:** nats_eventbus.go 63-111, 186-211 **Invariant Enforced:** Namespace Boundary Isolation (at NATS subject level) **Strengths:** - NATS subjects include namespace as prefix - Local node events are deduplicated (not re-delivered) - Wildcard patterns work via NATS native subject matching - Pattern tracking prevents duplicate NATS subscriptions **Gap Identified:** - No sanitization of namespace before formatting NATS subject (relies on EventBus callers to pass valid namespaces) - Mitigation: JetStreamEventStore sanitizes at storage layer (defense in depth) --- ### JetStreamEventStore: Storage-Layer Isolation **Architecture:** ``` JetStreamEventStore ├── Namespace: "tenant-abc" → Stream: "tenant_abc_events" ├── Subject: "{namespace}_{streamName}.events.{actorType}.{actorID}" └── SaveEvent() → publishes only to namespaced stream ``` **Lines:** store/jetstream.go 66-106, 148-157 **Invariants Enforced:** Namespace Boundary Isolation, Namespace Name Safety **Strengths:** - Separate JetStream streams per namespace (complete storage isolation) - sanitizeSubject() applied to namespace before stream name (lines 83) - GetEvents() queries only the configured stream (cannot see other namespaces) - NewJetStreamEventStoreWithNamespace() has clear, explicit constructor **Gaps Identified:** 1. **No namespace field in Event struct** - Current: namespace is transport metadata, not event data - Implication: Replayed events don't carry their namespace - Risk: Difficult to audit which namespace an event belonged to - Refactoring: Add `Namespace string` field to Event (JSON: "namespace", omitempty) 2. **No namespace validation before CreateNamespacedEventStore** - Current: Invalid names are silently sanitized - Example: NewJetStreamEventStoreWithNamespace(conn, "events", "tenant.abc") succeeds, but stream becomes "tenant_abc_events" - Implication: Behavior change is silent; developer thinks they're using "tenant.abc" but storage uses "tenant_abc" - Refactoring: Add Namespace.Validate() function; reject invalid names with clear errors --- ### Pattern Matching: Token-Based Consistency **Implementation:** pattern.go 27-77 **Invariant Enforced:** Subject Pattern Matching Consistency **Strengths:** - Token-based matching (dot-separated) matches NATS conventions - "*" matches exactly one token - ">" matches one or more tokens (only at end) - Recursive matching is clear and correct **Gaps Identified:** 1. **MatchActorPattern has two code paths** - Simple patterns (no dots): prefix/suffix wildcard matching (lines 183-193) - Complex patterns (with dots): token-based matching via MatchNamespacePattern() - Implication: Inconsistency between actor ID patterns and namespace patterns - Example: "order-*" matches "order-123" but "order.*" matches "order.us.east" - Mitigation: Document the difference; clarify when to use dots vs. prefixes 2. **IsWildcardPattern is simple but misses hierarchical wildcards** - Current: Checks for "*" or ">" anywhere in pattern - Fine for EventBus (correct) - No issue, but worth documenting for future use --- ### SubscriptionFilter: Composable Filtering **Implementation:** pattern.go 85-144 **Usage:** EventBus.SubscribeWithFilter(), NATSEventBus filtering **Strengths:** - EventTypes filter is OR (any one matches) - ActorPattern filter is AND with EventTypes - IsEmpty() correctly detects no filters **No Gaps:** Filtering is correctly implemented. --- ## Refactoring Priorities ### P1: Add Namespace to Event Metadata (2-3 days) **Why:** Required for observability and audit trails **Changes:** ```go type Event struct { ID string EventType string ActorID string Namespace string // ← NEW: Logical boundary this event belongs to Version int64 Data map[string]interface{} Timestamp time.Time Metadata map[string]string } ``` **Impact:** - Enables: `event.Namespace` in replayed events - Enables: Filtering on namespace in read models - Enables: Clear audit trail of which namespace an event came from - Breaking: None (new field, optional in JSON via omitempty) **Testing:** Verify namespace is set during SaveEvent() and persists through replay --- ### P2: Add Namespace Validation (1 day) **Why:** Prevent silent behavior changes from namespace sanitization **Changes:** ```go // In application or aether package func ValidateNamespace(ns string) error { if ns == "" { return errors.New("namespace cannot be empty") } // Allow alphanumeric, hyphens, underscores, dots for hierarchies if !regexp.MustCompile(`^[a-zA-Z0-9._-]+$`).MatchString(ns) { return fmt.Errorf("namespace contains invalid characters: %q", ns) } return nil } // Update constructors func NewJetStreamEventStoreWithNamespace(conn, streamName, namespace string) error { if err := ValidateNamespace(namespace); err != nil { return err // Reject immediately, don't sanitize silently } // ... rest of initialization } ``` **Impact:** - Prevents: Silent sanitization of invalid names - Enables: Clear error messages for misconfigured namespaces - Breaking: Existing invalid namespaces may fail validation (rare, can be migrated) **Testing:** TestValidateNamespace cases for valid/invalid formats --- ### P3: Create NamespacedEventBus Wrapper (2-3 days) **Why:** Improve API safety; prevent accidental wildcard subscriptions **Changes:** ```go type NamespacedEventBus struct { bus *EventBus namespace string // Bound to single namespace } func (neb *NamespacedEventBus) Subscribe() <-chan *Event { // ALWAYS exact match, never wildcard // Reject if pattern contains "*" or ">" return neb.bus.Subscribe(neb.namespace) } func (neb *NamespacedEventBus) SubscribeWithFilter(filter *SubscriptionFilter) <-chan *Event { return neb.bus.SubscribeWithFilter(neb.namespace, filter) } func (neb *NamespacedEventBus) Publish(event *Event) { // Verify event is for this namespace (once namespace field is added) if event.Namespace != neb.namespace { return errors.New("event namespace mismatch") } neb.bus.Publish(neb.namespace, event) } ``` **Impact:** - Prevents: Accidental wildcard subscriptions - Enables: Easier-to-use API for single-namespace scenarios - Breaking: None (additive wrapper) **Testing:** TestNamespacedEventBus verifies wildcards are rejected --- ### P4: Cross-Namespace Integration Tests (1-2 days) **Why:** Verify isolation at all layers (storage, local pub/sub, cross-node) **Test Cases:** 1. SaveEvent(store1, ns1) → GetEvents(store2, ns2) returns empty 2. Publish(ns1) → Subscribe(ns2) receives nothing 3. SaveEvent(store1, ns1, "prod.orders") → GetEvents(store2, "prod.users") returns empty 4. Publish("prod.orders") → Subscribe("prod.*") receives it 5. Publish("prod.orders") → Subscribe(">") receives it 6. Cross-node: Publish on node1 → Subscribe on node2, verify namespace isolation **Impact:** - Confidence: Safety invariant is enforced at all layers - Coverage: Currently no integration tests for namespace isolation - Effort: 1-2 days --- ### P5: Document Namespace Hierarchies and Patterns (1 day) **Why:** Clarify intended use of dot-separated namespaces **Documentation:** - Define hierarchical namespace format: `{domain}.{environment}.{tenant}` - Examples: - "orders.prod.acme" - ACME Corp, production orders domain - "users.staging.acme" - ACME Corp, staging users domain - "acme.>" - All namespaces for ACME tenant - "*.prod.*" - All production namespaces across all domains - Warning: Dots are sanitized to underscores at JetStream level - "orders.prod" → stored in "orders_prod_events" stream - Namespace isolation still works, but stream name is sanitized **Impact:** - Clarity: Applications understand how to structure namespaces - Examples: Clear patterns for multi-tenant and multi-domain setups --- ## Security Considerations ### Wildcard Subscriptions Bypass Isolation **Intentional Design:** Wildcard patterns receive events from multiple namespaces. This is necessary for: - Cross-cutting concerns (logging, auditing, monitoring) - Trusted system components (ops, admin code) - Distributed tracing (observability across boundaries) **Documented In:** - eventbus.go lines 10-16 (EventBroadcaster) - eventbus.go lines 63-66 (EventBus) - nats_eventbus.go lines 15-20 (NATSEventBus) - pattern.go lines 19-26 (MatchNamespacePattern) **Mitigations:** 1. **Code Review:** Audit any use of "*" or ">" in subscriptions 2. **Logging:** NATSEventBus logs wildcard subscriptions (lines 100-101) 3. **Metrics:** Wildcard subscriptions are tracked (RecordSubscribe) 4. **Documentation:** Explicit warnings in all relevant locations **Risk Assessment:** - Low if: Wildcard access is restricted to trusted components (ops, admin code paths) - High if: User-facing APIs accept subscription patterns from external clients - High if: Feature flags control subscription patterns (default should be exact, not wildcard) **Recommended Controls:** - Restrict wildcard subscriptions to admin/ops code - Audit logs for all wildcard subscription creation - Code review checklist: "Does this code use wildcard subscriptions? If yes, is it justified?" - Disable wildcard subscriptions by default in application APIs --- ## Testing Strategy ### Existing Tests (namespace_test.go, pattern_test.go) - ✓ Stream naming with and without namespace - ✓ Sanitization of special characters - ✓ Pattern matching (single token, multi-token, wildcards) - ✓ Actor type extraction ### Missing Integration Tests - ✗ SaveEvent in one namespace invisible to another namespace's GetEvents - ✗ Publish to exact namespace not received by different exact namespace - ✗ Publish to "prod.orders" IS received by Subscribe("prod.*") - ✗ Cross-node namespace isolation ### Recommended Test Suite ```go // store_namespace_isolation_test.go func TestStorageIsolation_DifferentNamespaces(t *testing.T) func TestStorageIsolation_SameStreamName(t *testing.T) func TestStorageIsolation_ActorIDConflict(t *testing.T) // eventbus_namespace_isolation_test.go func TestEventBusExactNamespace(t *testing.T) func TestEventBusWildcardPattern(t *testing.T) func TestEventBusFilterWithNamespace(t *testing.T) // nats_eventbus_namespace_isolation_integration_test.go func TestNATSEventBusCrossNodeIsolation(t *testing.T) func TestNATSEventBusWildcardCrossNode(t *testing.T) ``` --- ## Implementation Checklist - [ ] **Phase 1: Core (0 risk)** - [ ] Add namespace field to Event struct (omitempty JSON) - [ ] Update SaveEvent to populate event.Namespace - [ ] Create integration tests for storage isolation - [ ] **Phase 2: Validation (low risk)** - [ ] Create ValidateNamespace() function - [ ] Update NewJetStreamEventStoreWithNamespace to validate - [ ] Document namespace format conventions - [ ] **Phase 3: Convenience (low risk)** - [ ] Create NamespacedEventBus wrapper - [ ] Add examples for both generic and namespaced use cases - [ ] **Phase 4: Observability (low risk)** - [ ] Document hierarchical namespace patterns - [ ] Update examples with prod/staging/tenant scenarios - [ ] Add to vision.md or architecture guide - [ ] **Phase 5: Safety (no code changes)** - [ ] Add wildcard subscription warnings to API docs - [ ] Create audit logging guide (optional implementation) - [ ] Add security checklist to CLAUDE.md --- ## References **Full Domain Model:** DOMAIN_MODEL_NAMESPACE_ISOLATION.md **Key Code Locations:** - EventBus: `/aether/eventbus.go` (268 lines) - NATSEventBus: `/aether/nats_eventbus.go` (231 lines) - Pattern Matching: `/aether/pattern.go` (197 lines) - JetStream Storage: `/aether/store/jetstream.go` (382 lines) - Namespace Tests: `/aether/store/namespace_test.go` (125 lines) **Design Philosophy:** - Vision: `/aether/vision.md` ("Primitives Over Frameworks") - Organization: `/aether/CLAUDE.md` ("Namespace Isolation" bounded context) --- ## Conclusion **Status: Implementation Meets Domain Model (Mostly)** The Namespace Isolation context has **strong primitive foundations**: - ✓ Storage-layer isolation (separate JetStream streams) - ✓ Pub/sub isolation (exactSubscribers per namespace) - ✓ Pattern matching (NATS-native wildcards) - ✓ Safety warnings (documented wildcard risks) **Remaining Work:** 1. Add namespace metadata to events (enables audit trails, tracing) 2. Add explicit validation (prevents silent sanitization) 3. Improve API ergonomics (NamespacedEventBus wrapper) 4. Integration tests (confidence in safety invariants) **No architectural changes needed.** Refactoring is additive: improvements to observability, safety, and usability without changing core design. The system correctly enforces the core invariant: **Events in namespace X are invisible to queries from namespace Y**, except through explicit wildcard subscriptions by trusted components.