Support NATS-style wildcard patterns ("*" and ">") for subscribing
to events across multiple namespaces. This enables cross-cutting
concerns like logging, monitoring, and auditing without requiring
separate subscriptions for each namespace.
- Add pattern.go with MatchNamespacePattern and IsWildcardPattern
- Update EventBus to track wildcard subscribers separately
- Update NATSEventBus to use NATS native wildcard support
- Add comprehensive tests for pattern matching and EventBus wildcards
- Document security implications in all relevant code comments
Closes #20
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
84 lines
2.8 KiB
Go
84 lines
2.8 KiB
Go
package aether
|
|
|
|
import "strings"
|
|
|
|
// MatchNamespacePattern checks if a namespace matches a pattern.
|
|
// Patterns follow NATS subject matching conventions where tokens are separated by dots:
|
|
// - "*" matches exactly one token (any sequence without ".")
|
|
// - ">" matches one or more tokens (only valid at the end of a pattern)
|
|
// - Exact strings match exactly
|
|
//
|
|
// Examples:
|
|
// - "tenant-a" matches "tenant-a" (exact match)
|
|
// - "*" matches any single-token namespace like "tenant-a" or "production"
|
|
// - ">" matches any namespace with one or more tokens
|
|
// - "prod.*" matches "prod.tenant", "prod.orders" (but not "prod.tenant.orders")
|
|
// - "prod.>" matches "prod.tenant", "prod.tenant.orders", "prod.a.b.c"
|
|
// - "*.tenant.*" matches "prod.tenant.orders", "staging.tenant.events"
|
|
//
|
|
// Security Considerations:
|
|
// Wildcard subscriptions provide cross-namespace visibility. Use with caution:
|
|
// - "*" or ">" patterns receive events from ALL matching namespaces
|
|
// - This bypasses namespace isolation for the subscriber
|
|
// - Only grant wildcard subscription access to trusted system components
|
|
// - Consider auditing wildcard subscription usage
|
|
// - For multi-tenant systems, wildcard access should be restricted to admin/ops
|
|
// - Use the most specific pattern possible to minimize exposure
|
|
func MatchNamespacePattern(pattern, namespace string) bool {
|
|
// Empty pattern matches nothing
|
|
if pattern == "" {
|
|
return false
|
|
}
|
|
|
|
// ">" matches everything when used alone
|
|
if pattern == ">" {
|
|
return namespace != ""
|
|
}
|
|
|
|
patternTokens := strings.Split(pattern, ".")
|
|
namespaceTokens := strings.Split(namespace, ".")
|
|
|
|
return matchTokens(patternTokens, namespaceTokens)
|
|
}
|
|
|
|
// matchTokens recursively matches pattern tokens against namespace tokens
|
|
func matchTokens(patternTokens, namespaceTokens []string) bool {
|
|
// If pattern is exhausted, namespace must also be exhausted
|
|
if len(patternTokens) == 0 {
|
|
return len(namespaceTokens) == 0
|
|
}
|
|
|
|
patternToken := patternTokens[0]
|
|
|
|
// ">" matches one or more remaining tokens (must be last pattern token)
|
|
if patternToken == ">" {
|
|
// ">" requires at least one token to match
|
|
return len(namespaceTokens) >= 1
|
|
}
|
|
|
|
// If namespace is exhausted but pattern has more tokens, no match
|
|
if len(namespaceTokens) == 0 {
|
|
return false
|
|
}
|
|
|
|
namespaceToken := namespaceTokens[0]
|
|
|
|
// "*" matches exactly one token
|
|
if patternToken == "*" {
|
|
return matchTokens(patternTokens[1:], namespaceTokens[1:])
|
|
}
|
|
|
|
// Exact match required
|
|
if patternToken == namespaceToken {
|
|
return matchTokens(patternTokens[1:], namespaceTokens[1:])
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// IsWildcardPattern returns true if the pattern contains wildcards (* or >).
|
|
// Wildcard patterns can match multiple namespaces and bypass namespace isolation.
|
|
func IsWildcardPattern(pattern string) bool {
|
|
return strings.Contains(pattern, "*") || strings.Contains(pattern, ">")
|
|
}
|