From 688e28da84f636e3a7d0a3a12d8f5f1879e4234e Mon Sep 17 00:00:00 2001 From: mattisonchao Date: Thu, 5 Mar 2026 01:07:20 +0800 Subject: [PATCH 1/4] [improve][broker] PIP-455: Add Async Resource List Filtering API to AuthorizationProvider Co-Authored-By: Claude Opus 4.6 --- pip/pip-455.md | 144 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 pip/pip-455.md diff --git a/pip/pip-455.md b/pip/pip-455.md new file mode 100644 index 0000000000000..9c887a39ed0d4 --- /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. + * + *

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> filterAsync( + FilterContext context, List 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 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. From 256c4a6c8300ce76a200dfa893e599c492e4d1ce Mon Sep 17 00:00:00 2001 From: mattisonchao Date: Thu, 5 Mar 2026 01:12:44 +0800 Subject: [PATCH 2/4] Update PIP-455: simplify FilterContext and remove web executor usage - Remove parent field from FilterContext (resource names already contain hierarchy) - Remove sort from filter example (caller's responsibility) - Use thenAccept instead of thenAcceptAsync since filterAsync already offloads from metadata thread Co-Authored-By: Claude Opus 4.6 --- pip/pip-455.md | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/pip/pip-455.md b/pip/pip-455.md index 9c887a39ed0d4..c44cef90e24d6 100644 --- a/pip/pip-455.md +++ b/pip/pip-455.md @@ -41,9 +41,6 @@ public enum ResourceType { ```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 } ``` @@ -57,7 +54,7 @@ public class FilterContext { * 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 context the filter context containing resource type * @param resources the list of resource names to filter * @param role the role requesting the list * @param authData authentication data for the role @@ -82,39 +79,33 @@ The `filterAsync` method will be called in the async chain of each list endpoint ```java tenantResources().listTenantsAsync() .thenCompose(tenants -> authorizationService.filterAsync( - new FilterContext(ResourceType.TENANT, null), + new FilterContext(ResourceType.TENANT), tenants, role, authData)) - .thenAcceptAsync(filtered -> { - List deepCopy = new ArrayList<>(filtered); - deepCopy.sort(null); - asyncResponse.resume(deepCopy); - }, pulsar().getWebService().getWebServiceExecutor()) + .thenAccept(asyncResponse::resume) ``` **ClustersBase.getClusters():** ```java clusterResources().listAsync() .thenCompose(clusters -> authorizationService.filterAsync( - new FilterContext(ResourceType.CLUSTER, null), + new FilterContext(ResourceType.CLUSTER), clusters, role, authData)) - .thenAcceptAsync(filtered -> asyncResponse.resume( + .thenAccept(filtered -> asyncResponse.resume( filtered.stream() .filter(c -> !Constants.GLOBAL_CLUSTER.equals(c)) - .collect(Collectors.toSet())), - pulsar().getWebService().getWebServiceExecutor()) + .collect(Collectors.toSet()))) ``` **Namespaces.getTenantNamespaces():** ```java tenantResources().getListOfNamespacesAsync(tenant) .thenCompose(namespaces -> authorizationService.filterAsync( - new FilterContext(ResourceType.NAMESPACE, tenant), + new FilterContext(ResourceType.NAMESPACE), namespaces, role, authData)) - .thenAcceptAsync(response::resume, - pulsar().getWebService().getWebServiceExecutor()) + .thenAccept(response::resume) ``` -**Topics list endpoints** would follow the same pattern with `ResourceType.TOPIC` and the namespace as the parent. +**Topics list endpoints** would follow the same pattern with `ResourceType.TOPIC`. ### Authorization bypass From e5335fe6c38ea4f8fdf3b54a9fc6761974a89eaa Mon Sep 17 00:00:00 2001 From: mattisonchao Date: Thu, 5 Mar 2026 01:13:38 +0800 Subject: [PATCH 3/4] Rename PIP-455 to PIP-458 Co-Authored-By: Claude Opus 4.6 --- pip/{pip-455.md => pip-458.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename pip/{pip-455.md => pip-458.md} (98%) diff --git a/pip/pip-455.md b/pip/pip-458.md similarity index 98% rename from pip/pip-455.md rename to pip/pip-458.md index c44cef90e24d6..8ba5ce21d2940 100644 --- a/pip/pip-455.md +++ b/pip/pip-458.md @@ -1,4 +1,4 @@ -# PIP-455: Add Async Resource List Filtering API to AuthorizationProvider +# PIP-458: Add Async Resource List Filtering API to AuthorizationProvider ## Motivation From e3cfa265534ea4046a1d29eeaa9d94fae4042213 Mon Sep 17 00:00:00 2001 From: mattisonchao Date: Tue, 24 Mar 2026 15:42:39 +0800 Subject: [PATCH 4/4] [improve][pip] PIP-458: Refine proposal with enriched FilterContext, template alignment, and detailed design - Restructured to follow the official PIP template (Background, High Level Design, Detailed Design, etc.) - Added parentResource field to FilterContext for namespace/topic filtering context - Added resource name format documentation table - Added AuthorizationService delegation method (was missing) - Documented interaction with existing authorization gates - Added Security Considerations and Geo-Replication sections - Added Performance Considerations section - Strengthened test plan with specific test categories - Added rejected alternative: replacing auth gate with per-item filtering - Fixed integration examples to match actual codebase - Marked status as Draft --- pip/pip-458.md | 273 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 211 insertions(+), 62 deletions(-) diff --git a/pip/pip-458.md b/pip/pip-458.md index 8ba5ce21d2940..0382b7685af24 100644 --- a/pip/pip-458.md +++ b/pip/pip-458.md @@ -1,31 +1,125 @@ # PIP-458: Add Async Resource List Filtering API to AuthorizationProvider -## Motivation +*Status: Draft* -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. +# Background knowledge -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. +Pulsar's `AuthorizationProvider` is a pluggable interface (`pulsar-broker-common`) that brokers use to make authorization decisions. It exposes async methods for checking permissions on tenants, namespaces, topics, and clusters (e.g., `allowTenantOperationAsync`, `allowNamespaceOperationAsync`). `AuthorizationService` wraps this provider and adds a guard on the `authorizationEnabled` configuration flag before delegating. + +Pulsar's Admin REST API exposes list endpoints for clusters, tenants, namespaces, and topics. These endpoints are async — they retrieve data from the metadata store, apply post-processing, and call `asyncResponse.resume()` to return the result. Most list endpoints perform an all-or-nothing authorization check before returning the full list (e.g., `TenantOperation.LIST_TENANTS`, `NamespaceOperation.GET_TOPICS`). + +JAX-RS provides a `ContainerResponseFilter` hook that runs after the endpoint returns. Its `filter()` method is synchronous (returns `void`), which makes it unsuitable for any authorization logic that needs to access metadata asynchronously. + +# Motivation + +Currently, if a user is authorized for a LIST operation, they see **all** resources; otherwise they get a 403. 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 use a `ContainerResponseFilter`. However, because `filter()` is synchronous and `asyncResponse.resume()` may execute on the metadata thread or 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 +# Goals + +## In Scope + +- New default method on `AuthorizationProvider` for async resource filtering. +- A `FilterContext` class to carry resource type and parent resource information. +- A corresponding delegation method on `AuthorizationService` that respects the `authorizationEnabled` flag. +- 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). + +# High Level Design -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. +A new default method `filterAsync` is added to the `AuthorizationProvider` interface. It accepts a `FilterContext` (resource type + optional parent resource), the list of resource names, the user's role, and authentication data. It returns a `CompletableFuture>` containing the filtered list. -### In Scope +The default implementation returns the full list unchanged, so existing `AuthorizationProvider` implementations continue to work without modification. + +Each list endpoint (`getClusters`, `getTenants`, `getTenantNamespaces`, `getTopics`) inserts a `.thenCompose(resources -> authorizationService.filterAsync(...))` step into its existing async chain, after the list is retrieved and before `asyncResponse.resume()`. + +`AuthorizationService` wraps the call with the standard `authorizationEnabled` check — when authorization is disabled, the filtering step is skipped entirely. + +# Detailed Design + +## Design & Implementation Details + +### Interaction with existing authorization gates + +The existing all-or-nothing authorization check (e.g., `TenantOperation.LIST_TENANTS`, `NamespaceOperation.GET_TOPICS`) remains unchanged. `filterAsync` is invoked *after* the user passes the existing gate: + +- A user who fails the LIST permission check still receives a 403 — `filterAsync` is never called. +- A user who passes the LIST permission check will have their results filtered by `filterAsync`. + +This design keeps the existing security model intact. Replacing the gate with pure per-item filtering would change the security semantics (403 → empty list) and is out of scope. + +### Integration into list endpoints -- 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 +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 +validateBothSuperUserAndTenantOperation(null, TenantOperation.LIST_TENANTS) + .thenCompose(__ -> tenantResources().listTenantsAsync()) + .thenCompose(tenants -> authorizationService.filterAsync( + new FilterContext(ResourceType.TENANT), + tenants, clientAppId(), clientAuthData())) + .thenAccept(filtered -> { + List deepCopy = new ArrayList<>(filtered); + deepCopy.sort(null); + asyncResponse.resume(deepCopy); + }) +``` + +**ClustersBase.getClusters():** + +Note: The clusters list endpoint currently does not perform an authorization check. This PIP does not add one — `filterAsync` is still called to allow the provider to filter cluster names if desired. + +```java +clusterResources().listAsync() + .thenApply(clusters -> clusters.stream() + .filter(cluster -> !Constants.GLOBAL_CLUSTER.equals(cluster)) + .collect(Collectors.toList())) + .thenCompose(clusters -> authorizationService.filterAsync( + new FilterContext(ResourceType.CLUSTER), + clusters, clientAppId(), clientAuthData())) + .thenAccept(filtered -> asyncResponse.resume(new LinkedHashSet<>(filtered))) +``` + +**NamespacesBase.internalGetTenantNamespaces():** +```java +validateTenantOperationAsync(tenant, TenantOperation.LIST_NAMESPACES) + .thenCompose(__ -> tenantResources().tenantExistsAsync(tenant)) + .thenCompose(existed -> { + if (!existed) { + throw new RestException(Status.NOT_FOUND, "Tenant not found"); + } + return tenantResources().getListOfNamespacesAsync(tenant); + }) + .thenCompose(namespaces -> authorizationService.filterAsync( + new FilterContext(ResourceType.NAMESPACE, tenant), + namespaces, clientAppId(), clientAuthData())) +``` -### Out of Scope +**Namespaces.getTopics():** +```java +validateNamespaceOperationAsync(namespaceName, NamespaceOperation.GET_TOPICS) + .thenCompose(__ -> getNamespacePoliciesAsync(namespaceName)) + .thenCompose(policies -> internalGetListOfTopics(response, policies, mode)) + .thenApply(topics -> filterSystemTopic(topics, includeSystemTopic)) + .thenCompose(topics -> authorizationService.filterAsync( + new FilterContext(ResourceType.TOPIC, namespaceName.toString()), + topics, clientAppId(), clientAuthData())) + .thenAccept(response::resume) +``` -- 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-facing Changes -## Public Interfaces +### Public API -### New `ResourceType` enum +#### New `ResourceType` enum ```java public enum ResourceType { @@ -36,15 +130,55 @@ public enum ResourceType { } ``` -### New `FilterContext` class +#### New `FilterContext` class ```java public class FilterContext { private final ResourceType resourceType; + /** + * The parent resource under which the listed resources reside. + *

+ */ + private final String parentResource; + + public FilterContext(ResourceType resourceType) { + this(resourceType, null); + } + + public FilterContext(ResourceType resourceType, String parentResource) { + this.resourceType = resourceType; + this.parentResource = parentResource; + } + + public ResourceType getResourceType() { + return resourceType; + } + + public String getParentResource() { + return parentResource; + } } ``` -### New default method on `AuthorizationProvider` +#### Resource name formats + +The `resources` list passed to `filterAsync` uses the same format as the corresponding list endpoint's response: + +| ResourceType | Format | Example | +|---|---|---| +| `CLUSTER` | Short cluster name | `"us-east-1"` | +| `TENANT` | Short tenant name | `"my-tenant"` | +| `NAMESPACE` | `{tenant}/{namespace}` | `"my-tenant/my-namespace"` | +| `TOPIC` | Full topic URL | `"persistent://my-tenant/my-ns/my-topic"` | + +Implementations should be prepared to handle these formats when parsing resource names. + +#### New default method on `AuthorizationProvider` ```java /** @@ -54,7 +188,11 @@ public class FilterContext { * 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 + *

Implementations that perform per-item authorization checks should batch or + * parallelize checks where possible to avoid serializing N sequential RPCs, which + * could significantly increase latency for large resource lists. + * + * @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 @@ -69,67 +207,78 @@ default CompletableFuture> filterAsync( 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()`: +#### New delegation method on `AuthorizationService` -**TenantsBase.getTenants():** ```java -tenantResources().listTenantsAsync() - .thenCompose(tenants -> authorizationService.filterAsync( - new FilterContext(ResourceType.TENANT), - tenants, role, authData)) - .thenAccept(asyncResponse::resume) +public CompletableFuture> filterAsync( + FilterContext context, List resources, String role, + AuthenticationDataSource authData) { + if (!this.conf.isAuthorizationEnabled()) { + return CompletableFuture.completedFuture(resources); + } + return provider.filterAsync(context, resources, role, authData); +} ``` -**ClustersBase.getClusters():** -```java -clusterResources().listAsync() - .thenCompose(clusters -> authorizationService.filterAsync( - new FilterContext(ResourceType.CLUSTER), - clusters, role, authData)) - .thenAccept(filtered -> asyncResponse.resume( - filtered.stream() - .filter(c -> !Constants.GLOBAL_CLUSTER.equals(c)) - .collect(Collectors.toSet()))) -``` +# Security Considerations -**Namespaces.getTenantNamespaces():** -```java -tenantResources().getListOfNamespacesAsync(tenant) - .thenCompose(namespaces -> authorizationService.filterAsync( - new FilterContext(ResourceType.NAMESPACE), - namespaces, role, authData)) - .thenAccept(response::resume) -``` +- **No weakening of existing checks**: The all-or-nothing LIST permission gate remains. `filterAsync` adds an additional layer of filtering; it cannot grant access to resources that the existing gate would deny. +- **Multi-tenancy**: By design, `filterAsync` enables stricter tenant isolation — providers can ensure that one tenant cannot see another tenant's namespaces or topics, even if the caller has LIST permission. +- **Default is permissive**: The default no-op implementation returns the full list. Deployments that need filtering must explicitly opt in by providing a custom `AuthorizationProvider`. + +# Backward & Forward Compatibility -**Topics list endpoints** would follow the same pattern with `ResourceType.TOPIC`. +## Upgrade -### Authorization bypass +No special steps required. The new method is a `default` method on the `AuthorizationProvider` interface, returning the full list by default. Existing custom implementations continue to work without changes. -When authorization is disabled (`authorizationEnabled=false`), the `filterAsync` call should be skipped entirely to avoid unnecessary overhead. +## Downgrade / Rollback -## Compatibility, Deprecation, and Migration Plan +No special steps required. Rolling back to a version without `filterAsync` simply removes the filtering step — list endpoints return their full, unfiltered results as before. -- **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. +## Pulsar Geo-Replication Upgrade & Downgrade/Rollback Considerations -## Test Plan +No impact. The filtering is applied at the REST API layer in each broker independently. It does not affect replication state, topic metadata, or cross-cluster communication. -- 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 +# Performance Considerations -## Rejected Alternatives +The `filterAsync` method is invoked on every list request, so implementations should be mindful of performance: -### Per-resource-type methods (e.g., `filterTenantsAsync`, `filterNamespacesAsync`) +- **Batch authorization checks**: Rather than issuing N sequential authorization RPCs for N resources, implementations should parallelize checks (e.g., using `CompletableFuture.allOf`) or use a batch API if available. +- **Caching**: For deployments with stable ACLs, caching authorization decisions with a short TTL can significantly reduce latency. +- **Short-circuit for super users**: Implementations may choose to skip filtering entirely for super users or admin roles. + +The default implementation (return full list) adds negligible overhead since it returns an already-completed future. + +# Test Plan + +- **Unit tests for `FilterContext`**: Verify construction with and without parent resource, getter behavior. +- **Unit tests for default `filterAsync`**: Verify the default implementation returns the full list unchanged. +- **Unit tests for `AuthorizationService.filterAsync`**: + - Returns the full list when `authorizationEnabled=false` (provider is never called). + - Delegates to the provider when `authorizationEnabled=true`. +- **Unit tests with a custom filtering provider**: Register a mock `AuthorizationProvider` that filters based on role and resource type; verify correct filtering for each `ResourceType`. +- **Integration tests for each list endpoint** (clusters, tenants, namespaces, topics): + - With a no-op filter provider: verify the endpoint returns the same results as before. + - With a filtering provider: verify the endpoint returns only the permitted subset. + - Verify that `parentResource` is correctly populated in the `FilterContext` (null for clusters/tenants, tenant name for namespaces, namespace name for topics). +- **Thread safety test**: Verify that a `filterAsync` implementation performing async metadata lookups does not deadlock or block the calling thread. + +# 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` +## 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. + +## Replacing the existing auth gate with per-item filtering + +An alternative design would skip the all-or-nothing LIST permission check and rely solely on `filterAsync` to determine visibility. This was rejected because it changes the existing security model — deployments that rely on the 403 behavior for unauthorized users would silently start returning empty lists instead. The current design is additive: it layers filtering on top of the existing gate without altering its semantics. + +# Links + +* Mailing List discussion thread: +* Mailing List voting thread: