mirror of
https://github.com/nextcloud/server.git
synced 2026-06-29 12:24:50 +02:00
4d5841761f
Shares using the OCM multi-protocol envelope (name multi, with the secret carried in a sibling protocol entry such as webdav) were rejected with Missing sharedSecret in protocol. Scan every protocol entry for the shared secret during validation, resolve the secret from the matching entry, and let the files provider serve the webdav entry of a multi envelope. Covers the file and folder resource types. Signed-off-by: Micke Nordin <kano@sunet.se>
826 lines
28 KiB
PHP
826 lines
28 KiB
PHP
<?php
|
|
|
|
/**
|
|
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
namespace OCA\FederatedFileSharing\OCM;
|
|
|
|
use OC\AppFramework\Http;
|
|
use OC\Files\Filesystem;
|
|
use OC\OCM\OCMSignatoryManager;
|
|
use OC\OCM\Rfc9421SignatoryManager;
|
|
use OCA\FederatedFileSharing\AddressHandler;
|
|
use OCA\FederatedFileSharing\FederatedShareProvider;
|
|
use OCA\Federation\TrustedServers;
|
|
use OCA\Files_Sharing\Activity\Providers\RemoteShares;
|
|
use OCA\Files_Sharing\External\ExternalShare;
|
|
use OCA\Files_Sharing\External\ExternalShareMapper;
|
|
use OCA\Files_Sharing\External\Manager;
|
|
use OCA\GlobalSiteSelector\Service\SlaveService;
|
|
use OCA\Polls\Db\Share;
|
|
use OCP\Activity\IManager as IActivityManager;
|
|
use OCP\App\IAppManager;
|
|
use OCP\Constants;
|
|
use OCP\Federation\Exceptions\ActionNotSupportedException;
|
|
use OCP\Federation\Exceptions\AuthenticationFailedException;
|
|
use OCP\Federation\Exceptions\BadRequestException;
|
|
use OCP\Federation\Exceptions\ProviderCouldNotAddShareException;
|
|
use OCP\Federation\ICloudFederationFactory;
|
|
use OCP\Federation\ICloudFederationProviderManager;
|
|
use OCP\Federation\ICloudFederationShare;
|
|
use OCP\Federation\ICloudIdManager;
|
|
use OCP\Federation\ISignedCloudFederationProvider;
|
|
use OCP\Files\IFilenameValidator;
|
|
use OCP\Files\ISetupManager;
|
|
use OCP\Files\NotFoundException;
|
|
use OCP\HintException;
|
|
use OCP\Http\Client\IClientService;
|
|
use OCP\IAppConfig;
|
|
use OCP\IConfig;
|
|
use OCP\IGroupManager;
|
|
use OCP\IURLGenerator;
|
|
use OCP\IUser;
|
|
use OCP\IUserManager;
|
|
use OCP\Notification\IManager as INotificationManager;
|
|
use OCP\OCM\IOCMDiscoveryService;
|
|
use OCP\Security\Signature\ISignatureManager;
|
|
use OCP\Server;
|
|
use OCP\Share\Exceptions\ShareNotFound;
|
|
use OCP\Share\IManager;
|
|
use OCP\Share\IProviderFactory;
|
|
use OCP\Share\IShare;
|
|
use OCP\Util;
|
|
use Override;
|
|
use Psr\Log\LoggerInterface;
|
|
use SensitiveParameter;
|
|
|
|
class CloudFederationProviderFiles implements ISignedCloudFederationProvider {
|
|
public function __construct(
|
|
private readonly IAppManager $appManager,
|
|
private readonly FederatedShareProvider $federatedShareProvider,
|
|
private readonly AddressHandler $addressHandler,
|
|
private readonly IUserManager $userManager,
|
|
private readonly IManager $shareManager,
|
|
private readonly ICloudIdManager $cloudIdManager,
|
|
private readonly IActivityManager $activityManager,
|
|
private readonly INotificationManager $notificationManager,
|
|
private readonly IURLGenerator $urlGenerator,
|
|
private readonly ICloudFederationFactory $cloudFederationFactory,
|
|
private readonly ICloudFederationProviderManager $cloudFederationProviderManager,
|
|
private readonly IGroupManager $groupManager,
|
|
private readonly IConfig $config,
|
|
private readonly Manager $externalShareManager,
|
|
private readonly LoggerInterface $logger,
|
|
private readonly IFilenameValidator $filenameValidator,
|
|
private readonly IProviderFactory $shareProviderFactory,
|
|
private readonly ISetupManager $setupManager,
|
|
private readonly ExternalShareMapper $externalShareMapper,
|
|
private readonly IOCMDiscoveryService $discoveryService,
|
|
private readonly IClientService $clientService,
|
|
private readonly ISignatureManager $signatureManager,
|
|
private readonly OCMSignatoryManager $signatoryManager,
|
|
private readonly IAppConfig $appConfig,
|
|
) {
|
|
}
|
|
|
|
#[Override]
|
|
public function getShareType(): string {
|
|
return 'file';
|
|
}
|
|
|
|
#[Override]
|
|
public function shareReceived(ICloudFederationShare $share): string {
|
|
if (!$this->isS2SEnabled(true)) {
|
|
throw new ProviderCouldNotAddShareException('Server does not support federated cloud sharing', '', Http::STATUS_SERVICE_UNAVAILABLE);
|
|
}
|
|
|
|
$protocol = $share->getProtocol();
|
|
$protocolName = $protocol['name'] ?? '';
|
|
// The files provider serves the webdav protocol. It is named directly in the
|
|
// legacy and new single-protocol formats, and carried as a sibling entry in
|
|
// the OCM multi-protocol envelope (name => 'multi').
|
|
if ($protocolName === 'multi') {
|
|
if (!isset($protocol['webdav']) || !is_array($protocol['webdav'])) {
|
|
throw new ProviderCouldNotAddShareException('Unsupported protocol for data exchange.', '', Http::STATUS_NOT_IMPLEMENTED);
|
|
}
|
|
} elseif ($protocolName !== 'webdav') {
|
|
throw new ProviderCouldNotAddShareException('Unsupported protocol for data exchange.', '', Http::STATUS_NOT_IMPLEMENTED);
|
|
}
|
|
|
|
[, $remote] = $this->addressHandler->splitUserRemote($share->getOwner());
|
|
|
|
// for backward compatibility make sure that the remote url stored in the
|
|
// database ends with a trailing slash
|
|
if (!str_ends_with($remote, '/')) {
|
|
$remote = $remote . '/';
|
|
}
|
|
|
|
$token = $share->getShareSecret();
|
|
$name = $share->getResourceName();
|
|
$owner = $share->getOwnerDisplayName() ?: $share->getOwner();
|
|
$shareWith = $share->getShareWith();
|
|
$remoteId = $share->getProviderId();
|
|
$sharedByFederatedId = $share->getSharedBy();
|
|
$ownerFederatedId = $share->getOwner();
|
|
$shareType = $this->mapShareTypeToNextcloud($share->getShareType());
|
|
|
|
// Check for must-exchange-token requirement
|
|
$requirements = $protocol['webdav']['requirements'] ?? $protocol['options']['requirements'] ?? [];
|
|
$mustExchangeToken = in_array('must-exchange-token', $requirements);
|
|
$accessToken = '';
|
|
|
|
if ($mustExchangeToken) {
|
|
// Exchange the sharedSecret for an access token (required)
|
|
$accessToken = $this->exchangeToken($remote, $token);
|
|
if ($accessToken === null) {
|
|
throw new ProviderCouldNotAddShareException('Failed to exchange token as required by must-exchange-token', '', Http::STATUS_BAD_REQUEST);
|
|
}
|
|
} else {
|
|
// Check if remote has exchange-token capability and try to exchange (optional)
|
|
try {
|
|
$ocmProvider = $this->discoveryService->discover(rtrim($remote, '/'));
|
|
if ($ocmProvider->getCapabilities()->hasExchangeToken()) {
|
|
$accessToken = $this->exchangeToken($remote, $token) ?? '';
|
|
$this->logger->debug('Exchanged token for remote with exchange-token capability', ['remote' => $remote, 'success' => !empty($accessToken)]);
|
|
}
|
|
} catch (\Exception $e) {
|
|
$this->logger->debug('Could not discover remote capabilities for token exchange', ['remote' => $remote, 'exception' => $e]);
|
|
}
|
|
}
|
|
|
|
// if no explicit information about the person who created the share was sent
|
|
// we assume that the share comes from the owner
|
|
if ($sharedByFederatedId === null) {
|
|
$sharedByFederatedId = $ownerFederatedId;
|
|
}
|
|
|
|
if ($remote && $token && $name && $owner && $remoteId && $shareWith) {
|
|
if (!$this->filenameValidator->isFilenameValid($name)) {
|
|
throw new ProviderCouldNotAddShareException('The mountpoint name contains invalid characters.', '', Http::STATUS_BAD_REQUEST);
|
|
}
|
|
|
|
$user = null;
|
|
$group = null;
|
|
|
|
if ($shareType === IShare::TYPE_USER) {
|
|
$this->logger->debug('shareWith before, ' . $shareWith, ['app' => 'files_sharing']);
|
|
Util::emitHook(
|
|
'\OCA\Files_Sharing\API\Server2Server',
|
|
'preLoginNameUsedAsUserName',
|
|
['uid' => &$shareWith]
|
|
);
|
|
$this->logger->debug('shareWith after, ' . $shareWith, ['app' => 'files_sharing']);
|
|
|
|
$user = $this->userManager->get($shareWith);
|
|
if ($user === null) {
|
|
throw new ProviderCouldNotAddShareException('User does not exists', '', Http::STATUS_BAD_REQUEST);
|
|
}
|
|
|
|
$this->setupManager->setupForUser($user);
|
|
} else {
|
|
$group = $this->groupManager->get($shareWith);
|
|
if ($group === null) {
|
|
throw new ProviderCouldNotAddShareException('Group does not exists', '', Http::STATUS_BAD_REQUEST);
|
|
}
|
|
}
|
|
|
|
$externalShare = new ExternalShare();
|
|
$externalShare->generateId();
|
|
$externalShare->setRemote($remote);
|
|
$externalShare->setRemoteId($remoteId);
|
|
$externalShare->setRefreshToken($token); // refresh token (sharedSecret)
|
|
$externalShare->setAccessToken($accessToken ?: null);
|
|
$externalShare->setName($name);
|
|
$externalShare->setOwner($owner);
|
|
$externalShare->setShareType($shareType);
|
|
$externalShare->setAccepted(IShare::STATUS_PENDING);
|
|
|
|
try {
|
|
$this->externalShareManager->addShare($externalShare, $user ?: $group);
|
|
|
|
// get DisplayName about the owner of the share
|
|
$ownerDisplayName = $this->getUserDisplayName($ownerFederatedId);
|
|
|
|
$trustedServers = null;
|
|
if ($this->appManager->isEnabledForAnyone('federation')
|
|
&& class_exists(TrustedServers::class)) {
|
|
try {
|
|
$trustedServers = Server::get(TrustedServers::class);
|
|
} catch (\Throwable $e) {
|
|
$this->logger->debug('Failed to create TrustedServers', ['exception' => $e]);
|
|
}
|
|
}
|
|
|
|
if ($shareType === IShare::TYPE_USER) {
|
|
$event = $this->activityManager->generateEvent();
|
|
$event->setApp('files_sharing')
|
|
->setType('remote_share')
|
|
->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_RECEIVED, [$ownerFederatedId, trim($name, '/'), $ownerDisplayName])
|
|
->setAffectedUser($shareWith)
|
|
->setObject('remote_share', (string)$externalShare->getId(), $name);
|
|
Server::get(IActivityManager::class)->publish($event);
|
|
$this->notifyAboutNewShare($shareWith, (string)$externalShare->getId(), $ownerFederatedId, $sharedByFederatedId, $name, $ownerDisplayName);
|
|
|
|
// If auto-accept is enabled, accept the share
|
|
if ($this->federatedShareProvider->isFederatedTrustedShareAutoAccept() && $trustedServers?->isTrustedServer($remote) === true) {
|
|
$this->externalShareManager->acceptShare($externalShare, $user);
|
|
}
|
|
} else {
|
|
$groupMembers = $group->getUsers();
|
|
foreach ($groupMembers as $user) {
|
|
$event = $this->activityManager->generateEvent();
|
|
$event->setApp('files_sharing')
|
|
->setType('remote_share')
|
|
->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_RECEIVED, [$ownerFederatedId, trim($name, '/'), $ownerDisplayName])
|
|
->setAffectedUser($user->getUID())
|
|
->setObject('remote_share', (string)$externalShare->getId(), $name);
|
|
Server::get(IActivityManager::class)->publish($event);
|
|
$this->notifyAboutNewShare($user->getUID(), (string)$externalShare->getId(), $ownerFederatedId, $sharedByFederatedId, $name, $ownerDisplayName);
|
|
|
|
// If auto-accept is enabled, accept the share
|
|
if ($this->federatedShareProvider->isFederatedTrustedShareAutoAccept() && $trustedServers?->isTrustedServer($remote) === true) {
|
|
$this->externalShareManager->acceptShare($externalShare, $user);
|
|
}
|
|
}
|
|
}
|
|
|
|
return (string)$externalShare->getId();
|
|
} catch (\Exception $e) {
|
|
$this->logger->error('Server can not add remote share.', [
|
|
'app' => 'files_sharing',
|
|
'exception' => $e,
|
|
]);
|
|
throw new ProviderCouldNotAddShareException('internal server error, was not able to add share from ' . $remote, '', HTTP::STATUS_INTERNAL_SERVER_ERROR);
|
|
}
|
|
}
|
|
|
|
throw new ProviderCouldNotAddShareException('server can not add remote share, missing parameter', '', HTTP::STATUS_BAD_REQUEST);
|
|
}
|
|
|
|
#[Override]
|
|
public function notificationReceived(string $notificationType, string $providerId, array $notification): array {
|
|
return match ($notificationType) {
|
|
'SHARE_ACCEPTED' => $this->shareAccepted($providerId, $notification),
|
|
'SHARE_DECLINED' => $this->shareDeclined($providerId, $notification),
|
|
'SHARE_UNSHARED' => $this->unshare($providerId, $notification),
|
|
'REQUEST_RESHARE' => $this->reshareRequested($providerId, $notification),
|
|
'RESHARE_UNDO' => $this->undoReshare($providerId, $notification),
|
|
'RESHARE_CHANGE_PERMISSION' => $this->updateResharePermissions($providerId, $notification),
|
|
default => throw new BadRequestException([$notificationType]),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Map OCM share type (strings) to Nextcloud internal share types (integer)
|
|
* @return IShare::TYPE_GROUP|IShare::TYPE_USER
|
|
*/
|
|
private function mapShareTypeToNextcloud(string $shareType): int {
|
|
return $shareType === 'group' ? IShare::TYPE_GROUP : IShare::TYPE_USER;
|
|
}
|
|
|
|
private function notifyAboutNewShare(string $shareWith, string $shareId, $ownerFederatedId, $sharedByFederatedId, string $name, string $displayName): void {
|
|
$notification = $this->notificationManager->createNotification();
|
|
$notification->setApp('files_sharing')
|
|
->setUser($shareWith)
|
|
->setDateTime(new \DateTime())
|
|
->setObject('remote_share', $shareId)
|
|
->setSubject('remote_share', [$ownerFederatedId, $sharedByFederatedId, trim($name, '/'), $displayName]);
|
|
|
|
$declineAction = $notification->createAction();
|
|
$declineAction->setLabel('decline')
|
|
->setLink($this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkTo('', 'ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending/' . $shareId)), 'DELETE');
|
|
$notification->addAction($declineAction);
|
|
|
|
$acceptAction = $notification->createAction();
|
|
$acceptAction->setLabel('accept')
|
|
->setLink($this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkTo('', 'ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending/' . $shareId)), 'POST');
|
|
$notification->addAction($acceptAction);
|
|
|
|
$this->notificationManager->notify($notification);
|
|
}
|
|
|
|
/**
|
|
* process notification that the recipient accepted a share
|
|
*
|
|
* @param array{sharedSecret?: string} $notification
|
|
* @return array<string>
|
|
* @throws ActionNotSupportedException
|
|
* @throws AuthenticationFailedException
|
|
* @throws BadRequestException
|
|
* @throws HintException
|
|
*/
|
|
private function shareAccepted(string $id, array $notification): array {
|
|
if (!$this->isS2SEnabled()) {
|
|
throw new ActionNotSupportedException('Server does not support federated cloud sharing');
|
|
}
|
|
|
|
if (!isset($notification['sharedSecret'])) {
|
|
throw new BadRequestException(['sharedSecret']);
|
|
}
|
|
|
|
$token = $notification['sharedSecret'];
|
|
|
|
$share = $this->federatedShareProvider->getShareById($id);
|
|
|
|
$this->verifyShare($share, $token);
|
|
$this->executeAcceptShare($share);
|
|
|
|
if ($share->getShareOwner() !== $share->getSharedBy()
|
|
&& !$this->userManager->userExists($share->getSharedBy())) {
|
|
// only if share was initiated from another instance
|
|
[, $remote] = $this->addressHandler->splitUserRemote($share->getSharedBy());
|
|
$remoteId = $this->federatedShareProvider->getRemoteId($share);
|
|
$notification = $this->cloudFederationFactory->getCloudFederationNotification();
|
|
$notification->setMessage(
|
|
'SHARE_ACCEPTED',
|
|
'file',
|
|
$remoteId,
|
|
[
|
|
'sharedSecret' => $token,
|
|
'message' => 'Recipient accepted the re-share'
|
|
]
|
|
);
|
|
$this->cloudFederationProviderManager->sendNotification($remote, $notification);
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @throws ShareNotFound
|
|
*/
|
|
protected function executeAcceptShare(IShare $share): void {
|
|
$user = $this->getCorrectUser($share);
|
|
|
|
try {
|
|
$fileId = $share->getNode()->getId();
|
|
[$file, $link] = $this->getFile($user, $fileId);
|
|
} catch (\Exception) {
|
|
throw new ShareNotFound();
|
|
}
|
|
|
|
$event = $this->activityManager->generateEvent();
|
|
$event->setApp('files_sharing')
|
|
->setType('remote_share')
|
|
->setAffectedUser($user->getUID())
|
|
->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_ACCEPTED, [$share->getSharedWith(), [$fileId => $file]])
|
|
->setObject('files', $fileId, $file)
|
|
->setLink($link);
|
|
$this->activityManager->publish($event);
|
|
}
|
|
|
|
/**
|
|
* process notification that the recipient declined a share
|
|
*
|
|
* @param array{sharedSecret?: string} $notification
|
|
* @return array<string>
|
|
* @throws ActionNotSupportedException
|
|
* @throws AuthenticationFailedException
|
|
* @throws BadRequestException
|
|
* @throws ShareNotFound
|
|
* @throws HintException
|
|
*
|
|
*/
|
|
protected function shareDeclined(string $id, array $notification): array {
|
|
if (!$this->isS2SEnabled()) {
|
|
throw new ActionNotSupportedException('Server does not support federated cloud sharing');
|
|
}
|
|
|
|
if (!isset($notification['sharedSecret'])) {
|
|
throw new BadRequestException(['sharedSecret']);
|
|
}
|
|
|
|
$token = $notification['sharedSecret'];
|
|
|
|
$share = $this->federatedShareProvider->getShareById($id);
|
|
|
|
$this->verifyShare($share, $token);
|
|
|
|
if ($share->getShareOwner() !== $share->getSharedBy()) {
|
|
[, $remote] = $this->addressHandler->splitUserRemote($share->getSharedBy());
|
|
$remoteId = $this->federatedShareProvider->getRemoteId($share);
|
|
$notification = $this->cloudFederationFactory->getCloudFederationNotification();
|
|
$notification->setMessage(
|
|
'SHARE_DECLINED',
|
|
'file',
|
|
$remoteId,
|
|
[
|
|
'sharedSecret' => $token,
|
|
'message' => 'Recipient declined the re-share'
|
|
]
|
|
);
|
|
$this->cloudFederationProviderManager->sendNotification($remote, $notification);
|
|
}
|
|
|
|
$this->executeDeclineShare($share);
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* delete declined share and create a activity
|
|
*
|
|
* @throws ShareNotFound
|
|
*/
|
|
protected function executeDeclineShare(IShare $share): void {
|
|
$this->federatedShareProvider->removeShareFromTable($share->getId());
|
|
|
|
$user = $this->getCorrectUser($share);
|
|
|
|
try {
|
|
$fileId = $share->getNode()->getId();
|
|
[$file, $link] = $this->getFile($user, $fileId);
|
|
} catch (\Exception) {
|
|
throw new ShareNotFound();
|
|
}
|
|
|
|
$event = $this->activityManager->generateEvent();
|
|
$event->setApp('files_sharing')
|
|
->setType('remote_share')
|
|
->setAffectedUser($user->getUID())
|
|
->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_DECLINED, [$share->getSharedWith(), [$fileId => $file]])
|
|
->setObject('files', $fileId, $file)
|
|
->setLink($link);
|
|
$this->activityManager->publish($event);
|
|
}
|
|
|
|
/**
|
|
* received the notification that the owner unshared a file from you
|
|
*
|
|
* @param array{sharedSecret?: string} $notification
|
|
* @return array<string>
|
|
* @throws AuthenticationFailedException
|
|
* @throws BadRequestException
|
|
*/
|
|
private function undoReshare(string $id, array $notification): array {
|
|
if (!isset($notification['sharedSecret'])) {
|
|
throw new BadRequestException(['sharedSecret']);
|
|
}
|
|
$token = $notification['sharedSecret'];
|
|
|
|
$share = $this->federatedShareProvider->getShareById($id);
|
|
|
|
$this->verifyShare($share, $token);
|
|
$this->federatedShareProvider->removeShareFromTable($share->getId());
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* unshare file from self
|
|
*
|
|
* @param array{sharedSecret?: string} $notification
|
|
* @return array<string>
|
|
* @throws ActionNotSupportedException
|
|
* @throws BadRequestException
|
|
*/
|
|
private function unshare(string $id, array $notification): array {
|
|
if (!$this->isS2SEnabled(true)) {
|
|
throw new ActionNotSupportedException('incoming shares disabled!');
|
|
}
|
|
|
|
if (!isset($notification['sharedSecret'])) {
|
|
throw new BadRequestException(['sharedSecret']);
|
|
}
|
|
$token = $notification['sharedSecret'];
|
|
|
|
$share = $this->externalShareMapper->getShareByRemoteIdAndToken($id, $token);
|
|
|
|
if ($token && $id && $share !== null) {
|
|
$remote = $this->cleanupRemote($share->getRemote());
|
|
|
|
$owner = $this->cloudIdManager->getCloudId($share->getOwner(), $remote);
|
|
$mountpoint = $share->getMountpoint();
|
|
$user = $share->getUser();
|
|
|
|
$this->externalShareMapper->delete($share);
|
|
|
|
$ownerDisplayName = $this->getUserDisplayName($owner->getId());
|
|
|
|
if ($share->getShareType() === IShare::TYPE_USER) {
|
|
if ($share->getAccepted()) {
|
|
$path = trim($mountpoint, '/');
|
|
} else {
|
|
$path = trim($share->getName(), '/');
|
|
}
|
|
$notification = $this->notificationManager->createNotification();
|
|
$notification->setApp('files_sharing')
|
|
->setUser($share->getUser())
|
|
->setObject('remote_share', (string)$share->getId());
|
|
$this->notificationManager->markProcessed($notification);
|
|
|
|
$event = $this->activityManager->generateEvent();
|
|
$event->setApp('files_sharing')
|
|
->setType('remote_share')
|
|
->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_UNSHARED, [$owner->getId(), $path, $ownerDisplayName])
|
|
->setAffectedUser($user)
|
|
->setObject('remote_share', $share->getId(), $path);
|
|
Server::get(IActivityManager::class)->publish($event);
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
private function cleanupRemote(string $remote): string {
|
|
$remote = substr($remote, strpos($remote, '://') + 3);
|
|
|
|
return rtrim($remote, '/');
|
|
}
|
|
|
|
/**
|
|
* recipient of a share request to re-share the file with another user
|
|
*
|
|
* @param array{sharedSecret?: string, shareWith?: string, senderId?: string} $notification
|
|
* @return array<string>
|
|
* @throws AuthenticationFailedException
|
|
* @throws BadRequestException
|
|
* @throws ProviderCouldNotAddShareException
|
|
* @throws ShareNotFound
|
|
*/
|
|
protected function reshareRequested(string $id, array $notification) {
|
|
if (!isset($notification['sharedSecret'])) {
|
|
throw new BadRequestException(['sharedSecret']);
|
|
}
|
|
$token = $notification['sharedSecret'];
|
|
|
|
if (!isset($notification['shareWith'])) {
|
|
throw new BadRequestException(['shareWith']);
|
|
}
|
|
$shareWith = $notification['shareWith'];
|
|
|
|
if (!isset($notification['senderId'])) {
|
|
throw new BadRequestException(['senderId']);
|
|
}
|
|
$senderId = $notification['senderId'];
|
|
|
|
$share = $this->federatedShareProvider->getShareById($id);
|
|
|
|
// We have to respect the default share permissions
|
|
$permissions = $share->getPermissions() & (int)$this->config->getAppValue('core', 'shareapi_default_permissions', (string)Constants::PERMISSION_ALL);
|
|
$share->setPermissions($permissions);
|
|
|
|
// don't allow to share a file back to the owner
|
|
try {
|
|
[$user, $remote] = $this->addressHandler->splitUserRemote($shareWith);
|
|
$owner = $share->getShareOwner();
|
|
$currentServer = $this->addressHandler->generateRemoteURL();
|
|
if ($this->addressHandler->compareAddresses($user, $remote, $owner, $currentServer)) {
|
|
throw new ProviderCouldNotAddShareException('Resharing back to the owner is not allowed: ' . $id);
|
|
}
|
|
} catch (\Exception $e) {
|
|
throw new ProviderCouldNotAddShareException($e->getMessage());
|
|
}
|
|
|
|
$this->verifyShare($share, $token);
|
|
|
|
// check if re-sharing is allowed
|
|
if ($share->getPermissions() & Constants::PERMISSION_SHARE) {
|
|
// the recipient of the initial share is now the initiator for the re-share
|
|
$share->setSharedBy($share->getSharedWith());
|
|
$share->setSharedWith($shareWith);
|
|
$result = $this->federatedShareProvider->create($share);
|
|
$this->federatedShareProvider->storeRemoteId($result->getId(), $senderId);
|
|
return ['token' => $result->getToken(), 'providerId' => $result->getId()];
|
|
} else {
|
|
throw new ProviderCouldNotAddShareException('resharing not allowed for share: ' . $id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update permission of a re-share so that the share dialog shows the right
|
|
* permission if the owner or the sender changes the permission
|
|
*
|
|
* @return string[]
|
|
* @throws AuthenticationFailedException
|
|
* @throws BadRequestException
|
|
*/
|
|
protected function updateResharePermissions(string $id, array $notification): array {
|
|
throw new HintException('Updating reshares not allowed');
|
|
}
|
|
|
|
/**
|
|
* @return list{?string, string} with internal path of the file and a absolute link to it
|
|
*/
|
|
private function getFile(IUser $user, int $fileSource): array {
|
|
$this->setupManager->setupForUser($user);
|
|
|
|
try {
|
|
$file = Filesystem::getPath($fileSource);
|
|
} catch (NotFoundException $e) {
|
|
$file = null;
|
|
}
|
|
$args = Filesystem::is_dir($file) ? ['dir' => $file] : ['dir' => dirname($file), 'scrollto' => $file];
|
|
$urlGenerator = Server::get(IURLGenerator::class);
|
|
$link = $urlGenerator->getAbsoluteURL(
|
|
$urlGenerator->linkTo('files', 'index.php', $args)
|
|
);
|
|
|
|
return [$file, $link];
|
|
}
|
|
|
|
/**
|
|
* Check if we are the initiator or the owner of a re-share and return the correct UID
|
|
*/
|
|
protected function getCorrectUser(IShare $share): IUser {
|
|
if ($user = $this->userManager->get($share->getShareOwner())) {
|
|
return $user;
|
|
}
|
|
|
|
$user = $this->userManager->get($share->getSharedBy());
|
|
if ($user === null) {
|
|
throw new \RuntimeException('Neither the share owner or the share initiator exist');
|
|
}
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* check if we got the right share
|
|
*
|
|
* @throws AuthenticationFailedException
|
|
*/
|
|
protected function verifyShare(IShare $share, string $token): bool {
|
|
if (
|
|
$share->getShareType() === IShare::TYPE_REMOTE
|
|
&& $share->getToken() === $token
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
if ($share->getShareType() === IShare::TYPE_CIRCLE) {
|
|
try {
|
|
$knownShare = $this->shareManager->getShareByToken($token);
|
|
if ($knownShare->getId() === $share->getId()) {
|
|
return true;
|
|
}
|
|
} catch (ShareNotFound $e) {
|
|
}
|
|
}
|
|
|
|
throw new AuthenticationFailedException();
|
|
}
|
|
|
|
/**
|
|
* Check if server-to-server sharing is enabled
|
|
*/
|
|
private function isS2SEnabled(bool $incoming = false): bool {
|
|
$result = $this->appManager->isEnabledForUser('files_sharing');
|
|
|
|
if ($incoming) {
|
|
$result = $result && $this->federatedShareProvider->isIncomingServer2serverShareEnabled();
|
|
} else {
|
|
$result = $result && $this->federatedShareProvider->isOutgoingServer2serverShareEnabled();
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
#[Override]
|
|
public function getSupportedShareTypes(): array {
|
|
return ['user', 'group'];
|
|
}
|
|
|
|
public function getUserDisplayName(string $userId): string {
|
|
// check if gss is enabled and available
|
|
if (!$this->appManager->isEnabledForAnyone('globalsiteselector')
|
|
|| !class_exists('\OCA\GlobalSiteSelector\Service\SlaveService')) {
|
|
return '';
|
|
}
|
|
|
|
try {
|
|
$slaveService = Server::get(SlaveService::class);
|
|
} catch (\Throwable $e) {
|
|
$this->logger->error(
|
|
$e->getMessage(),
|
|
['exception' => $e]
|
|
);
|
|
return '';
|
|
}
|
|
|
|
return $slaveService->getUserDisplayName($this->cloudIdManager->removeProtocolFromUrl($userId), false);
|
|
}
|
|
|
|
#[Override]
|
|
public function getFederationIdFromSharedSecret(
|
|
#[SensitiveParameter]
|
|
string $sharedSecret,
|
|
array $payload,
|
|
): string {
|
|
$provider = $this->shareProviderFactory->getProviderForType(IShare::TYPE_REMOTE);
|
|
try {
|
|
$share = $provider->getShareByToken($sharedSecret);
|
|
} catch (ShareNotFound) {
|
|
// Maybe we're dealing with a share federated from another server
|
|
$share = $this->externalShareManager->getShareByToken($sharedSecret);
|
|
if ($share === false) {
|
|
return '';
|
|
}
|
|
|
|
return $share->getUser() . '@' . $share->getRemote();
|
|
}
|
|
|
|
// if uid_owner is a local account, the request comes from the recipient
|
|
// if not, request comes from the instance that owns the share and recipient is the re-sharer
|
|
if ($this->userManager->get($share->getShareOwner()) !== null) {
|
|
return $share->getSharedWith();
|
|
} else {
|
|
return $share->getShareOwner();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exchange a sharedSecret (refresh token) for an access token via the remote server's token endpoint
|
|
*
|
|
* @param string $remote The remote server URL
|
|
* @param string $sharedSecret The shared secret to exchange
|
|
* @return string|null The access token, or null on failure
|
|
*/
|
|
private function exchangeToken(string $remote, #[SensitiveParameter] string $sharedSecret): ?string {
|
|
try {
|
|
$ocmProvider = $this->discoveryService->discover(rtrim($remote, '/'));
|
|
$tokenEndpoint = $ocmProvider->getTokenEndPoint();
|
|
|
|
if ($tokenEndpoint === '') {
|
|
$this->logger->warning('Remote server does not expose tokenEndPoint', ['remote' => $remote]);
|
|
return null;
|
|
}
|
|
|
|
$client = $this->clientService->newClient();
|
|
$clientId = parse_url($this->urlGenerator->getAbsoluteURL('/'), PHP_URL_HOST);
|
|
|
|
$payload = [
|
|
'grant_type' => 'authorization_code',
|
|
'client_id' => $clientId,
|
|
'code' => $sharedSecret,
|
|
];
|
|
|
|
$options = [
|
|
'body' => http_build_query($payload),
|
|
'headers' => [
|
|
'Content-Type' => 'application/x-www-form-urlencoded',
|
|
],
|
|
'timeout' => 10,
|
|
'connect_timeout' => 10,
|
|
];
|
|
|
|
try {
|
|
$options = $this->signatureManager->signOutgoingRequestIClientPayload(
|
|
new Rfc9421SignatoryManager($this->signatoryManager),
|
|
$options,
|
|
'post',
|
|
$tokenEndpoint
|
|
);
|
|
$this->logger->debug('Token request signed successfully', ['remote' => $remote]);
|
|
} catch (\Exception $e) {
|
|
$this->logger->error('Failed to sign token request', [
|
|
'remote' => $remote,
|
|
'exception' => $e,
|
|
'endpoint' => $tokenEndpoint,
|
|
]);
|
|
return null;
|
|
}
|
|
|
|
$response = $client->post($tokenEndpoint, $options);
|
|
|
|
$statusCode = $response->getStatusCode();
|
|
if ($statusCode !== 200) {
|
|
$this->logger->warning('Token exchange returned unexpected HTTP status', [
|
|
'remote' => $remote,
|
|
'status' => $statusCode,
|
|
]);
|
|
return null;
|
|
}
|
|
|
|
$data = json_decode($response->getBody(), true);
|
|
|
|
if (!is_array($data)) {
|
|
$this->logger->warning('Token exchange response is not valid JSON', ['remote' => $remote]);
|
|
return null;
|
|
}
|
|
|
|
$accessToken = $data['access_token'] ?? null;
|
|
$tokenType = $data['token_type'] ?? null;
|
|
|
|
if (!is_string($accessToken) || $accessToken === '') {
|
|
$this->logger->warning('Token exchange response missing or invalid access_token', ['remote' => $remote]);
|
|
return null;
|
|
}
|
|
|
|
if (!is_string($tokenType) || strtolower($tokenType) !== 'bearer') {
|
|
$this->logger->warning('Token exchange response has unexpected token_type', [
|
|
'remote' => $remote,
|
|
'token_type' => $tokenType,
|
|
]);
|
|
return null;
|
|
}
|
|
|
|
$this->logger->debug('Successfully exchanged token for access token', ['remote' => $remote]);
|
|
return $accessToken;
|
|
} catch (\Exception $e) {
|
|
$this->logger->warning('Failed to exchange token', ['remote' => $remote, 'exception' => $e]);
|
|
return null;
|
|
}
|
|
}
|
|
}
|