# Namespace Isolation Architecture Diagram ## System Architecture ``` ┌─────────────────────────────────────────────────────────────────┐ │ Application Layer │ │ (Defines namespace meaning, validates format, controls access) │ └──────────────┬─────────────────────────────────────────────────┘ │ ┌──────┴──────────────────────────────────────────┐ │ │ v v ┌─────────────────────┐ ┌──────────────────────┐ │ EventBus │ │ JetStreamEventStore │ │ (Local Pub/Sub) │ │ (Persistent Store) │ │ │ │ │ │ publish(ns, event) │ │ saveEvent(ns, event) │ │ subscribe(pattern) │ │ getEvents(ns, actor) │ │ │ │ │ │ exactSubscribers │ │ Stream: │ │ [ns1] → [sub1] │ │ {namespace}_events │ │ [ns2] → [sub2] │ │ │ │ wildcardSubscribers │ │ Subject: │ │ [*] → [sub3] │ │ events.{type}.{id} │ │ [prod.>] → [sub4] │ │ │ └──────────┬──────────┘ └──────────┬───────────┘ │ │ │ (local node) │ (local node) │ │ v v ┌──────────────────────────────────────────────────────┐ │ NATSEventBus (Cross-Node) │ │ │ │ publish(ns) → aether.events.{namespace} │ │ subscribe(pattern) → aether.events.{pattern} │ │ │ │ NATS Subjects: │ │ ├─ aether.events.tenant-abc │ │ ├─ aether.events.tenant-def │ │ ├─ aether.events.prod.* (wildcard) │ │ └─ aether.events.> (all) │ └──────────────────────────────────────────────────────┘ │ │ (NATS cluster) v ┌──────────────────────────┐ │ Node 1 | Node 2 | ... │ │ (NATS cluster members) │ └──────────────────────────┘ ``` --- ## Invariant Enforcement Layers ### Layer 1: EventBus (Memory Isolation) ``` Exact Namespace Isolation ──────────────────────── publish("tenant-a", event) └─> exactSubscribers["tenant-a"] → [sub1, sub2] ✓ Receives └─> exactSubscribers["tenant-b"] → [sub3] ✗ Does NOT receive └─> wildcardSubscribers → [sub4 ("*"), sub5 (">")] ✓ Receive (intentional) Implementation: ├─ exactSubscribers: map[namespace] → [subscriber] (isolation boundary) ├─ wildcardSubscribers: [subscriber] (cross-boundary, trusted) └─ publish() delivers to both groups (filter matching applied) ``` ### Layer 2: JetStreamEventStore (Storage Isolation) ``` Storage Namespace Isolation ────────────────────────── Application 1: NewJetStreamEventStoreWithNamespace(natsConn, "events", "tenant-a") └─> Stream: "tenant_a_events" ├─ Subject: events.order.order-123 └─ Subject: events.user.user-456 Application 2: NewJetStreamEventStoreWithNamespace(natsConn, "events", "tenant-b") └─> Stream: "tenant_b_events" ├─ Subject: events.order.order-789 └─ Subject: events.user.user-012 Isolation Guarantee: └─ GetEvents(store1, actor) → only from "tenant_a_events" stream GetEvents(store2, actor) → only from "tenant_b_events" stream (Different NATS streams = complete isolation) ``` ### Layer 3: NATS Subject Routing (Cross-Node) ``` NATS Subject Hierarchy ───────────────────── aether.events.tenant-a ← Exact subscription aether.events.tenant-b ← Exact subscription aether.events.prod.orders ← Hierarchical aether.events.prod.users ← Hierarchical aether.events.staging.orders ← Hierarchical Pattern Matching: ├─ "tenant-a" → aether.events.tenant-a only ├─ "*" → aether.events.X (one token, no dots) ├─ "prod.*" → aether.events.prod.orders, prod.users ├─ "prod.>" → aether.events.prod.orders, prod.users, prod.orders.legacy └─ ">" → aether.events.ANYTHING (global, auditing only) ``` --- ## Event Flow: Publish and Subscribe ### Scenario 1: Exact Namespace Isolation ``` Application Code: ───────────────── bus := aether.NewEventBus() ch1 := bus.Subscribe("tenant-a") // Exact match: isolation enforced ch2 := bus.Subscribe("tenant-b") // Exact match: isolation enforced event := Event{ActorID: "order-123", EventType: "OrderPlaced"} bus.Publish("tenant-a", event) // Publish to tenant-a Result: ├─ ch1 ← event ✓ (matched exactly) ├─ ch2 ← (nothing) ✗ (different namespace) └─ Isolation enforced ``` **Implementation:** ```go func (eb *EventBus) Publish(namespaceID string, event *Event) { // 1. Deliver to exact subscribers subscribers := eb.exactSubscribers[namespaceID] // Only this namespace for _, sub := range subscribers { eb.deliverToSubscriber(sub, event, namespaceID) } // 2. Deliver to matching wildcard subscribers for _, sub := range eb.wildcardSubscribers { if MatchNamespacePattern(sub.pattern, namespaceID) { // Pattern matching eb.deliverToSubscriber(sub, event, namespaceID) } } } ``` --- ### Scenario 2: Wildcard Subscription (Cross-Boundary) ``` Application Code (Auditing System): ──────────────────────────────────── bus := aether.NewEventBus() chAudit := bus.Subscribe(">") // Wildcard: receives all namespaces bus.Publish("tenant-a", event1) // Publish to tenant-a bus.Publish("tenant-b", event2) // Publish to tenant-b Result: ├─ chAudit ← event1 ✓ (matches ">") └─ chAudit ← event2 ✓ (matches ">") Note: Wildcard intentionally bypasses isolation for observability (only granted to trusted auditing/logging/monitoring code) ``` --- ### Scenario 3: Cross-Node Publishing ``` Node 1: Node 2: ──────── ──────── neb1 := NewNATSEventBus(conn1) neb2 := NewNATSEventBus(conn2) ch1 := neb1.Subscribe("tenant-a") ch2 := neb2.Subscribe("tenant-a") neb1.Publish("tenant-a", event) │ ├─> EventBus.Publish() [local] │ └─> ch1 ← event (local delivery) │ └─> NATS Publish(aether.events.tenant-a) [cross-node] │ └─> [NATS Broker] │ └─> Node 2: Subscription aether.events.tenant-a └─> ch2 ← event (remote delivery) Result: Both nodes receive, isolation enforced at namespace level ``` --- ## Pattern Matching Rules ### Token-Based Matching (NATS-Native) ``` Namespace: "prod.orders.acme" (3 tokens: ["prod", "orders", "acme"]) Patterns and Results: ├─ "prod.orders.acme" → MATCH (exact) ├─ "prod.orders.*" → MATCH ("*" matches "acme") ├─ "prod.*.acme" → MATCH ("*" matches "orders") ├─ "prod.>" → MATCH (">" matches "orders.acme") ├─ "prod.*" → NO MATCH ("*" doesn't match multiple tokens) ├─ "*" → NO MATCH ("*" doesn't match multiple tokens) ├─ ">" → MATCH (">" matches everything) └─ "prod.users.acme" → NO MATCH (different tokens) Rules: ├─ "." separates tokens ├─ "*" matches exactly one token (anything except ".") ├─ ">" matches one or more tokens (only at end) ├─ Exact strings match exactly └─ Empty pattern matches nothing ``` --- ## Subject Sanitization ### Why Sanitization Is Needed NATS subject tokens have restrictions: - Cannot contain spaces: "tenant abc" ✗ - Cannot contain dots: "tenant.abc" (becomes two tokens) - Cannot contain wildcards: "tenant*abc" ✗ - Cannot contain ">": "tenant>abc" ✗ ### Sanitization Process ``` Input Namespace: "tenant.abc" Sanitization: Replace . with _ Output Stream Name: "tenant_abc_events" NATS Subject: "tenant_abc_events.events.order.order-123" Security Guarantee: └─ No way to inject NATS wildcards or special subjects via namespace ``` ### Sanitization Rules ```go func sanitizeSubject(s string) string { s = strings.ReplaceAll(s, " ", "_") // space → underscore s = strings.ReplaceAll(s, ".", "_") // dot → underscore s = strings.ReplaceAll(s, "*", "_") // star → underscore s = strings.ReplaceAll(s, ">", "_") // greater → underscore return s } ``` **Examples:** ``` "tenant abc" → "tenant_abc" "tenant.acme" → "tenant_acme" "tenant*acme" → "tenant_acme" "tenant>acme" → "tenant_acme" "prod.orders.v2" → "prod_orders_v2" ``` --- ## Value Objects ### Namespace ``` type Namespace string Characteristics: ├─ Identity: Defined by string value alone ├─ Equality: Namespace("a") == Namespace("a") ├─ Immutability: Cannot change (value type) ├─ Meaning: Application-defined (tenant, domain, environment, etc.) └─ Format: Alphanumeric + hyphens + dots (for hierarchies) Examples: ├─ "tenant-a" (simple) ├─ "tenant-123" (alphanumeric) ├─ "prod.orders" (hierarchical: domain.environment) ├─ "prod.us.east" (hierarchical: domain.env.region) └─ "acme.prod.orders" (hierarchical: tenant.env.domain) ``` ### SubjectPattern ``` type SubjectPattern string Characteristics: ├─ NATS-native patterns (*, >) ├─ Dot-separated tokens ├─ Wildcards bypass isolation (documented) └─ Applied consistently via MatchNamespacePattern() Examples: ├─ "tenant-a" (exact: isolation enforced) ├─ "*" (single-token wildcard) ├─ "prod.*" (match prod.X) ├─ "prod.>" (match prod.X.Y.Z) └─ ">" (match all: auditing only) ``` --- ## Commands and Their Effects ### DefineNamespace ``` Input: namespace string └─ Validates format (application responsibility) └─ Creates logical boundary identifier Result: Namespace can be used in: ├─ NewJetStreamEventStoreWithNamespace(conn, "events", namespace) ├─ EventBus.Subscribe(namespace) └─ EventBus.Publish(namespace, event) ``` ### PublishToNamespace ``` Input: namespace string, event *Event └─ Validates event structure Processing: ├─ EventBus.Publish() │ ├─ Deliver to exactSubscribers[namespace] │ └─ Deliver to matching wildcard subscribers └─ NATSEventBus.Publish() └─ Publish to NATS subject "aether.events.{namespace}" Result: Event delivered to all subscribers matching the namespace ``` ### SubscribeToNamespace ``` Input: pattern string, filter *SubscriptionFilter └─ Validates pattern format Processing: ├─ EventBus.SubscribeWithFilter() │ ├─ If pattern has wildcards: add to wildcardSubscribers │ └─ Else: add to exactSubscribers[pattern] └─ NATSEventBus.SubscribeWithFilter() └─ Create NATS subscription to "aether.events.{pattern}" Result: Channel created that receives matching events (filtered by EventTypes and ActorPattern if provided) ``` --- ## Policies ### Policy 1: Namespace Event Routing ``` Trigger: publish(namespace, event) ──────────────────────────────── Action: 1. Deliver to exactSubscribers[namespace] 2. Deliver to all wildcardSubscribers where pattern matches namespace Filter Application: ├─ EventTypes: Event must have one of the specified types ├─ ActorPattern: Event.ActorID must match pattern └─ Logic: AND (both must match if both specified) Invariant Enforced: └─ Namespace Boundary Isolation (events in namespace X don't leak to namespace Y, except through intentional wildcard subscriptions) ``` ### Policy 2: NATS Subject Namespacing ``` Trigger: NATSEventBus.Publish(namespace, event) ────────────────────────────────────────────── Action: Format subject with namespace prefix └─ subject := fmt.Sprintf("aether.events.%s", namespace) Example: ├─ namespace = "tenant-a" └─ subject = "aether.events.tenant-a" Purpose: └─ Ensures cross-node events respect namespace boundaries (NATS natively routes aether.events.tenant-a separately from aether.events.tenant-b) ``` ### Policy 3: Storage Stream Isolation ``` Trigger: NewJetStreamEventStoreWithNamespace(conn, streamName, namespace) ────────────────────────────────────────────────────────────────────── Action: Create separate JetStream stream per namespace └─ effectiveStreamName := fmt.Sprintf("%s_%s", sanitizeSubject(namespace), streamName) → "tenant-a_events", "tenant-b_events", etc. Purpose: ├─ Complete storage-layer isolation (separate NATS streams) ├─ GetEvents(store1) cannot see events from GetEvents(store2) └─ Prevents accidental data leakage even if EventBus logic fails Invariant Enforced: └─ Namespace Boundary Isolation (storage layer) ``` --- ## Failure Scenarios ### Scenario 1: Attempting Cross-Namespace Read ``` Application Code: ───────────────── store1 := NewJetStreamEventStoreWithNamespace(conn, "events", "tenant-a") store2 := NewJetStreamEventStoreWithNamespace(conn, "events", "tenant-b") store1.SaveEvent(Event{ActorID: "order-123", ...}) store2.GetEvents("order-123", 0) Expected Result: ├─ store2 queries its own stream "tenant_b_events" └─ Returns: empty (order-123 was not saved in tenant_b_events) Isolation Enforced: ✓ ``` ### Scenario 2: Wildcard Subscription Receives Cross-Namespace ``` Application Code: ───────────────── bus := NewEventBus() chWildcard := bus.Subscribe(">") // CAUTION: bypass isolation! chExact := bus.Subscribe("tenant-a") // isolation enforced bus.Publish("tenant-a", event1) bus.Publish("tenant-b", event2) Result: ├─ chExact ← event1 ✓ (only tenant-a) ├─ chWildcard ← event1 ✓ (matches ">") ├─ chWildcard ← event2 ✓ (matches ">") └─ Isolation bypassed intentionally for auditing Risk: chWildcard should only be used by trusted auditing code ``` ### Scenario 3: Invalid Namespace (Current Behavior) ``` Current Implementation: ───────────────────── namespace := "prod.orders" // Contains dot (invalid NATS token) store := NewJetStreamEventStoreWithNamespace(conn, "events", namespace) Behavior: └─ sanitizeSubject("prod.orders") → "prod_orders" → Stream: "prod_orders_events" (dot replaced with underscore) Problem: Silent transformation └─ Developer thinks they're using "prod.orders" └─ Storage actually uses "prod_orders" └─ No validation error, behavior change is implicit Future (Proposed): ────────────────── Behavior: └─ ValidateNamespace("prod.orders") → Error: "namespace contains invalid characters" → Application must fix: use "prod_orders" or define hierarchical format Benefit: Explicit error > silent transformation ``` --- ## Testing Strategy ### Unit Tests (Existing) ``` ✓ pattern_test.go - MatchNamespacePattern() with various patterns - IsWildcardPattern() detection ✓ namespace_test.go - sanitizeSubject() character replacement - Stream naming with/without namespace - Actor type extraction ``` ### Integration Tests (Needed) ``` ✗ store_isolation_test.go - SaveEvent(store1, ns1) → GetEvents(store2, ns2) returns empty - Different streams have completely isolated data ✗ eventbus_isolation_test.go - Publish(ns1) → Subscribe(ns2) receives nothing - Publish(ns1) → Subscribe(wildcard) receives it ✗ cross_node_isolation_test.go - Node1 Publish("tenant-a") → Node2 Subscribe("tenant-b") blocked - Node1 Publish("prod.orders") → Node2 Subscribe("prod.*") receives it ✗ pattern_matching_test.go - Complex hierarchical patterns verified - Edge cases: dots, prefixes, suffixes ``` --- ## Summary: Invariants and Their Enforcement | Invariant | Enforced At | Mechanism | |-----------|-------------|-----------| | **Namespace Boundary Isolation** | EventBus, JetStream, NATS | exactSubscribers per namespace; separate streams; subject routing | | **Namespace Name Safety** | JetStream SaveEvent | sanitizeSubject() prevents NATS injection | | **Wildcard Subscriptions Bypass Isolation** | EventBus logic | Separate wildcardSubscribers list; documented in comments | | **Subject Pattern Matching Consistency** | MatchNamespacePattern() | Token-based matching with * and > | **Result:** Three-layer isolation (memory, storage, network) with intentional, documented exceptions for trusted components.