Skip to content

Commit e6ad3a5

Browse files
committed
feat(ocm): event on ocm discovery and ocm request
Signed-off-by: Maxence Lange <[email protected]>
1 parent 1829269 commit e6ad3a5

26 files changed

+975
-191
lines changed

apps/cloud_federation_api/appinfo/routes.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@
2525
'url' => '/invite-accepted',
2626
'verb' => 'POST',
2727
'root' => '/ocm',
28-
]
28+
],
29+
30+
// needs to be kept at the bottom of the list
31+
[
32+
'name' => 'OCMRequest#manageOCMRequests',
33+
'url' => '/{ocmPath}',
34+
'requirements' => ['ocmPath' => '.*'],
35+
'verb' => ['GET', 'POST', 'PUT', 'DELETE'],
36+
'root' => '/ocm',
37+
],
2938
],
3039
];

apps/cloud_federation_api/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
'OCA\\CloudFederationAPI\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
1111
'OCA\\CloudFederationAPI\\Capabilities' => $baseDir . '/../lib/Capabilities.php',
1212
'OCA\\CloudFederationAPI\\Config' => $baseDir . '/../lib/Config.php',
13+
'OCA\\CloudFederationAPI\\Controller\\OCMRequestController' => $baseDir . '/../lib/Controller/OCMRequestController.php',
1314
'OCA\\CloudFederationAPI\\Controller\\RequestHandlerController' => $baseDir . '/../lib/Controller/RequestHandlerController.php',
1415
'OCA\\CloudFederationAPI\\Db\\FederatedInvite' => $baseDir . '/../lib/Db/FederatedInvite.php',
1516
'OCA\\CloudFederationAPI\\Db\\FederatedInviteMapper' => $baseDir . '/../lib/Db/FederatedInviteMapper.php',

