Skip to content

Commit ae25077

Browse files
authored
Merge pull request #56899 from nextcloud/feat/noid/ocm-capabilities
2 parents 17aa77e + 6af64a5 commit ae25077

26 files changed

+962
-170
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: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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+
if (!mb_check_encoding($ocmPath, 'UTF-8')) {
57+
throw new OCMArgumentException('path is not UTF-8');
58+
}
59+
60+
try {
61+
// if request is signed and well signed, no exceptions are thrown
62+
// if request is not signed and host is known for not supporting signed request, no exceptions are thrown
63+
$signedRequest = $this->ocmDiscoveryService->getIncomingSignedRequest();
64+
} catch (IncomingRequestException $e) {
65+
$this->logger->warning('incoming ocm request exception', ['exception' => $e]);
66+
return new JSONResponse(['message' => $e->getMessage(), 'validationErrors' => []], Http::STATUS_BAD_REQUEST);
67+
}
68+
69+
// assuming that ocm request contains a json array
70+
$payload = $signedRequest?->getBody() ?? file_get_contents('php://input');
71+
try {
72+
$payload = ($payload) ? json_decode($payload, true, 512, JSON_THROW_ON_ERROR) : null;
73+
} catch (JsonException $e) {
74+
$this->logger->debug('json decode error', ['exception' => $e]);
75+
$payload = null;
76+
}
77+
78+
$event = new OCMEndpointRequestEvent(
79+
$this->request->getMethod(),
80+
preg_replace('@/+@', '/', $ocmPath),
81+
$payload,
82+
$signedRequest?->getOrigin()
83+
);
84+
$this->eventDispatcher->dispatchTyped($event);
85+
86+
return $event->getResponse() ?? new DataResponse('', Http::STATUS_NOT_FOUND);
87+
}
88+
}

apps/cloud_federation_api/lib/Controller/RequestHandlerController.php

