From b2380f236a67e47daa6483e9763b308dda8d3357 Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Mon, 2 Nov 2026 10:30:09 -0100 Subject: [PATCH 1/3] feat(api): allow non-admin users to generate notifications Signed-off-by: Maxence Lange Assisted-by: ClaudeCode:claude-opus-5-5 --- lib/Capabilities.php | 1 + lib/Controller/APIController.php | 10 +++++++ openapi-administration.json | 29 --------------------- openapi-full.json | 29 --------------------- tests/Unit/CapabilitiesTest.php | 1 + tests/Unit/Controller/APIControllerTest.php | 14 ++++++++++ 6 files changed, 26 insertions(+), 58 deletions(-) diff --git a/lib/Capabilities.php b/lib/Capabilities.php index 58cadcba1..e8d0a07c6 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -60,6 +60,7 @@ public function getCapabilities(): array { 'admin-notifications' => [ 'ocs', 'cli', + 'self', ], ], ]; diff --git a/lib/Controller/APIController.php b/lib/Controller/APIController.php index dafb21ff4..1e0c106d8 100644 --- a/lib/Controller/APIController.php +++ b/lib/Controller/APIController.php @@ -17,7 +17,9 @@ use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; +use OCP\AppFramework\Services\IAppConfig; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IGroupManager; use OCP\IL10N; use OCP\IRequest; use OCP\IUser; @@ -47,6 +49,7 @@ public function __construct( protected IValidator $richValidator, protected IL10N $l, protected LoggerInterface $logger, + protected IGroupManager $groupManager, ) { parent::__construct($appName, $request); } @@ -96,6 +99,7 @@ public function generateNotification(string $userId, string $shortMessage, strin * 200: Notification generated successfully, returned id is the notification ID for future delete requests * 400: Provided data was invalid, check error field of the response of log file for details */ + #[NoAdminRequired] #[OpenAPI(scope: OpenAPI::SCOPE_ADMINISTRATION)] #[ApiRoute(verb: 'POST', url: '/api/{apiVersion3}/admin_notifications/{userId}', requirements: ['apiVersion3' => '(v3)'])] public function generateNotificationV3( @@ -105,6 +109,12 @@ public function generateNotificationV3( array $subjectParameters = [], array $messageParameters = [], ): DataResponse { + $actor = $this->userSession->getUser(); + if (!$this->groupManager->isAdmin($actor->getUID()) + && $userId !== $actor->getUID()) { + return new DataResponse(['error' => 'user'], Http::STATUS_BAD_REQUEST); + } + $user = $this->userManager->get($userId); if (!$user instanceof IUser) { diff --git a/openapi-administration.json b/openapi-administration.json index 6c5dc7d9f..218777ec1 100644 --- a/openapi-administration.json +++ b/openapi-administration.json @@ -454,7 +454,6 @@ "post": { "operationId": "api-generate-notification-v3", "summary": "Generate a notification with rich object parameters for a user", - "description": "This endpoint requires admin access", "tags": [ "api" ], @@ -633,34 +632,6 @@ } } } - }, - "403": { - "description": "Logged in account must be an admin", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } } } } diff --git a/openapi-full.json b/openapi-full.json index 5b988068f..7f8022afd 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -565,7 +565,6 @@ "post": { "operationId": "api-generate-notification-v3", "summary": "Generate a notification with rich object parameters for a user", - "description": "This endpoint requires admin access", "tags": [ "api" ], @@ -744,34 +743,6 @@ } } } - }, - "403": { - "description": "Logged in account must be an admin", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } } } } diff --git a/tests/Unit/CapabilitiesTest.php b/tests/Unit/CapabilitiesTest.php index 11586c39b..e7eb3e459 100644 --- a/tests/Unit/CapabilitiesTest.php +++ b/tests/Unit/CapabilitiesTest.php @@ -73,6 +73,7 @@ public function testGetCapabilities(bool $webpush, bool $webpushBrowser, array $ 'admin-notifications' => [ 'ocs', 'cli', + 'self', ], ], ], $capabilities->getCapabilities()); diff --git a/tests/Unit/Controller/APIControllerTest.php b/tests/Unit/Controller/APIControllerTest.php index b24449501..b9fc81573 100644 --- a/tests/Unit/Controller/APIControllerTest.php +++ b/tests/Unit/Controller/APIControllerTest.php @@ -12,7 +12,9 @@ use OCA\Notifications\Controller\APIController; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Services\IAppConfig; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IGroupManager; use OCP\IL10N; use OCP\IRequest; use OCP\IUser; @@ -38,6 +40,7 @@ class APIControllerTest extends TestCase { protected IValidator&MockObject $richValidator; protected IL10N&MockObject $l; protected LoggerInterface&MockObject $logger; + protected IGroupManager&MockObject $groupManager; protected APIController $controller; @@ -54,6 +57,7 @@ protected function setUp(): void { $this->richValidator = $this->createMock(IValidator::class); $this->l = $this->createMock(IL10N::class); $this->logger = $this->createMock(LoggerInterface::class); + $this->groupManager = $this->createMock(IGroupManager::class); $this->controller = new APIController( 'notifications', @@ -66,6 +70,7 @@ protected function setUp(): void { $this->richValidator, $this->l, $this->logger, + $this->groupManager, ); } @@ -95,6 +100,9 @@ public function testGenerateNotification(string $userId, string $short, string $ ->method('get') ->with($userId) ->willReturn($u); + $this->userSession->expects($this->any()) + ->method('getUser') + ->willReturn($this->mockUser($userId)); $dateTime = new \DateTime(); $dateTime->setTimestamp($time); @@ -156,4 +164,10 @@ public function testGenerateNotification(string $userId, string $short, string $ $this->assertInstanceOf(DataResponse::class, $response); $this->assertSame($statusCode, $response->getStatus()); } + + private function mockUser(string $uid): IUser&MockObject { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn($uid); + return $user; + } } From 9455541c19eadb5b6aecaf52988d1a895f014aa0 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Mon, 2 Nov 2026 14:04:37 +0100 Subject: [PATCH 2/3] test(integration): Add integration test for self-notifications Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: Joas Schilling --- .../features/admin-notification-v3.feature | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/Integration/features/admin-notification-v3.feature diff --git a/tests/Integration/features/admin-notification-v3.feature b/tests/Integration/features/admin-notification-v3.feature new file mode 100644 index 000000000..dc82f75e7 --- /dev/null +++ b/tests/Integration/features/admin-notification-v3.feature @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: CC0-1.0 +Feature: admin-notification-v3 + Background: + Given user "test1" exists + Given user "test2" exists + Given as user "test1" + + Scenario: Non-admin user creates a notification for themselves + When sending "POST" to "/apps/notifications/api/v3/admin_notifications/test1?format=json" with + | subject | self notification | + | message | this is the message | + Then status code is 200 + And user "test1" has 1 notifications on v2 + And last notification on v2 matches + | app | admin_notifications | + | subject | self notification | + | message | this is the message | + | object_type | admin_notifications | + + Scenario: Non-admin user can not create a notification for another user + When sending "POST" to "/apps/notifications/api/v3/admin_notifications/test2?format=json" with + | subject | foreign notification | + Then status code is 400 + Given as user "test2" + Then user "test2" has 0 notifications on v2 + + Scenario: Admin creates a notification for another user + Given as user "admin" + When sending "POST" to "/apps/notifications/api/v3/admin_notifications/test1?format=json" with + | subject | admin notification | + Then status code is 200 + Given as user "test1" + Then user "test1" has 1 notifications on v2 + And last notification on v2 matches + | app | admin_notifications | + | subject | admin notification | + | object_type | admin_notifications | From 30c1afd78db9ef2f645cd1a139fb936a8ba72828 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Mon, 2 Nov 2026 14:30:34 +0100 Subject: [PATCH 3/3] chore(cs): Apply autofixes Signed-off-by: Joas Schilling --- lib/Controller/APIController.php | 5 ++--- tests/Unit/Controller/APIControllerTest.php | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/Controller/APIController.php b/lib/Controller/APIController.php index 1e0c106d8..138846ee2 100644 --- a/lib/Controller/APIController.php +++ b/lib/Controller/APIController.php @@ -17,7 +17,6 @@ use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; -use OCP\AppFramework\Services\IAppConfig; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IGroupManager; use OCP\IL10N; @@ -110,8 +109,8 @@ public function generateNotificationV3( array $messageParameters = [], ): DataResponse { $actor = $this->userSession->getUser(); - if (!$this->groupManager->isAdmin($actor->getUID()) - && $userId !== $actor->getUID()) { + if (!$this->groupManager->isAdmin($actor?->getUID() ?? '') + && $userId !== $actor?->getUID()) { return new DataResponse(['error' => 'user'], Http::STATUS_BAD_REQUEST); } diff --git a/tests/Unit/Controller/APIControllerTest.php b/tests/Unit/Controller/APIControllerTest.php index b9fc81573..37169dd61 100644 --- a/tests/Unit/Controller/APIControllerTest.php +++ b/tests/Unit/Controller/APIControllerTest.php @@ -12,7 +12,6 @@ use OCA\Notifications\Controller\APIController; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\Services\IAppConfig; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IGroupManager; use OCP\IL10N;