aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/OCM
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/OCM')
-rw-r--r--lib/private/OCM/Model/OCMProvider.php120
-rw-r--r--lib/private/OCM/Model/OCMResource.php12
-rw-r--r--lib/private/OCM/OCMDiscoveryService.php74
-rw-r--r--lib/private/OCM/OCMSignatoryManager.php160
4 files changed, 295 insertions, 71 deletions
diff --git a/lib/private/OCM/Model/OCMProvider.php b/lib/private/OCM/Model/OCMProvider.php
index 17a356428f7..be13d65a40f 100644
--- a/lib/private/OCM/Model/OCMProvider.php
+++ b/lib/private/OCM/Model/OCMProvider.php
@@ -9,28 +9,35 @@ declare(strict_types=1);
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');
}
/**
@@ -70,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
@@ -88,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
*/
@@ -152,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) {
@@ -173,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');
}
@@ -188,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 = [];
@@ -207,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 c69763ca4ba..3d619db1927 100644
--- a/lib/private/OCM/Model/OCMResource.php
+++ b/lib/private/OCM/Model/OCMResource.php
@@ -16,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 = [];
@@ -40,7 +40,7 @@ class OCMResource implements IOCMResource {
}
/**
- * @param string[] $shareTypes
+ * @param list<string> $shareTypes
*
* @return $this
*/
@@ -51,7 +51,7 @@ class OCMResource implements IOCMResource {
}
/**
- * @return string[]
+ * @return list<string>
*/
public function getShareTypes(): array {
return $this->shareTypes;
@@ -85,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 62313a9af80..a151bbc753c 100644
--- a/lib/private/OCM/OCMDiscoveryService.php
+++ b/lib/private/OCM/OCMDiscoveryService.php
@@ -9,6 +9,7 @@ declare(strict_types=1);
namespace OC\OCM;
+use GuzzleHttp\Exception\ConnectException;
use JsonException;
use OCP\AppFramework\Http;
use OCP\Http\Client\IClientService;
@@ -16,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;
/**
@@ -25,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');
@@ -47,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
}
@@ -66,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();
@@ -82,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
@@ -91,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;
+ }
+ }
+}