Skip to content

feat!: add TaskAuthorizationProvider SPI for per-user task authorization#935

Open
kabir wants to merge 1 commit into
a2aproject:mainfrom
kabir:authorisation-spi
Open

feat!: add TaskAuthorizationProvider SPI for per-user task authorization#935
kabir wants to merge 1 commit into
a2aproject:mainfrom
kabir:authorisation-spi

Conversation

@kabir

@kabir kabir commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Add an opt-in SPI for per-user task authorization, enforced via a CDI decorator around RequestHandler.

  • TaskOperation enum identifies which operation triggered the check
  • TaskAuthorizationProvider interface defines checkRead/checkWrite/ checkCreate/recordOwnership/isTaskRecorded methods
  • AuthorizationRequestHandlerDecorator (@decorator @priority(50)) wraps RequestHandler, calling the provider before/after each method
  • For onListTasks, filtering is pushed to TaskStore.list() via a new ServerCallContext parameter
  • InMemoryTaskStore filters all tasks before pagination (exact results)
  • JpaDatabaseTaskStore uses an iterative fetch loop to accumulate pageSize authorized results across DB pages (correct page sizes)
  • Streaming ownership recording wraps the publisher to call recordOwnership on the first task event
  • QuarkusCallContextFactory propagates Quarkus SecurityIdentity into gRPC ServerCallContext, so authorization checks see the authenticated user. Uses Instance for optional injection in apps without security configured
  • Integration tests cover all 3 transports (JSON-RPC, REST, gRPC) across reference, multiversion, and compat-0.3 modules

BREAKING CHANGE: TaskStore.list() now requires a ServerCallContext parameter. Custom TaskStore implementations must update their list() signature and apply authorization filtering when a TaskAuthorizationProvider is present.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an opt-in SPI for per-user task authorization (TaskAuthorizationProvider) along with a decorator to enforce read, write, and create permissions across request handlers and task stores. Feedback on these changes highlights a potential IllegalArgumentException in the gRPC call context factories when processing binary metadata headers, an inefficiency in JpaDatabaseTaskStore's iterative pagination query limit, and a security concern regarding information exposure via the pre-authorization task count.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@kabir

kabir commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces per-user task authorization to the SDK by adding a new TaskAuthorizationProvider SPI, an AuthorizationRequestHandlerDecorator to intercept requests, and updating TaskStore implementations to support authorization filtering during task listing. It also includes comprehensive integration tests and documentation updates. The review feedback highlights critical compilation errors in both InMemoryTaskStore and JpaDatabaseTaskStore due to unhandled checked exceptions (A2AError) thrown by checkRead inside the list methods. Additionally, it is recommended to annotate the ServerCallContext parameter with @nullable in TaskStore.list and its implementations to ensure null safety.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread server-common/src/main/java/org/a2aproject/sdk/server/tasks/TaskStore.java Outdated
@kabir kabir force-pushed the authorisation-spi branch from c11f888 to 90fdc52 Compare June 16, 2026 14:05

@ehsavoie ehsavoie left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The User interface only has isAuthenticated() and getUsername(). There's no way to get roles or claims. The TaskAuthorizationProvider receives a ServerCallContext so implementers can reach into state to get the raw SecurityIdentity, but this couples the provider to Quarkus internals. A getRoles(): Set method or getAttribute(String): Object would make the interface more useful for role-based authorization without requiring framework coupling. This isn't blocking, but it's a natural next step.

The anonymous User implementations in QuarkusCallContextFactory. Lines 39-49 create an anonymous User implementation inline. This works but makes it impossible to test equality or do instanceof checks. A named record like AuthenticatedUser(String username) would be cleaner and more testable.

@Override
public <V> ServerCallContext create(StreamObserver<V> responseObserver) {
User user;
if (securityIdentityInstance.isResolvable()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a duplicate of QuarkusCallContextFactory maybe we should have a helper method for this

.thenComparing(Task::id))
.toList();

if (authorizationProvider != null && context != null) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not adding this in the stream filtering above ?

@Inject
A2AConfigProvider configProvider;

private final @org.jspecify.annotations.Nullable TaskAuthorizationProvider authorizationProvider;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be @nullable

public TaskPushNotificationConfig onCreateTaskPushNotificationConfig(TaskPushNotificationConfig params,
ServerCallContext context) throws A2AError {
String taskId = params.taskId();
if (taskId != null) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If taskId is null the task can be created ? Is that correct ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No — if taskId is null, the delegate (DefaultRequestHandler) immediately throws InvalidParamsError("taskId is required") at line 793. The null guard in the decorator just avoids calling enforceWrite with a null argument; the request is never allowed through.

}
EventKind result = delegate.onMessageSend(params, context);
String resultTaskId = extractTaskId(result);
recordOwnershipIfNeeded(context, resultTaskId, TaskOperation.MESSAGE_SEND);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TOCTOU race in onMessageSend ownership recording.

If two concurrent onMessageSend calls arrive for the same new task (no taskId in the message), both pass enforceCreate, the delegate creates the task, and both reach recordOwnershipIfNeeded. The isTaskRecorded + recordOwnership sequence isn't atomic. The javadoc says recordOwnership "must be idempotent," which handles the data integrity side, but there's a window where isTaskRecorded returns false for both threads. The TestTaskAuthorizationProvider correctly uses putIfAbsent, so this is safe in practice as long as implementers follow the
idempotency contract. This is documented but worth highlighting in a "common pitfall" section of the interface javadoc.

@kabir kabir force-pushed the authorisation-spi branch from 90fdc52 to a541239 Compare June 17, 2026 09:48
@kabir

kabir commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a per-user task authorization SPI (TaskAuthorizationProvider) along with an intercepting decorator (AuthorizationRequestHandlerDecorator) to enforce access control across gRPC, JSON-RPC, and REST transports, including compatibility layers. While the security architecture is well-designed, several critical issues must be addressed before merging. Specifically, there are compilation failures in both JpaDatabaseTaskStore and InMemoryTaskStore due to unhandled checked exceptions (A2AError) within stream filters and database queries. Additionally, the iterative fetching logic in JpaDatabaseTaskStore is inefficient and poses an infinite loop risk under heavy unauthorized task loads. Finally, potential NullPointerExceptions exist in the JSON-RPC route handlers if the user subject is null when instantiating AuthenticatedUser.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Add an opt-in SPI that lets multi-user deployments control which
authenticated user may read, write, or create tasks. When a CDI bean
implementing TaskAuthorizationProvider is present the SDK wraps the
RequestHandler in an AuthorizationRequestHandlerDecorator that enforces
per-operation checks and records task ownership on creation.

Key additions:
- TaskAuthorizationProvider interface with checkRead/checkWrite/
  checkCreate/isTaskRecorded/recordOwnership contract
- AuthorizationRequestHandlerDecorator that intercepts every
  RequestHandler method and delegates to the provider
- AuthenticatedUser record replacing anonymous User implementations
  across all transports (gRPC, JSON-RPC, REST) and compat-0.3
- User.getAttribute() default method for extensible role/claim access
- Authorization-aware filtering in InMemoryTaskStore.list() and
  JpaDatabaseTaskStore.list() with correct pagination
- Comprehensive test suite covering all transports, multi-version
  dispatching, and the decorator itself
- Security guidance in README and javadoc: fail-closed ownership
  policy, TOCTOU race pitfall, CDI injection requirement

BREAKING CHANGE: TaskStore.list() signature now includes
ServerCallContext to support authorization filtering.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@kabir kabir force-pushed the authorisation-spi branch from a541239 to a67c490 Compare June 17, 2026 10:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants