Organize all product strategy and domain modeling documentation into a dedicated .product-strategy directory for better separation from code. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
425 lines
15 KiB
Markdown
425 lines
15 KiB
Markdown
# 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.
|