Skip to content

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

Open
kabir wants to merge 4 commits into
a2aproject:mainfrom
kabir:authorisation-spi
Open

feat!: add TaskAuthorizationProvider SPI for per-user task authorization#935
kabir wants to merge 4 commits 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.

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<SecurityIdentity> 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.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@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 ?

}
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.

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