Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,25 @@ private void enforceRead(ServerCallContext context, String taskId, TaskOperation
}
}

/**
* List-scoped read check for {@code onListTasks}.
* <p>
* {@code LIST_TASKS} has no single task ID, so the provider is invoked with an
* empty-string sentinel representing the whole list scope (a {@code null} task ID
* would break providers that key lookups on the task ID, e.g.
* {@code ConcurrentHashMap}-backed stores). Denying the check rejects the call
* outright; otherwise per-task {@code checkRead} filtering is applied by the
* {@code TaskStore} during {@code list()}.
*/
private static final String LIST_TASKS_SCOPE_ID = "";

private void enforceListRead(ServerCallContext context) throws A2AError {
if (authorizationProvider != null
&& !authorizationProvider.checkRead(context, LIST_TASKS_SCOPE_ID, TaskOperation.LIST_TASKS)) {
throw new TaskNotFoundError();
}
}

@Override
public Task onGetTask(TaskQueryParams params, ServerCallContext context) throws A2AError {
enforceRead(context, params.id(), TaskOperation.GET_TASK);
Expand All @@ -181,6 +200,8 @@ public Task onGetTask(TaskQueryParams params, ServerCallContext context) throws

@Override
public ListTasksResult onListTasks(ListTasksParams params, ServerCallContext context) throws A2AError {
// List-scoped read check; per-task filtering is additionally applied by the TaskStore.
enforceListRead(context);
return delegate.onListTasks(params, context);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,29 @@ void onListTaskPushNotificationConfigs_denied() throws A2AError {
verifyNoInteractions(delegate);
}

@Test
void onListTasks_denied() throws A2AError {
ListTasksParams params = new ListTasksParams();
when(authorizationProvider.checkRead(context, "", TaskOperation.LIST_TASKS)).thenReturn(false);

assertThrows(TaskNotFoundError.class, () -> decorator.onListTasks(params, context));
verifyNoInteractions(delegate);
}

@Test
void onListTasks_allowed() throws A2AError {
ListTasksParams params = new ListTasksParams();
ListTasksResult expected = new ListTasksResult(Collections.emptyList(), 0, 0, null);
when(authorizationProvider.checkRead(context, "", TaskOperation.LIST_TASKS)).thenReturn(true);
when(delegate.onListTasks(params, context)).thenReturn(expected);

ListTasksResult result = decorator.onListTasks(params, context);

assertEquals(expected, result);
verify(authorizationProvider).checkRead(context, "", TaskOperation.LIST_TASKS);
verify(delegate).onListTasks(params, context);
}

@Test
void authorizeTaskAccess_allowed() throws A2AError {
when(authorizationProvider.checkRead(context, "task-1", TaskOperation.SUBSCRIBE_TO_TASK)).thenReturn(true);
Expand Down
Loading