This is an automated email from the ASF dual-hosted git repository. mattisonchao pushed a commit to branch pip-455-async-resource-list-filtering in repository https://gitbox.apache.org/repos/asf/pulsar.git
commit 688e28da84f636e3a7d0a3a12d8f5f1879e4234e Author: mattisonchao <[email protected]> AuthorDate: Thu Mar 5 01:07:20 2026 +0800 [improve][broker] PIP-455: Add Async Resource List Filtering API to AuthorizationProvider Co-Authored-By: Claude Opus 4.6 <[email protected]> --- pip/pip-455.md | 144 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/pip/pip-455.md b/pip/pip-455.md new file mode 100644 index 00000000000..9c887a39ed0 --- /dev/null +++ b/pip/pip-455.md @@ -0,0 +1,144 @@ +# PIP-455: Add Async Resource List Filtering API to AuthorizationProvider + +## Motivation + +Currently, Pulsar's list operations (list tenants, namespaces, clusters, topics) use an all-or-nothing authorization model. If the user is authorized for the LIST operation (e.g., `TenantOperation.LIST_TENANTS`), they see all resources; otherwise they get a 403 error. There is no way for an `AuthorizationProvider` to filter list results per-item — for example, only returning tenants or namespaces that the user has access to. + +Users who need per-item filtering today must rely on a JAX-RS `ContainerResponseFilter`. However, the `ContainerResponseFilter.filter()` method is synchronous (returns `void`), so any authorization check that requires metadata access must block the calling thread. When `asyncResponse.resume()` is executed on the metadata thread (or the web executor thread), blocking metadata operations in a response filter can exhaust the thread pool and cause deadlocks. + +This PIP proposes adding a default method to `AuthorizationProvider` that allows async per-item filtering of list results, called inside the endpoint method where async execution is natural. + +## Goal + +Provide a pluggable, async-safe mechanism for `AuthorizationProvider` implementations to filter resources returned by list operations (clusters, tenants, namespaces, topics) without blocking any thread pool. + +### In Scope + +- New default method on `AuthorizationProvider` for async resource filtering +- A `FilterContext` class to carry resource type and parent resource information +- Integration into the list endpoints for clusters, tenants, namespaces, and topics + +### Out of Scope + +- Changing the existing authorization check model (the all-or-nothing gate remains) +- Providing a built-in filtering implementation in `PulsarAuthorizationProvider` (this PIP only adds the extension point) + +## Public Interfaces + +### New `ResourceType` enum + +```java +public enum ResourceType { + CLUSTER, + TENANT, + NAMESPACE, + TOPIC +} +``` + +### New `FilterContext` class + +```java +public class FilterContext { + private final ResourceType resourceType; + private final String parent; // e.g., tenant name when listing namespaces, + // namespace name when listing topics, + // null when listing tenants or clusters +} +``` + +### New default method on `AuthorizationProvider` + +```java +/** + * Filter a list of resources based on authorization. + * + * <p>Called after a list operation (e.g., list tenants, list namespaces) to allow + * the authorization provider to filter results per-item. The default implementation + * returns the full list without filtering. + * + * @param context the filter context containing resource type and parent resource + * @param resources the list of resource names to filter + * @param role the role requesting the list + * @param authData authentication data for the role + * @return a CompletableFuture containing the filtered list of resource names + */ +default CompletableFuture<List<String>> filterAsync( + FilterContext context, List<String> resources, String role, + AuthenticationDataSource authData) { + return CompletableFuture.completedFuture(resources); +} +``` + +The default implementation returns the full list (no filtering), preserving backward compatibility. Custom `AuthorizationProvider` implementations can override this to implement per-item authorization filtering. + +## Proposed Changes + +### Integration into list endpoints + +The `filterAsync` method will be called in the async chain of each list endpoint, after the list is retrieved from the metadata store and before `asyncResponse.resume()`: + +**TenantsBase.getTenants():** +```java +tenantResources().listTenantsAsync() + .thenCompose(tenants -> authorizationService.filterAsync( + new FilterContext(ResourceType.TENANT, null), + tenants, role, authData)) + .thenAcceptAsync(filtered -> { + List<String> deepCopy = new ArrayList<>(filtered); + deepCopy.sort(null); + asyncResponse.resume(deepCopy); + }, pulsar().getWebService().getWebServiceExecutor()) +``` + +**ClustersBase.getClusters():** +```java +clusterResources().listAsync() + .thenCompose(clusters -> authorizationService.filterAsync( + new FilterContext(ResourceType.CLUSTER, null), + clusters, role, authData)) + .thenAcceptAsync(filtered -> asyncResponse.resume( + filtered.stream() + .filter(c -> !Constants.GLOBAL_CLUSTER.equals(c)) + .collect(Collectors.toSet())), + pulsar().getWebService().getWebServiceExecutor()) +``` + +**Namespaces.getTenantNamespaces():** +```java +tenantResources().getListOfNamespacesAsync(tenant) + .thenCompose(namespaces -> authorizationService.filterAsync( + new FilterContext(ResourceType.NAMESPACE, tenant), + namespaces, role, authData)) + .thenAcceptAsync(response::resume, + pulsar().getWebService().getWebServiceExecutor()) +``` + +**Topics list endpoints** would follow the same pattern with `ResourceType.TOPIC` and the namespace as the parent. + +### Authorization bypass + +When authorization is disabled (`authorizationEnabled=false`), the `filterAsync` call should be skipped entirely to avoid unnecessary overhead. + +## Compatibility, Deprecation, and Migration Plan + +- **Backward compatible**: The new method is a `default` method on the `AuthorizationProvider` interface, returning the full list by default. Existing custom implementations will continue to work without changes. +- **No deprecation**: No existing APIs are deprecated. +- **Migration**: Users who currently use `ContainerResponseFilter` for list filtering can migrate to overriding `filterAsync` in their custom `AuthorizationProvider` to avoid thread-blocking issues. + +## Test Plan + +- Unit tests verifying the default implementation returns the full list +- Unit tests verifying a custom implementation can filter list results +- Integration tests for each list endpoint (clusters, tenants, namespaces, topics) with a filtering `AuthorizationProvider` +- Test that `filterAsync` is skipped when authorization is disabled + +## Rejected Alternatives + +### Per-resource-type methods (e.g., `filterTenantsAsync`, `filterNamespacesAsync`) + +Using separate methods for each resource type would require adding a new method every time a new filterable resource type is introduced. A single method with `FilterContext` is more extensible. + +### Using `ContainerResponseFilter` + +The JAX-RS `ContainerResponseFilter` API is synchronous and cannot perform async authorization checks without blocking the calling thread. This leads to thread pool exhaustion and potential deadlocks when the filter needs to access metadata.
