arunsrajan commented on code in PR #26010: URL: https://github.com/apache/camel/pull/26010#discussion_r3913329482
########## components/camel-alibaba/camel-alibaba-eventbridge/src/main/java/org/apache/camel/component/alibaba/eventbridge/EventSourceCache.java: ########## @@ -0,0 +1,362 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.alibaba.eventbridge; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import com.aliyun.eventbridge.EventBridgeClient; +import com.aliyun.eventbridge.models.EventBusEntry; +import com.aliyun.eventbridge.models.EventRuleDTO; +import com.aliyun.eventbridge.models.ListEventBusesRequest; +import com.aliyun.eventbridge.models.ListEventBusesResponse; +import com.aliyun.eventbridge.models.ListRulesRequest; +import com.aliyun.eventbridge.models.ListRulesResponse; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.apache.camel.component.alibaba.eventbridge.models.AllowedEventBus; +import org.apache.camel.component.alibaba.eventbridge.models.AllowedEventSource; +import org.apache.camel.util.ObjectHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * TTL-based in-memory cache for verified Alibaba Cloud EventBridge bus, event source, and source-scoped event type + * definitions. + * <p> + * Implements a two-phase validated cache workflow: + * <ol> + * <li>Fetch and validate configured event sources and event types against Alibaba Cloud API ({@code listEventBuses} and + * {@code listRules}).</li> + * <li><b>Only after validation passes</b>, populate the cache with the verified {@link BusMetadata} for fast runtime + * comparison.</li> + * </ol> + */ +final class EventSourceCache { + + private static final Logger LOG = LoggerFactory.getLogger(EventSourceCache.class); + private static final Gson GSON = new Gson(); + private static final int PAGE_LIMIT = 100; + + /** + * Java 16 record holding cached bus metadata including mapped event sources and their permitted event types. + */ + public record BusMetadata(boolean exists, Map<String, Set<String>> sourceToTypesMap) { + public BusMetadata { + if (sourceToTypesMap == null) { + sourceToTypesMap = Collections.emptyMap(); + } else { + Map<String, Set<String>> unmodifiable = new HashMap<>(); + for (Map.Entry<String, Set<String>> entry : sourceToTypesMap.entrySet()) { + unmodifiable.put(entry.getKey(), Collections.unmodifiableSet(new HashSet<>(entry.getValue()))); + } + sourceToTypesMap = Collections.unmodifiableMap(unmodifiable); + } + } + + public boolean isKnownSource(String source) { + if (source == null || sourceToTypesMap.isEmpty()) { + return true; + } + return sourceToTypesMap.containsKey(source.trim()) || sourceToTypesMap.containsKey("*"); + } + + public boolean isKnownType(String source, String eventType) { + if (eventType == null || sourceToTypesMap.isEmpty()) { + return true; + } + Set<String> types = sourceToTypesMap.get(source != null ? source.trim() : null); + if (types == null || types.isEmpty()) { + types = sourceToTypesMap.get("*"); + } + if (types == null || types.isEmpty()) { + return true; + } + return types.contains(eventType.trim()); + } + } + + /** + * Java 16 record representing a TTL-aware cache entry. + */ + public record CacheEntry<T>(T value, long expiryTime) { + public boolean isExpired(long now) { + return now >= expiryTime; + } + } + + private final Map<String, CacheEntry<BusMetadata>> cache = new ConcurrentHashMap<>(); + private final long ttlMillis; + + EventSourceCache(long ttlMillis) { + this.ttlMillis = ttlMillis; + } + + /** + * Validates the given {@code eventBusName} and any configured {@link AllowedEventBus} definitions against Alibaba + * Cloud. Upon successful validation, the verified metadata is stored in the cache. + * + * @param eventBusName the target event bus name + * @param allowedBus the configured whitelist rules for this bus, or {@code null} + * @param validateSource whether to validate event source existence against Alibaba Cloud + * @param validateType whether to validate event types against Alibaba Cloud rule filter patterns + * @param client the EventBridge client instance; if {@code null}, validation is bypassed + * @return the verified {@link BusMetadata} + */ + BusMetadata validateAndUpdateCache( + String eventBusName, AllowedEventBus allowedBus, + boolean validateSource, boolean validateType, EventBridgeClient client) { + if (client == null || ObjectHelper.isEmpty(eventBusName)) { + return new BusMetadata(true, Collections.emptyMap()); + } + + long now = System.currentTimeMillis(); + CacheEntry<BusMetadata> entry = cache.get(eventBusName); + if (entry != null && !entry.isExpired(now)) { + return entry.value(); + } + + boolean busExists = fetchEventBusExists(eventBusName, client); + if (!busExists) { + throw new IllegalArgumentException( + String.format("Event bus '%s' does not exist in Alibaba Cloud EventBridge", eventBusName)); + } + + Map<String, Set<String>> cloudSourceToTypes = fetchCloudSourceToTypes(eventBusName, client); + + if (validateSource && allowedBus != null && !allowedBus.allowedSources().isEmpty() && !cloudSourceToTypes.isEmpty()) { + for (String source : allowedBus.allowedSources().keySet()) { + if (!cloudSourceToTypes.containsKey(source) && !cloudSourceToTypes.containsKey("*")) { + throw new IllegalArgumentException( + String.format( + "Event source '%s' is not registered in Alibaba Cloud rules for event bus '%s'. Known sources: %s", + source, eventBusName, cloudSourceToTypes.keySet())); + } + } + } + + if (validateType && allowedBus != null && !allowedBus.allowedSources().isEmpty() && !cloudSourceToTypes.isEmpty()) { + for (Map.Entry<String, AllowedEventSource> sourceEntry : allowedBus.allowedSources().entrySet()) { + String source = sourceEntry.getKey(); + Set<String> allowedTypes = sourceEntry.getValue().allowedEventTypes(); + if (allowedTypes != null && !allowedTypes.isEmpty()) { + Set<String> cloudTypes = cloudSourceToTypes.get(source); + if (cloudTypes == null || cloudTypes.isEmpty()) { + cloudTypes = cloudSourceToTypes.get("*"); + } + if (cloudTypes != null && !cloudTypes.isEmpty()) { + for (String type : allowedTypes) { + if (!cloudTypes.contains(type)) { + throw new IllegalArgumentException( + String.format( + "Event type '%s' is not registered in Alibaba Cloud rules for source '%s' on bus '%s'. Allowed in Cloud: %s", + type, source, eventBusName, cloudTypes)); + } + } + } + } + } + } + + BusMetadata metadata = new BusMetadata(true, cloudSourceToTypes); + cache.put(eventBusName, new CacheEntry<>(metadata, now + ttlMillis)); + return metadata; + } + + /** + * Checks if the event bus is known to exist. + */ + boolean isKnownEventBus(String eventBusName, EventBridgeClient client) { + if (client == null || ObjectHelper.isEmpty(eventBusName)) { + return true; + } + long now = System.currentTimeMillis(); + CacheEntry<BusMetadata> entry = cache.get(eventBusName); + if (entry != null && !entry.isExpired(now)) { + return entry.value().exists(); + } + return fetchEventBusExists(eventBusName, client); + } + + /** + * Checks if the event source is registered for the given bus. + */ + boolean isKnownEventSource(String eventBusName, String eventSource, EventBridgeClient client) { + if (client == null || ObjectHelper.isEmpty(eventBusName) || ObjectHelper.isEmpty(eventSource)) { + return true; + } + BusMetadata metadata = validateAndUpdateCache(eventBusName, null, false, false, client); + return metadata.isKnownSource(eventSource); + } + + /** + * Checks if the event type is valid for the given event source on the bus. + */ + boolean isKnownEventType(String eventBusName, String eventSource, String eventType, EventBridgeClient client) { + if (client == null || ObjectHelper.isEmpty(eventBusName) || ObjectHelper.isEmpty(eventType)) { + return true; + } + BusMetadata metadata = validateAndUpdateCache(eventBusName, null, false, false, client); + return metadata.isKnownType(eventSource, eventType); + } + + /** + * Fetches whether the event bus exists via {@code listEventBuses}. + */ + boolean fetchEventBusExists(String eventBusName, EventBridgeClient client) { + try { Review Comment: Fixed in the latest commit. `fetchEventBusExists` and `fetchCloudRules` now operate strictly **fail-closed**. If an exception occurs when communicating with Alibaba Cloud EventBridge (network errors, permission errors, rate limiting, etc.), the error is logged and an `IllegalArgumentException` is thrown to ensure unvalidated event buses are never silently treated as valid during outages. Verified with unit test `testCloudApiFailureFailsClosedOnBusCheck`. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
