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, ">") } // SubscriptionFilter defines optional filters for event subscriptions. // All configured filters are combined with AND logic - an event must match // all specified criteria to be delivered to the subscriber. // // Filter Processing: // - EventTypes: Event must have an EventType matching at least one in the list (OR within types) // - ActorPattern: Event's ActorID must match the pattern (supports * and > wildcards) // // Filtering is applied client-side in the EventBus. For NATSEventBus, namespace-level // filtering uses NATS subject patterns, while EventTypes and ActorPattern filtering // happens after message receipt. type SubscriptionFilter struct { // EventTypes filters events by type. Empty slice means all event types. // If specified, only events with an EventType in this list are delivered. // Example: []string{"OrderPlaced", "OrderShipped"} receives only those event types. EventTypes []string // ActorPattern filters events by actor ID pattern. Empty string means all actors. // Supports NATS-style wildcards: // - "*" matches a single token (e.g., "order-*" matches "order-123", "order-456") // - ">" matches one or more tokens (e.g., "order.>" matches "order.us.123", "order.eu.456") // Example: "order-*" receives events only for actors starting with "order-" ActorPattern string } // IsEmpty returns true if no filters are configured. func (f *SubscriptionFilter) IsEmpty() bool { return len(f.EventTypes) == 0 && f.ActorPattern == "" } // Matches returns true if the event matches all configured filters. // An empty filter matches all events. func (f *SubscriptionFilter) Matches(event *Event) bool { if event == nil { return false } // Check event type filter if len(f.EventTypes) > 0 { typeMatch := false for _, et := range f.EventTypes { if event.EventType == et { typeMatch = true break } } if !typeMatch { return false } } // Check actor pattern filter if f.ActorPattern != "" { if !MatchActorPattern(f.ActorPattern, event.ActorID) { return false } } return true } // MatchActorPattern checks if an actor ID matches a pattern. // Uses the same matching logic as MatchNamespacePattern for consistency. // // Patterns: // - "*" matches a single token (e.g., "order-*" matches "order-123") // - ">" matches one or more tokens (e.g., "order.>" matches "order.us.east") // - Exact strings match exactly (e.g., "order-123" matches only "order-123") // // Note: For simple prefix matching without dots (e.g., "order-*" matching "order-123"), // this uses simplified matching where "*" matches any remaining characters in a token. func MatchActorPattern(pattern, actorID string) bool { // Empty pattern matches nothing if pattern == "" { return false } // Empty actor ID matches nothing except ">" if actorID == "" { return false } // If pattern contains dots, use token-based matching (same as namespace) if strings.Contains(pattern, ".") || strings.Contains(actorID, ".") { return MatchNamespacePattern(pattern, actorID) } // Simple matching for non-tokenized patterns // ">" matches any non-empty actor ID if pattern == ">" { return true } // "*" matches any single-token actor ID (no dots) if pattern == "*" { return true } // Check for suffix wildcard (e.g., "order-*") if strings.HasSuffix(pattern, "*") { prefix := strings.TrimSuffix(pattern, "*") return strings.HasPrefix(actorID, prefix) } // Check for suffix multi-match (e.g., "order->") if strings.HasSuffix(pattern, ">") { prefix := strings.TrimSuffix(pattern, ">") return strings.HasPrefix(actorID, prefix) } // Exact match return pattern == actorID }