diff options
Diffstat (limited to 'lib/private/OCM')
-rw-r--r-- | lib/private/OCM/Model/OCMProvider.php | 141 | ||||
-rw-r--r-- | lib/private/OCM/Model/OCMResource.php | 33 | ||||
-rw-r--r-- | lib/private/OCM/OCMDiscoveryService.php | 95 | ||||
-rw-r--r-- | lib/private/OCM/OCMSignatoryManager.php | 160 |
4 files changed, 301 insertions, 128 deletions
diff --git a/lib/private/OCM/Model/OCMProvider.php b/lib/private/OCM/Model/OCMProvider.php index 084d4f8479d..be13d65a40f 100644 --- a/lib/private/OCM/Model/OCMProvider.php +++ b/lib/private/OCM/Model/OCMProvider.php @@ -3,51 +3,41 @@ declare(strict_types=1); /** - * @copyright 2023, Maxence Lange <maxence@artificial-owl.com> - * - * @author Maxence Lange <maxence@artificial-owl.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\OCM\Model; +use NCU\Security\Signature\Model\Signatory; use OCP\EventDispatcher\IEventDispatcher; +use OCP\IConfig; use OCP\OCM\Events\ResourceTypeRegisterEvent; use OCP\OCM\Exceptions\OCMArgumentException; use OCP\OCM\Exceptions\OCMProviderException; -use OCP\OCM\IOCMProvider; +use OCP\OCM\ICapabilityAwareOCMProvider; use OCP\OCM\IOCMResource; /** * @since 28.0.0 */ -class OCMProvider implements IOCMProvider { +class OCMProvider implements ICapabilityAwareOCMProvider { + private string $provider; private bool $enabled = false; private string $apiVersion = ''; + private string $inviteAcceptDialog = ''; + private array $capabilities = []; private string $endPoint = ''; /** @var IOCMResource[] */ private array $resourceTypes = []; - + private ?Signatory $signatory = null; private bool $emittedEvent = false; public function __construct( protected IEventDispatcher $dispatcher, + protected IConfig $config, ) { + $this->provider = 'Nextcloud ' . $config->getSystemValue('version'); } /** @@ -87,6 +77,30 @@ class OCMProvider implements IOCMProvider { } /** + * returns the invite accept dialog + * + * @return string + * @since 32.0.0 + */ + public function getInviteAcceptDialog(): string { + return $this->inviteAcceptDialog; + } + + /** + * set the invite accept dialog + * + * @param string $inviteAcceptDialog + * + * @return $this + * @since 32.0.0 + */ + public function setInviteAcceptDialog(string $inviteAcceptDialog): static { + $this->inviteAcceptDialog = $inviteAcceptDialog; + + return $this; + } + + /** * @param string $endPoint * * @return $this @@ -105,6 +119,34 @@ class OCMProvider implements IOCMProvider { } /** + * @return string + */ + public function getProvider(): string { + return $this->provider; + } + + /** + * @param array $capabilities + * + * @return $this + */ + public function setCapabilities(array $capabilities): static { + foreach ($capabilities as $value) { + if (!in_array($value, $this->capabilities)) { + array_push($this->capabilities, $value); + } + } + + return $this; + } + + /** + * @return array + */ + public function getCapabilities(): array { + return $this->capabilities; + } + /** * create a new resource to later add it with {@see IOCMProvider::addResourceType()} * @return IOCMResource */ @@ -169,19 +211,27 @@ class OCMProvider implements IOCMProvider { throw new OCMArgumentException('resource not found'); } + public function setSignatory(Signatory $signatory): void { + $this->signatory = $signatory; + } + + public function getSignatory(): ?Signatory { + return $this->signatory; + } + /** * import data from an array * * @param array $data * - * @return $this + * @return OCMProvider&static * @throws OCMProviderException in case a descent provider cannot be generated from data - * @see self::jsonSerialize() */ public function import(array $data): static { $this->setEnabled(is_bool($data['enabled'] ?? '') ? $data['enabled'] : false) - ->setApiVersion((string)($data['apiVersion'] ?? '')) - ->setEndPoint($data['endPoint'] ?? ''); + // Fall back to old apiVersion for Nextcloud 30 compatibility + ->setApiVersion((string)($data['version'] ?? $data['apiVersion'] ?? '')) + ->setEndPoint($data['endPoint'] ?? ''); $resources = []; foreach (($data['resourceTypes'] ?? []) as $resourceData) { @@ -190,6 +240,16 @@ class OCMProvider implements IOCMProvider { } $this->setResourceTypes($resources); + if (isset($data['publicKey'])) { + // import details about the remote request signing public key, if available + $signatory = new Signatory(); + $signatory->setKeyId($data['publicKey']['keyId'] ?? ''); + $signatory->setPublicKey($data['publicKey']['publicKeyPem'] ?? ''); + if ($signatory->getKeyId() !== '' && $signatory->getPublicKey() !== '') { + $this->setSignatory($signatory); + } + } + if (!$this->looksValid()) { throw new OCMProviderException('remote provider does not look valid'); } @@ -205,18 +265,8 @@ class OCMProvider implements IOCMProvider { return ($this->getApiVersion() !== '' && $this->getEndPoint() !== ''); } - /** - * @return array{ - * enabled: bool, - * apiVersion: string, - * endPoint: string, - * resourceTypes: array{ - * name: string, - * shareTypes: string[], - * protocols: array<string, string> - * }[] - * } + * @since 28.0.0 */ public function jsonSerialize(): array { $resourceTypes = []; @@ -224,11 +274,24 @@ class OCMProvider implements IOCMProvider { $resourceTypes[] = $res->jsonSerialize(); } - return [ + $response = [ 'enabled' => $this->isEnabled(), - 'apiVersion' => $this->getApiVersion(), + 'apiVersion' => '1.0-proposal1', // deprecated, but keep it to stay compatible with old version + 'version' => $this->getApiVersion(), // informative but real version 'endPoint' => $this->getEndPoint(), + 'publicKey' => $this->getSignatory()?->jsonSerialize(), 'resourceTypes' => $resourceTypes ]; + + $capabilities = $this->getCapabilities(); + $inviteAcceptDialog = $this->getInviteAcceptDialog(); + if ($capabilities) { + $response['capabilities'] = $capabilities; + } + if ($inviteAcceptDialog) { + $response['inviteAcceptDialog'] = $inviteAcceptDialog; + } + return $response; + } } diff --git a/lib/private/OCM/Model/OCMResource.php b/lib/private/OCM/Model/OCMResource.php index c4a91f2eabf..3d619db1927 100644 --- a/lib/private/OCM/Model/OCMResource.php +++ b/lib/private/OCM/Model/OCMResource.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2023, Maxence Lange <maxence@artificial-owl.com> - * - * @author Maxence Lange <maxence@artificial-owl.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\OCM\Model; @@ -33,7 +16,7 @@ use OCP\OCM\IOCMResource; */ class OCMResource implements IOCMResource { private string $name = ''; - /** @var string[] */ + /** @var list<string> */ private array $shareTypes = []; /** @var array<string, string> */ private array $protocols = []; @@ -57,7 +40,7 @@ class OCMResource implements IOCMResource { } /** - * @param string[] $shareTypes + * @param list<string> $shareTypes * * @return $this */ @@ -68,7 +51,7 @@ class OCMResource implements IOCMResource { } /** - * @return string[] + * @return list<string> */ public function getShareTypes(): array { return $this->shareTypes; @@ -102,14 +85,14 @@ class OCMResource implements IOCMResource { */ public function import(array $data): static { return $this->setName((string)($data['name'] ?? '')) - ->setShareTypes($data['shareTypes'] ?? []) - ->setProtocols($data['protocols'] ?? []); + ->setShareTypes($data['shareTypes'] ?? []) + ->setProtocols($data['protocols'] ?? []); } /** * @return array{ * name: string, - * shareTypes: string[], + * shareTypes: list<string>, * protocols: array<string, string> * } */ diff --git a/lib/private/OCM/OCMDiscoveryService.php b/lib/private/OCM/OCMDiscoveryService.php index ac9bf2a3965..a151bbc753c 100644 --- a/lib/private/OCM/OCMDiscoveryService.php +++ b/lib/private/OCM/OCMDiscoveryService.php @@ -3,29 +3,13 @@ declare(strict_types=1); /** - * @copyright 2023, Maxence Lange <maxence@artificial-owl.com> - * - * @author Maxence Lange <maxence@artificial-owl.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\OCM; +use GuzzleHttp\Exception\ConnectException; use JsonException; use OCP\AppFramework\Http; use OCP\Http\Client\IClientService; @@ -33,8 +17,8 @@ use OCP\ICache; use OCP\ICacheFactory; use OCP\IConfig; use OCP\OCM\Exceptions\OCMProviderException; +use OCP\OCM\ICapabilityAwareOCMProvider; use OCP\OCM\IOCMDiscoveryService; -use OCP\OCM\IOCMProvider; use Psr\Log\LoggerInterface; /** @@ -42,18 +26,12 @@ use Psr\Log\LoggerInterface; */ class OCMDiscoveryService implements IOCMDiscoveryService { private ICache $cache; - private array $supportedAPIVersion = - [ - '1.0-proposal1', - '1.0', - '1.1' - ]; public function __construct( ICacheFactory $cacheFactory, private IClientService $clientService, private IConfig $config, - private IOCMProvider $provider, + private ICapabilityAwareOCMProvider $provider, private LoggerInterface $logger, ) { $this->cache = $cacheFactory->createDistributed('ocm-discovery'); @@ -64,18 +42,29 @@ class OCMDiscoveryService implements IOCMDiscoveryService { * @param string $remote * @param bool $skipCache * - * @return IOCMProvider + * @return ICapabilityAwareOCMProvider * @throws OCMProviderException */ - public function discover(string $remote, bool $skipCache = false): IOCMProvider { + public function discover(string $remote, bool $skipCache = false): ICapabilityAwareOCMProvider { $remote = rtrim($remote, '/'); + if (!str_starts_with($remote, 'http://') && !str_starts_with($remote, 'https://')) { + // if scheme not specified, we test both; + try { + return $this->discover('https://' . $remote, $skipCache); + } catch (OCMProviderException|ConnectException) { + return $this->discover('http://' . $remote, $skipCache); + } + } if (!$skipCache) { try { - $this->provider->import(json_decode($this->cache->get($remote) ?? '', true, 8, JSON_THROW_ON_ERROR) ?? []); - if ($this->supportedAPIVersion($this->provider->getApiVersion())) { - return $this->provider; // if cache looks valid, we use it + $cached = $this->cache->get($remote); + if ($cached === false) { + throw new OCMProviderException('Previous discovery failed.'); } + + $this->provider->import(json_decode($cached ?? '', true, 8, JSON_THROW_ON_ERROR) ?? []); + return $this->provider; } catch (JsonException|OCMProviderException $e) { // we ignore cache on issues } @@ -83,14 +72,14 @@ class OCMDiscoveryService implements IOCMDiscoveryService { $client = $this->clientService->newClient(); try { - $response = $client->get( - $remote . '/ocm-provider/', - [ - 'timeout' => 10, - 'verify' => !$this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates'), - 'connect_timeout' => 10, - ] - ); + $options = [ + 'timeout' => 10, + 'connect_timeout' => 10, + ]; + if ($this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates') === true) { + $options['verify'] = false; + } + $response = $client->get($remote . '/ocm-provider/', $options); if ($response->getStatusCode() === Http::STATUS_OK) { $body = $response->getBody(); @@ -99,8 +88,10 @@ class OCMDiscoveryService implements IOCMDiscoveryService { $this->cache->set($remote, $body, 60 * 60 * 24); } } catch (JsonException|OCMProviderException $e) { + $this->cache->set($remote, false, 5 * 60); throw new OCMProviderException('data returned by remote seems invalid - ' . ($body ?? '')); } catch (\Exception $e) { + $this->cache->set($remote, false, 5 * 60); $this->logger->warning('error while discovering ocm provider', [ 'exception' => $e, 'remote' => $remote @@ -108,30 +99,6 @@ class OCMDiscoveryService implements IOCMDiscoveryService { throw new OCMProviderException('error while requesting remote ocm provider'); } - if (!$this->supportedAPIVersion($this->provider->getApiVersion())) { - throw new OCMProviderException('API version not supported'); - } - return $this->provider; } - - /** - * Check the version from remote is supported. - * The minor version of the API will be ignored: - * 1.0.1 is identified as 1.0 - * - * @param string $version - * - * @return bool - */ - private function supportedAPIVersion(string $version): bool { - $dot1 = strpos($version, '.'); - $dot2 = strpos($version, '.', $dot1 + 1); - - if ($dot2 > 0) { - $version = substr($version, 0, $dot2); - } - - return (in_array($version, $this->supportedAPIVersion)); - } } diff --git a/lib/private/OCM/OCMSignatoryManager.php b/lib/private/OCM/OCMSignatoryManager.php new file mode 100644 index 00000000000..3b2cc595507 --- /dev/null +++ b/lib/private/OCM/OCMSignatoryManager.php @@ -0,0 +1,160 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\OCM; + +use NCU\Security\Signature\Enum\DigestAlgorithm; +use NCU\Security\Signature\Enum\SignatoryType; +use NCU\Security\Signature\Enum\SignatureAlgorithm; +use NCU\Security\Signature\Exceptions\IdentityNotFoundException; +use NCU\Security\Signature\ISignatoryManager; +use NCU\Security\Signature\ISignatureManager; +use NCU\Security\Signature\Model\Signatory; +use OC\Security\IdentityProof\Manager; +use OCP\IAppConfig; +use OCP\IURLGenerator; +use OCP\OCM\Exceptions\OCMProviderException; +use Psr\Log\LoggerInterface; + +/** + * @inheritDoc + * + * returns local signatory using IKeyPairManager + * extract optional signatory (keyId+public key) from ocm discovery service on remote instance + * + * @since 31.0.0 + */ +class OCMSignatoryManager implements ISignatoryManager { + public const PROVIDER_ID = 'ocm'; + public const APPCONFIG_SIGN_IDENTITY_EXTERNAL = 'ocm_signed_request_identity_external'; + public const APPCONFIG_SIGN_DISABLED = 'ocm_signed_request_disabled'; + public const APPCONFIG_SIGN_ENFORCED = 'ocm_signed_request_enforced'; + + public function __construct( + private readonly IAppConfig $appConfig, + private readonly ISignatureManager $signatureManager, + private readonly IURLGenerator $urlGenerator, + private readonly Manager $identityProofManager, + private readonly OCMDiscoveryService $ocmDiscoveryService, + private readonly LoggerInterface $logger, + ) { + } + + /** + * @inheritDoc + * + * @return string + * @since 31.0.0 + */ + public function getProviderId(): string { + return self::PROVIDER_ID; + } + + /** + * @inheritDoc + * + * @return array + * @since 31.0.0 + */ + public function getOptions(): array { + return [ + 'algorithm' => SignatureAlgorithm::RSA_SHA512, + 'digestAlgorithm' => DigestAlgorithm::SHA512, + 'extraSignatureHeaders' => [], + 'ttl' => 300, + 'dateHeader' => 'D, d M Y H:i:s T', + 'ttlSignatory' => 86400 * 3, + 'bodyMaxSize' => 50000, + ]; + } + + /** + * @inheritDoc + * + * @return Signatory + * @throws IdentityNotFoundException + * @since 31.0.0 + */ + public function getLocalSignatory(): Signatory { + /** + * TODO: manage multiple identity (external, internal, ...) to allow a limitation + * based on the requested interface (ie. only accept shares from globalscale) + */ + if ($this->appConfig->hasKey('core', self::APPCONFIG_SIGN_IDENTITY_EXTERNAL, true)) { + $identity = $this->appConfig->getValueString('core', self::APPCONFIG_SIGN_IDENTITY_EXTERNAL, lazy: true); + $keyId = 'https://' . $identity . '/ocm#signature'; + } else { + $keyId = $this->generateKeyId(); + } + + if (!$this->identityProofManager->hasAppKey('core', 'ocm_external')) { + $this->identityProofManager->generateAppKey('core', 'ocm_external', [ + 'algorithm' => 'rsa', + 'private_key_bits' => 2048, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + ]); + } + $keyPair = $this->identityProofManager->getAppKey('core', 'ocm_external'); + + $signatory = new Signatory(true); + $signatory->setKeyId($keyId); + $signatory->setPublicKey($keyPair->getPublic()); + $signatory->setPrivateKey($keyPair->getPrivate()); + return $signatory; + + } + + /** + * - tries to generate a keyId using global configuration (from signature manager) if available + * - generate a keyId using the current route to ocm shares + * + * @return string + * @throws IdentityNotFoundException + */ + private function generateKeyId(): string { + try { + return $this->signatureManager->generateKeyIdFromConfig('/ocm#signature'); + } catch (IdentityNotFoundException) { + } + + $url = $this->urlGenerator->linkToRouteAbsolute('cloud_federation_api.requesthandlercontroller.addShare'); + $identity = $this->signatureManager->extractIdentityFromUri($url); + + // catching possible subfolder to create a keyId like 'https://hostname/subfolder/ocm#signature + $path = parse_url($url, PHP_URL_PATH); + $pos = strpos($path, '/ocm/shares'); + $sub = ($pos) ? substr($path, 0, $pos) : ''; + + return 'https://' . $identity . $sub . '/ocm#signature'; + } + + /** + * @inheritDoc + * + * @param string $remote + * + * @return Signatory|null must be NULL if no signatory is found + * @since 31.0.0 + */ + public function getRemoteSignatory(string $remote): ?Signatory { + try { + $ocmProvider = $this->ocmDiscoveryService->discover($remote, true); + /** + * @experimental 31.0.0 + * @psalm-suppress UndefinedInterfaceMethod + */ + $signatory = $ocmProvider->getSignatory(); + $signatory?->setSignatoryType(SignatoryType::TRUSTED); + return $signatory; + } catch (OCMProviderException $e) { + $this->logger->warning('fail to get remote signatory', ['exception' => $e, 'remote' => $remote]); + return null; + } + } +} |