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.php297
-rw-r--r--lib/private/OCM/Model/OCMResource.php106
-rw-r--r--lib/private/OCM/OCMDiscoveryService.php104
-rw-r--r--lib/private/OCM/OCMSignatoryManager.php160
4 files changed, 667 insertions, 0 deletions
diff --git a/lib/private/OCM/Model/OCMProvider.php b/lib/private/OCM/Model/OCMProvider.php
new file mode 100644
index 00000000000..be13d65a40f
--- /dev/null
+++ b/lib/private/OCM/Model/OCMProvider.php
@@ -0,0 +1,297 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * 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\ICapabilityAwareOCMProvider;
+use OCP\OCM\IOCMResource;
+
+/**
+ * @since 28.0.0
+ */
+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');
+ }
+
+ /**
+ * @param bool $enabled
+ *
+ * @return $this
+ */
+ public function setEnabled(bool $enabled): static {
+ $this->enabled = $enabled;
+
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isEnabled(): bool {
+ return $this->enabled;
+ }
+
+ /**
+ * @param string $apiVersion
+ *
+ * @return $this
+ */
+ public function setApiVersion(string $apiVersion): static {
+ $this->apiVersion = $apiVersion;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getApiVersion(): string {
+ return $this->apiVersion;
+ }
+
+ /**
+ * 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
+ */
+ public function setEndPoint(string $endPoint): static {
+ $this->endPoint = $endPoint;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getEndPoint(): string {
+ return $this->endPoint;
+ }
+
+ /**
+ * @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
+ */
+ public function createNewResourceType(): IOCMResource {
+ return new OCMResource();
+ }
+
+ /**
+ * @param IOCMResource $resource
+ *
+ * @return $this
+ */
+ public function addResourceType(IOCMResource $resource): static {
+ $this->resourceTypes[] = $resource;
+
+ return $this;
+ }
+
+ /**
+ * @param IOCMResource[] $resourceTypes
+ *
+ * @return $this
+ */
+ public function setResourceTypes(array $resourceTypes): static {
+ $this->resourceTypes = $resourceTypes;
+
+ return $this;
+ }
+
+ /**
+ * @return IOCMResource[]
+ */
+ public function getResourceTypes(): array {
+ if (!$this->emittedEvent) {
+ $this->emittedEvent = true;
+ $event = new ResourceTypeRegisterEvent($this);
+ $this->dispatcher->dispatchTyped($event);
+ }
+
+ return $this->resourceTypes;
+ }
+
+ /**
+ * @param string $resourceName
+ * @param string $protocol
+ *
+ * @return string
+ * @throws OCMArgumentException
+ */
+ public function extractProtocolEntry(string $resourceName, string $protocol): string {
+ foreach ($this->getResourceTypes() as $resource) {
+ if ($resource->getName() === $resourceName) {
+ $entry = $resource->getProtocols()[$protocol] ?? null;
+ if (is_null($entry)) {
+ throw new OCMArgumentException('protocol not found');
+ }
+
+ return (string)$entry;
+ }
+ }
+
+ 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 OCMProvider&static
+ * @throws OCMProviderException in case a descent provider cannot be generated from data
+ */
+ public function import(array $data): static {
+ $this->setEnabled(is_bool($data['enabled'] ?? '') ? $data['enabled'] : false)
+ // Fall back to old apiVersion for Nextcloud 30 compatibility
+ ->setApiVersion((string)($data['version'] ?? $data['apiVersion'] ?? ''))
+ ->setEndPoint($data['endPoint'] ?? '');
+
+ $resources = [];
+ foreach (($data['resourceTypes'] ?? []) as $resourceData) {
+ $resource = new OCMResource();
+ $resources[] = $resource->import($resourceData);
+ }
+ $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');
+ }
+
+ return $this;
+ }
+
+
+ /**
+ * @return bool
+ */
+ private function looksValid(): bool {
+ return ($this->getApiVersion() !== '' && $this->getEndPoint() !== '');
+ }
+
+ /**
+ * @since 28.0.0
+ */
+ public function jsonSerialize(): array {
+ $resourceTypes = [];
+ foreach ($this->getResourceTypes() as $res) {
+ $resourceTypes[] = $res->jsonSerialize();
+ }
+
+ $response = [
+ 'enabled' => $this->isEnabled(),
+ '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
new file mode 100644
index 00000000000..3d619db1927
--- /dev/null
+++ b/lib/private/OCM/Model/OCMResource.php
@@ -0,0 +1,106 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\OCM\Model;
+
+use OCP\OCM\IOCMResource;
+
+/**
+ * @since 28.0.0
+ */
+class OCMResource implements IOCMResource {
+ private string $name = '';
+ /** @var list<string> */
+ private array $shareTypes = [];
+ /** @var array<string, string> */
+ private array $protocols = [];
+
+ /**
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setName(string $name): static {
+ $this->name = $name;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName(): string {
+ return $this->name;
+ }
+
+ /**
+ * @param list<string> $shareTypes
+ *
+ * @return $this
+ */
+ public function setShareTypes(array $shareTypes): static {
+ $this->shareTypes = $shareTypes;
+
+ return $this;
+ }
+
+ /**
+ * @return list<string>
+ */
+ public function getShareTypes(): array {
+ return $this->shareTypes;
+ }
+
+ /**
+ * @param array<string, string> $protocols
+ *
+ * @return $this
+ */
+ public function setProtocols(array $protocols): static {
+ $this->protocols = $protocols;
+
+ return $this;
+ }
+
+ /**
+ * @return array<string, string>
+ */
+ public function getProtocols(): array {
+ return $this->protocols;
+ }
+
+ /**
+ * import data from an array
+ *
+ * @param array $data
+ *
+ * @return $this
+ * @see self::jsonSerialize()
+ */
+ public function import(array $data): static {
+ return $this->setName((string)($data['name'] ?? ''))
+ ->setShareTypes($data['shareTypes'] ?? [])
+ ->setProtocols($data['protocols'] ?? []);
+ }
+
+ /**
+ * @return array{
+ * name: string,
+ * shareTypes: list<string>,
+ * protocols: array<string, string>
+ * }
+ */
+ public function jsonSerialize(): array {
+ return [
+ 'name' => $this->getName(),
+ 'shareTypes' => $this->getShareTypes(),
+ 'protocols' => $this->getProtocols()
+ ];
+ }
+}
diff --git a/lib/private/OCM/OCMDiscoveryService.php b/lib/private/OCM/OCMDiscoveryService.php
new file mode 100644
index 00000000000..a151bbc753c
--- /dev/null
+++ b/lib/private/OCM/OCMDiscoveryService.php
@@ -0,0 +1,104 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * 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;
+use OCP\ICache;
+use OCP\ICacheFactory;
+use OCP\IConfig;
+use OCP\OCM\Exceptions\OCMProviderException;
+use OCP\OCM\ICapabilityAwareOCMProvider;
+use OCP\OCM\IOCMDiscoveryService;
+use Psr\Log\LoggerInterface;
+
+/**
+ * @since 28.0.0
+ */
+class OCMDiscoveryService implements IOCMDiscoveryService {
+ private ICache $cache;
+
+ public function __construct(
+ ICacheFactory $cacheFactory,
+ private IClientService $clientService,
+ private IConfig $config,
+ private ICapabilityAwareOCMProvider $provider,
+ private LoggerInterface $logger,
+ ) {
+ $this->cache = $cacheFactory->createDistributed('ocm-discovery');
+ }
+
+
+ /**
+ * @param string $remote
+ * @param bool $skipCache
+ *
+ * @return ICapabilityAwareOCMProvider
+ * @throws OCMProviderException
+ */
+ 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 {
+ $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
+ }
+ }
+
+ $client = $this->clientService->newClient();
+ try {
+ $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();
+ // update provider with data returned by the request
+ $this->provider->import(json_decode($body, true, 8, JSON_THROW_ON_ERROR) ?? []);
+ $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
+ ]);
+ throw new OCMProviderException('error while requesting remote ocm provider');
+ }
+
+ return $this->provider;
+ }
+}
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;
+ }
+ }
+}