Lines changed: 5 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
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;
1816
use OC\OCM\OCMSignatoryManager;
@@ -44,6 +42,7 @@
4442
use OCP\IRequest;
4543
use OCP\IURLGenerator;
4644
use OCP\IUserManager;
45+
use OCP\OCM\IOCMDiscoveryService;
4746
use OCP\Share\Exceptions\ShareNotFound;
4847
use OCP\Util;
4948
use Psr\Log\LoggerInterface;
@@ -74,8 +73,8 @@ public function __construct(
7473
private readonly IAppConfig $appConfig,
7574
private ICloudFederationFactory $factory,
7675
private ICloudIdManager $cloudIdManager,
76+
private readonly IOCMDiscoveryService $ocmDiscoveryService,
7777
private readonly ISignatureManager $signatureManager,
78-
private readonly OCMSignatoryManager $signatoryManager,
7978
private ITimeFactory $timeFactory,
8079
) {
8180
parent::__construct($appName, $request);
@@ -108,9 +107,9 @@ public function __construct(
108107
public function addShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, $protocol, $shareType, $resourceType) {
109108
if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
110109
try {
111-
// if request is signed and well signed, no exception are thrown
110+
// if request is signed and well signed, no exceptions are thrown
112111
// if request is not signed and host is known for not supporting signed request, no exception are thrown
113-
$signedRequest = $this->getSignedRequest();
112+
$signedRequest = $this->ocmDiscoveryService->getIncomingSignedRequest();
114113
$this->confirmSignedOrigin($signedRequest, 'owner', $owner);
115114
} catch (IncomingRequestException $e) {
116115
$this->logger->warning('incoming request exception', ['exception' => $e]);
@@ -360,7 +359,7 @@ public function receiveNotification($notificationType, $resourceType, $providerI
360359
try {
361360
// if request is signed and well signed, no exception are thrown
362361
// if request is not signed and host is known for not supporting signed request, no exception are thrown
363-
$signedRequest = $this->getSignedRequest();
362+
$signedRequest = $this->ocmDiscoveryService->getIncomingSignedRequest();
364363
$this->confirmNotificationIdentity($signedRequest, $resourceType, $notification);
365364
} catch (IncomingRequestException $e) {
366365
$this->logger->warning('incoming request exception', ['exception' => $e]);
@@ -434,37 +433,6 @@ private function mapUid($uid) {
434433
}
435434

436435

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

apps/cloud_federation_api/tests/RequestHandlerControllerTest.php

Lines changed: 5 additions & 4 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;
@@ -29,6 +28,7 @@
2928
use OCP\IURLGenerator;
3029
use OCP\IUser;
3130
use OCP\IUserManager;
31+
use OCP\OCM\IOCMDiscoveryService;
3232
use PHPUnit\Framework\MockObject\MockObject;
3333
use Psr\Log\LoggerInterface;
3434
use Test\TestCase;
@@ -45,10 +45,11 @@ class RequestHandlerControllerTest extends TestCase {
4545
private FederatedInviteMapper&MockObject $federatedInviteMapper;
4646
private AddressHandler&MockObject $addressHandler;
4747
private IAppConfig&MockObject $appConfig;
48+
4849
private ICloudFederationFactory&MockObject $cloudFederationFactory;
4950
private ICloudIdManager&MockObject $cloudIdManager;
51+
private IOCMDiscoveryService&MockObject $discoveryService;
5052
private ISignatureManager&MockObject $signatureManager;
51-
private OCMSignatoryManager&MockObject $signatoryManager;
5253
private ITimeFactory&MockObject $timeFactory;
5354

5455
private RequestHandlerController $requestHandlerController;
@@ -69,8 +70,8 @@ protected function setUp(): void {
6970
$this->appConfig = $this->createMock(IAppConfig::class);
7071
$this->cloudFederationFactory = $this->createMock(ICloudFederationFactory::class);
7172
$this->cloudIdManager = $this->createMock(ICloudIdManager::class);
73+
$this->discoveryService = $this->createMock(IOCMDiscoveryService::class);
7274
$this->signatureManager = $this->createMock(ISignatureManager::class);
73-
$this->signatoryManager = $this->createMock(OCMSignatoryManager::class);
7475
$this->timeFactory = $this->createMock(ITimeFactory::class);
7576

7677
$this->requestHandlerController = new RequestHandlerController(
@@ -88,8 +89,8 @@ protected function setUp(): void {
8889
$this->appConfig,
8990
$this->cloudFederationFactory,
9091
$this->cloudIdManager,
92+
$this->discoveryService,
9193
$this->signatureManager,
92-
$this->signatoryManager,
9394
$this->timeFactory,
9495
);
9596
}

lib/composer/composer/autoload_classmap.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,9 +718,14 @@
718718
'OCP\\Notification\\InvalidValueException' => $baseDir . '/lib/public/Notification/InvalidValueException.php',
719719
'OCP\\Notification\\NotificationPreloadReason' => $baseDir . '/lib/public/Notification/NotificationPreloadReason.php',
720720
'OCP\\Notification\\UnknownNotificationException' => $baseDir . '/lib/public/Notification/UnknownNotificationException.php',
721+
'OCP\\OCM\\Enum\\ParamType' => $baseDir . '/lib/public/OCM/Enum/ParamType.php',
722+
'OCP\\OCM\\Events\\LocalOCMDiscoveryEvent' => $baseDir . '/lib/public/OCM/Events/LocalOCMDiscoveryEvent.php',
723+
'OCP\\OCM\\Events\\OCMEndpointRequestEvent' => $baseDir . '/lib/public/OCM/Events/OCMEndpointRequestEvent.php',
721724
'OCP\\OCM\\Events\\ResourceTypeRegisterEvent' => $baseDir . '/lib/public/OCM/Events/ResourceTypeRegisterEvent.php',
722725
'OCP\\OCM\\Exceptions\\OCMArgumentException' => $baseDir . '/lib/public/OCM/Exceptions/OCMArgumentException.php',
726+
'OCP\\OCM\\Exceptions\\OCMCapabilityException' => $baseDir . '/lib/public/OCM/Exceptions/OCMCapabilityException.php',
723727
'OCP\\OCM\\Exceptions\\OCMProviderException' => $baseDir . '/lib/public/OCM/Exceptions/OCMProviderException.php',
728+
'OCP\\OCM\\Exceptions\\OCMRequestException' => $baseDir . '/lib/public/OCM/Exceptions/OCMRequestException.php',
724729
'OCP\\OCM\\ICapabilityAwareOCMProvider' => $baseDir . '/lib/public/OCM/ICapabilityAwareOCMProvider.php',
725730
'OCP\\OCM\\IOCMDiscoveryService' => $baseDir . '/lib/public/OCM/IOCMDiscoveryService.php',
726731
'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
@@ -759,9 +759,14 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
759759
'OCP\\Notification\\InvalidValueException' => __DIR__ . '/../../..' . '/lib/public/Notification/InvalidValueException.php',
760760
'OCP\\Notification\\NotificationPreloadReason' => __DIR__ . '/../../..' . '/lib/public/Notification/NotificationPreloadReason.php',
761761
'OCP\\Notification\\UnknownNotificationException' => __DIR__ . '/../../..' . '/lib/public/Notification/UnknownNotificationException.php',
762+
'OCP\\OCM\\Enum\\ParamType' => __DIR__ . '/../../..' . '/lib/public/OCM/Enum/ParamType.php',
763+
'OCP\\OCM\\Events\\LocalOCMDiscoveryEvent' => __DIR__ . '/../../..' . '/lib/public/OCM/Events/LocalOCMDiscoveryEvent.php',
764+
'OCP\\OCM\\Events\\OCMEndpointRequestEvent' => __DIR__ . '/../../..' . '/lib/public/OCM/Events/OCMEndpointRequestEvent.php',
762765
'OCP\\OCM\\Events\\ResourceTypeRegisterEvent' => __DIR__ . '/../../..' . '/lib/public/OCM/Events/ResourceTypeRegisterEvent.php',
763766
'OCP\\OCM\\Exceptions\\OCMArgumentException' => __DIR__ . '/../../..' . '/lib/public/OCM/Exceptions/OCMArgumentException.php',
767+
'OCP\\OCM\\Exceptions\\OCMCapabilityException' => __DIR__ . '/../../..' . '/lib/public/OCM/Exceptions/OCMCapabilityException.php',
764768
'OCP\\OCM\\Exceptions\\OCMProviderException' => __DIR__ . '/../../..' . '/lib/public/OCM/Exceptions/OCMProviderException.php',
769+
'OCP\\OCM\\Exceptions\\OCMRequestException' => __DIR__ . '/../../..' . '/lib/public/OCM/Exceptions/OCMRequestException.php',
765770
'OCP\\OCM\\ICapabilityAwareOCMProvider' => __DIR__ . '/../../..' . '/lib/public/OCM/ICapabilityAwareOCMProvider.php',
766771
'OCP\\OCM\\IOCMDiscoveryService' => __DIR__ . '/../../..' . '/lib/public/OCM/IOCMDiscoveryService.php',
767772
'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($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($action['verb'] ?? 'GET');
192190

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

0 commit comments

Comments
 (0)