apps/cloud_federation_api/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class ComposerStaticInitCloudFederationAPI
2525
'OCA\\CloudFederationAPI\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
2626
'OCA\\CloudFederationAPI\\Capabilities' => __DIR__ . '/..' . '/../lib/Capabilities.php',
2727
'OCA\\CloudFederationAPI\\Config' => __DIR__ . '/..' . '/../lib/Config.php',
28+
'OCA\\CloudFederationAPI\\Controller\\OCMRequestController' => __DIR__ . '/..' . '/../lib/Controller/OCMRequestController.php',
2829
'OCA\\CloudFederationAPI\\Controller\\RequestHandlerController' => __DIR__ . '/..' . '/../lib/Controller/RequestHandlerController.php',
2930
'OCA\\CloudFederationAPI\\Db\\FederatedInvite' => __DIR__ . '/..' . '/../lib/Db/FederatedInvite.php',
3031
'OCA\\CloudFederationAPI\\Db\\FederatedInviteMapper' => __DIR__ . '/..' . '/../lib/Db/FederatedInviteMapper.php',
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\CloudFederationAPI\Controller;
11+
12+
use JsonException;
13+
use NCU\Security\Signature\Exceptions\IncomingRequestException;
14+
use OCP\AppFramework\Controller;
15+
use OCP\AppFramework\Http;
16+
use OCP\AppFramework\Http\Attribute\BruteForceProtection;
17+
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
18+
use OCP\AppFramework\Http\Attribute\PublicPage;
19+
use OCP\AppFramework\Http\DataResponse;
20+
use OCP\AppFramework\Http\JSONResponse;
21+
use OCP\AppFramework\Http\Response;
22+
use OCP\EventDispatcher\IEventDispatcher;
23+
use OCP\IRequest;
24+
use OCP\OCM\Events\OCMEndpointRequestEvent;
25+
use OCP\OCM\Exceptions\OCMArgumentException;
26+
use OCP\OCM\IOCMDiscoveryService;
27+
use Psr\Log\LoggerInterface;
28+
29+
class OCMRequestController extends Controller {
30+
public function __construct(
31+
string $appName,
32+
IRequest $request,
33+
private readonly IEventDispatcher $eventDispatcher,
34+
private readonly IOCMDiscoveryService $ocmDiscoveryService,
35+
private readonly LoggerInterface $logger,
36+
) {
37+
parent::__construct($appName, $request);
38+
}
39+
40+
/**
41+
* Method will catch any request done to /ocm/[...] and will broadcast an event.
42+
* The first parameter of the remaining subpath (post-/ocm/) is defined as
43+
* capability and should be used by listeners to filter incoming requests.
44+
*
45+
* @see OCMEndpointRequestEvent
46+
* @see OCMEndpointRequestEvent::getArgs
47+
*
48+
* @param string $ocmPath
49+
* @return Response
50+
* @throws OCMArgumentException
51+
*/
52+
#[NoCSRFRequired]
53+
#[PublicPage]
54+
#[BruteForceProtection(action: 'receiveOcmRequest')]
55+
public function manageOCMRequests(string $ocmPath): Response {
56+
try {
57+
json_encode($ocmPath, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
58+
} catch (JsonException) {
59+
throw new OCMArgumentException('path is not UTF-8');
60+
}
61+
62+
try {
63+
// if request is signed and well signed, no exceptions are thrown
64+
// if request is not signed and host is known for not supporting signed request, no exceptions are thrown
65+
$signedRequest = $this->ocmDiscoveryService->getIncomingSignedRequest();
66+
} catch (IncomingRequestException $e) {
67+
$this->logger->warning('incoming ocm request exception', ['exception' => $e]);
68+
return new JSONResponse(['message' => $e->getMessage(), 'validationErrors' => []], Http::STATUS_BAD_REQUEST);
69+
}
70+
71+
// assuming that ocm request contains a json array
72+
$payload = $signedRequest?->getBody() ?? file_get_contents('php://input');
73+
try {
74+
$payload = (!$payload) ? null : json_decode($payload, true, 512, JSON_THROW_ON_ERROR);
75+
} catch (JsonException $e) {
76+
$this->logger->debug('json decode error', ['exception' => $e]);
77+
$payload = null;
78+
}
79+
80+
$event = new OCMEndpointRequestEvent(
81+
$this->request->getMethod(),
82+
str_replace('//', '/', $ocmPath),
83+
$payload,
84+
$signedRequest?->getOrigin()
85+
);
86+
$this->eventDispatcher->dispatchTyped($event);
87+
88+
return $event->getResponse() ?? new DataResponse('', Http::STATUS_NOT_FOUND);
89+
}
90+
}

apps/cloud_federation_api/lib/Controller/RequestHandlerController.php

Lines changed: 5 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,8 @@
1111
use NCU\Security\Signature\Exceptions\IdentityNotFoundException;
1212
use NCU\Security\Signature\Exceptions\IncomingRequestException;
1313
use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
14-
use NCU\Security\Signature\Exceptions\SignatureException;
15-
use NCU\Security\Signature\Exceptions\SignatureNotFoundException;
1614
use NCU\Security\Signature\IIncomingSignedRequest;
1715
use NCU\Security\Signature\ISignatureManager;
18-
use OC\OCM\OCMSignatoryManager;
1916
use OCA\CloudFederationAPI\Config;
2017
use OCA\CloudFederationAPI\Db\FederatedInviteMapper;
2118
use OCA\CloudFederationAPI\Events\FederatedInviteAcceptedEvent;
@@ -39,11 +36,11 @@
3936
use OCP\Federation\ICloudFederationFactory;
4037
use OCP\Federation\ICloudFederationProviderManager;
4138
use OCP\Federation\ICloudIdManager;
42-
use OCP\IAppConfig;
4339
use OCP\IGroupManager;
4440
use OCP\IRequest;
4541
use OCP\IURLGenerator;
4642
use OCP\IUserManager;
43+
use OCP\OCM\IOCMDiscoveryService;
4744
use OCP\Share\Exceptions\ShareNotFound;
4845
use OCP\Util;
4946
use Psr\Log\LoggerInterface;
@@ -71,11 +68,10 @@ public function __construct(
7168
private IEventDispatcher $dispatcher,
7269
private FederatedInviteMapper $federatedInviteMapper,
7370
private readonly AddressHandler $addressHandler,
74-
private readonly IAppConfig $appConfig,
7571
private ICloudFederationFactory $factory,
7672
private ICloudIdManager $cloudIdManager,
73+
private readonly IOCMDiscoveryService $ocmDiscoveryService,
7774
private readonly ISignatureManager $signatureManager,
78-
private readonly OCMSignatoryManager $signatoryManager,
7975
private ITimeFactory $timeFactory,
8076
) {
8177
parent::__construct($appName, $request);
@@ -107,9 +103,9 @@ public function __construct(
107103
#[BruteForceProtection(action: 'receiveFederatedShare')]
108104
public function addShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, $protocol, $shareType, $resourceType) {
109105
try {
110-
// if request is signed and well signed, no exception are thrown
106+
// if request is signed and well signed, no exceptions are thrown
111107
// if request is not signed and host is known for not supporting signed request, no exception are thrown
112-
$signedRequest = $this->getSignedRequest();
108+
$signedRequest = $this->ocmDiscoveryService->getIncomingSignedRequest();
113109
$this->confirmSignedOrigin($signedRequest, 'owner', $owner);
114110
} catch (IncomingRequestException $e) {
115111
$this->logger->warning('incoming request exception', ['exception' => $e]);
@@ -357,7 +353,7 @@ public function receiveNotification($notificationType, $resourceType, $providerI
357353
try {
358354
// if request is signed and well signed, no exception are thrown
359355
// if request is not signed and host is known for not supporting signed request, no exception are thrown
360-
$signedRequest = $this->getSignedRequest();
356+
$signedRequest = $this->ocmDiscoveryService->getIncomingSignedRequest();
361357
$this->confirmNotificationIdentity($signedRequest, $resourceType, $notification);
362358
} catch (IncomingRequestException $e) {
363359
$this->logger->warning('incoming request exception', ['exception' => $e]);
@@ -430,37 +426,6 @@ private function mapUid($uid) {
430426
}
431427

432428

433-
/**
434-
* returns signed request if available.
435-
* throw an exception:
436-
* - if request is signed, but wrongly signed
437-
* - if request is not signed but instance is configured to only accept signed ocm request
438-
*
439-
* @return IIncomingSignedRequest|null null if remote does not (and never did) support signed request
440-
* @throws IncomingRequestException
441-
*/
442-
private function getSignedRequest(): ?IIncomingSignedRequest {
443-
try {
444-
$signedRequest = $this->signatureManager->getIncomingSignedRequest($this->signatoryManager);
445-
$this->logger->debug('signed request available', ['signedRequest' => $signedRequest]);
446-
return $signedRequest;
447-
} catch (SignatureNotFoundException|SignatoryNotFoundException $e) {
448-
$this->logger->debug('remote does not support signed request', ['exception' => $e]);
449-
// remote does not support signed request.
450-
// currently we still accept unsigned request until lazy appconfig
451-
// core.enforce_signed_ocm_request is set to true (default: false)
452-
if ($this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_ENFORCED, lazy: true)) {
453-
$this->logger->notice('ignored unsigned request', ['exception' => $e]);
454-
throw new IncomingRequestException('Unsigned request');
455-
}
456-
} catch (SignatureException $e) {
457-
$this->logger->warning('wrongly signed request', ['exception' => $e]);
458-
throw new IncomingRequestException('Invalid signature');
459-
}
460-
return null;
461-
}
462-
463-
464429
/**
465430
* confirm that the value related to $key entry from the payload is in format userid@hostname
466431
* and compare hostname with the origin of the signed request.

apps/cloud_federation_api/tests/RequestHandlerControllerTest.php

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
namespace OCA\CloudFederationApi\Tests;
1111

1212
use NCU\Security\Signature\ISignatureManager;
13-
use OC\OCM\OCMSignatoryManager;
1413
use OCA\CloudFederationAPI\Config;
1514
use OCA\CloudFederationAPI\Controller\RequestHandlerController;
1615
use OCA\CloudFederationAPI\Db\FederatedInvite;
@@ -23,12 +22,12 @@
2322
use OCP\Federation\ICloudFederationFactory;
2423
use OCP\Federation\ICloudFederationProviderManager;
2524
use OCP\Federation\ICloudIdManager;
26-
use OCP\IAppConfig;
2725
use OCP\IGroupManager;
2826
use OCP\IRequest;
2927
use OCP\IURLGenerator;
3028
use OCP\IUser;
3129
use OCP\IUserManager;
30+
use OCP\OCM\IOCMDiscoveryService;
3231
use PHPUnit\Framework\MockObject\MockObject;
3332
use Psr\Log\LoggerInterface;
3433
use Test\TestCase;
@@ -44,11 +43,10 @@ class RequestHandlerControllerTest extends TestCase {
4443
private IEventDispatcher&MockObject $eventDispatcher;
4544
private FederatedInviteMapper&MockObject $federatedInviteMapper;
4645
private AddressHandler&MockObject $addressHandler;
47-
private IAppConfig&MockObject $appConfig;
4846
private ICloudFederationFactory&MockObject $cloudFederationFactory;
4947
private ICloudIdManager&MockObject $cloudIdManager;
48+
private IOCMDiscoveryService&MockObject $discoveryService;
5049
private ISignatureManager&MockObject $signatureManager;
51-
private OCMSignatoryManager&MockObject $signatoryManager;
5250
private ITimeFactory&MockObject $timeFactory;
5351

5452
private RequestHandlerController $requestHandlerController;
@@ -66,11 +64,10 @@ protected function setUp(): void {
6664
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
6765
$this->federatedInviteMapper = $this->createMock(FederatedInviteMapper::class);
6866
$this->addressHandler = $this->createMock(AddressHandler::class);
69-
$this->appConfig = $this->createMock(IAppConfig::class);
7067
$this->cloudFederationFactory = $this->createMock(ICloudFederationFactory::class);
7168
$this->cloudIdManager = $this->createMock(ICloudIdManager::class);
69+
$this->discoveryService = $this->createMock(IOCMDiscoveryService::class);
7270
$this->signatureManager = $this->createMock(ISignatureManager::class);
73-
$this->signatoryManager = $this->createMock(OCMSignatoryManager::class);
7471
$this->timeFactory = $this->createMock(ITimeFactory::class);
7572

7673
$this->requestHandlerController = new RequestHandlerController(
@@ -85,11 +82,10 @@ protected function setUp(): void {
8582
$this->eventDispatcher,
8683
$this->federatedInviteMapper,
8784
$this->addressHandler,
88-
$this->appConfig,
8985
$this->cloudFederationFactory,
9086
$this->cloudIdManager,
87+
$this->discoveryService,
9188
$this->signatureManager,
92-
$this->signatoryManager,
9389
$this->timeFactory,
9490
);
9591
}

lib/composer/composer/autoload_classmap.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,9 +716,14 @@
716716
'OCP\\Notification\\InvalidValueException' => $baseDir . '/lib/public/Notification/InvalidValueException.php',
717717
'OCP\\Notification\\NotificationPreloadReason' => $baseDir . '/lib/public/Notification/NotificationPreloadReason.php',
718718
'OCP\\Notification\\UnknownNotificationException' => $baseDir . '/lib/public/Notification/UnknownNotificationException.php',
719+
'OCP\\OCM\\Enum\\ParamType' => $baseDir . '/lib/public/OCM/Enum/ParamType.php',
720+
'OCP\\OCM\\Events\\LocalOCMDiscoveryEvent' => $baseDir . '/lib/public/OCM/Events/LocalOCMDiscoveryEvent.php',
721+
'OCP\\OCM\\Events\\OCMEndpointRequestEvent' => $baseDir . '/lib/public/OCM/Events/OCMEndpointRequestEvent.php',
719722
'OCP\\OCM\\Events\\ResourceTypeRegisterEvent' => $baseDir . '/lib/public/OCM/Events/ResourceTypeRegisterEvent.php',
720723
'OCP\\OCM\\Exceptions\\OCMArgumentException' => $baseDir . '/lib/public/OCM/Exceptions/OCMArgumentException.php',
724+
'OCP\\OCM\\Exceptions\\OCMCapabilityException' => $baseDir . '/lib/public/OCM/Exceptions/OCMCapabilityException.php',
721725
'OCP\\OCM\\Exceptions\\OCMProviderException' => $baseDir . '/lib/public/OCM/Exceptions/OCMProviderException.php',
726+
'OCP\\OCM\\Exceptions\\OCMRequestException' => $baseDir . '/lib/public/OCM/Exceptions/OCMRequestException.php',
722727
'OCP\\OCM\\ICapabilityAwareOCMProvider' => $baseDir . '/lib/public/OCM/ICapabilityAwareOCMProvider.php',
723728
'OCP\\OCM\\IOCMDiscoveryService' => $baseDir . '/lib/public/OCM/IOCMDiscoveryService.php',
724729
'OCP\\OCM\\IOCMProvider' => $baseDir . '/lib/public/OCM/IOCMProvider.php',

lib/composer/composer/autoload_static.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,9 +757,14 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
757757
'OCP\\Notification\\InvalidValueException' => __DIR__ . '/../../..' . '/lib/public/Notification/InvalidValueException.php',
758758
'OCP\\Notification\\NotificationPreloadReason' => __DIR__ . '/../../..' . '/lib/public/Notification/NotificationPreloadReason.php',
759759
'OCP\\Notification\\UnknownNotificationException' => __DIR__ . '/../../..' . '/lib/public/Notification/UnknownNotificationException.php',
760+
'OCP\\OCM\\Enum\\ParamType' => __DIR__ . '/../../..' . '/lib/public/OCM/Enum/ParamType.php',
761+
'OCP\\OCM\\Events\\LocalOCMDiscoveryEvent' => __DIR__ . '/../../..' . '/lib/public/OCM/Events/LocalOCMDiscoveryEvent.php',
762+
'OCP\\OCM\\Events\\OCMEndpointRequestEvent' => __DIR__ . '/../../..' . '/lib/public/OCM/Events/OCMEndpointRequestEvent.php',
760763
'OCP\\OCM\\Events\\ResourceTypeRegisterEvent' => __DIR__ . '/../../..' . '/lib/public/OCM/Events/ResourceTypeRegisterEvent.php',
761764
'OCP\\OCM\\Exceptions\\OCMArgumentException' => __DIR__ . '/../../..' . '/lib/public/OCM/Exceptions/OCMArgumentException.php',
765+
'OCP\\OCM\\Exceptions\\OCMCapabilityException' => __DIR__ . '/../../..' . '/lib/public/OCM/Exceptions/OCMCapabilityException.php',
762766
'OCP\\OCM\\Exceptions\\OCMProviderException' => __DIR__ . '/../../..' . '/lib/public/OCM/Exceptions/OCMProviderException.php',
767+
'OCP\\OCM\\Exceptions\\OCMRequestException' => __DIR__ . '/../../..' . '/lib/public/OCM/Exceptions/OCMRequestException.php',
763768
'OCP\\OCM\\ICapabilityAwareOCMProvider' => __DIR__ . '/../../..' . '/lib/public/OCM/ICapabilityAwareOCMProvider.php',
764769
'OCP\\OCM\\IOCMDiscoveryService' => __DIR__ . '/../../..' . '/lib/public/OCM/IOCMDiscoveryService.php',
765770
'OCP\\OCM\\IOCMProvider' => __DIR__ . '/../../..' . '/lib/public/OCM/IOCMProvider.php',

lib/private/AppFramework/Routing/RouteParser.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ private function processRoute(array $route, string $appName, string $routeNamePr
7575
$root = $this->buildRootPrefix($route, $appName, $routeNamePrefix);
7676

7777
$url = $root . '/' . ltrim($route['url'], '/');
78-
$verb = strtoupper($route['verb'] ?? 'GET');
7978

8079
$split = explode('#', $name, 3);
8180
if (count($split) !== 2) {
@@ -95,7 +94,7 @@ private function processRoute(array $route, string $appName, string $routeNamePr
9594
$routeName = strtolower($routeNamePrefix . $appName . '.' . $controller . '.' . $action . $postfix);
9695

9796
$routeObject = new Route($url);
98-
$routeObject->method($verb);
97+
$routeObject->method((array)($route['verb'] ?? ['GET']));
9998

10099
// optionally register requirements for route. This is used to
101100
// tell the route parser how url parameters should be matched
@@ -174,7 +173,6 @@ private function processResources(array $resources, string $appName, string $rou
174173
$url = $root . '/' . ltrim($config['url'], '/');
175174
$method = $action['name'];
176175

177-
$verb = strtoupper($action['verb'] ?? 'GET');
178176
$collectionAction = $action['on-collection'] ?? false;
179177
if (!$collectionAction) {
180178
$url .= '/{id}';
@@ -188,7 +186,7 @@ private function processResources(array $resources, string $appName, string $rou
188186
$routeName = $routeNamePrefix . $appName . '.' . strtolower($resource) . '.' . $method;
189187

190188
$route = new Route($url);
191-
$route->method($verb);
189+
$route->method((array)($action['verb'] ?? ['GET']));
192190

193191
$route->defaults(['caller' => [$appName, $controllerName, $actionName]]);
194192

0 commit comments

Comments
 (0)