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>
329 lines
10 KiB
Markdown
329 lines
10 KiB
Markdown
# 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
|