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.

Reply via email to